diff --git a/AGENTS.md b/AGENTS.md index c237fbb17c92..3be5f33c3ff5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,7 @@ - Treat React mount/unmount effects as Strict-Mode-safe. Do not assume a component only mounts once; route-driven async startup and cleanup logic must be idempotent and must not leave refs or guards stuck false after a dev remount. - When using mount guards like `mountedRef`/`isMountedRef`, always set the ref to `true` inside the effect body and set it to `false` in cleanup. Never rely on `useRef(true)` alone across the component lifetime, because Strict Mode remounts can leave the guard stuck `false` and silently drop async results. - When a component reads multiple adjacent values from the same store hook, prefer a consolidated selector with `C.useShallow(...)` instead of multiple separate subscriptions. +- Keep types accurate. Do not use casts or misleading annotations to mask a real type mismatch just to get around an issue; fix the type or fix the implementation. - Do not add new exported functions, types, or constants unless they are required outside the file. Prefer file-local helpers for one-off implementation details and tests. - During refactors, do not delete existing guards, conditionals, or platform/test-specific behavior unless you have proven they are dead and the user asked for that behavior change. Port checks like `androidIsTestDevice` forward into the new code path instead of silently dropping them. - When addressing PR or review feedback, including bot or lint-style suggestions, do not apply it mechanically. Verify that the reported issue is real in this codebase and that the proposed fix is consistent with repo rules and improves correctness, behavior, or maintainability before making changes. diff --git a/shared/constants/types/team-building.tsx b/shared/constants/types/team-building.tsx index 119d8ae041b9..8485c5db0725 100644 --- a/shared/constants/types/team-building.tsx +++ b/shared/constants/types/team-building.tsx @@ -36,7 +36,7 @@ export type SearchKey = Array // Keyed so that we never get results that don't match the user's input (e.g. outdated results) export type Query = string -export type SearchResults = Map>> +export type SearchResults = Map>> export type ServiceResultCount = Map> export type SelectedUser = { diff --git a/shared/team-building/contacts.tsx b/shared/team-building/contacts.tsx index e3d63325c69c..8eda6c68cf6f 100644 --- a/shared/team-building/contacts.tsx +++ b/shared/team-building/contacts.tsx @@ -1,19 +1,32 @@ import * as React from 'react' +import * as C from '@/constants' import * as Kb from '@/common-adapters' import type * as T from '@/constants/types' import {useSettingsContactsState} from '@/stores/settings-contacts' import {useTBContext} from '@/stores/team-building' const useContactsProps = () => { - const contactsImported = useSettingsContactsState(s => s.importEnabled) - const contactsPermissionStatus = useSettingsContactsState(s => s.permissionStatus) - const isImportPromptDismissed = useSettingsContactsState(s => s.importPromptDismissed) - const numContactsImported = useSettingsContactsState(s => s.importedCount || 0) - - const importContactsLater = useSettingsContactsState(s => s.dispatch.importContactsLater) - const loadContactImportEnabled = useSettingsContactsState(s => s.dispatch.loadContactImportEnabled) - const editContactImportEnabled = useSettingsContactsState(s => s.dispatch.editContactImportEnabled) - const requestPermissions = useSettingsContactsState(s => s.dispatch.requestPermissions) + const { + contactsImported, + contactsPermissionStatus, + editContactImportEnabled, + importContactsLater, + isImportPromptDismissed, + loadContactImportEnabled, + numContactsImported, + requestPermissions, + } = useSettingsContactsState( + C.useShallow(s => ({ + contactsImported: s.importEnabled, + contactsPermissionStatus: s.permissionStatus, + editContactImportEnabled: s.dispatch.editContactImportEnabled, + importContactsLater: s.dispatch.importContactsLater, + isImportPromptDismissed: s.importPromptDismissed, + loadContactImportEnabled: s.dispatch.loadContactImportEnabled, + numContactsImported: s.importedCount || 0, + requestPermissions: s.dispatch.requestPermissions, + })) + ) const onAskForContactsLater = importContactsLater const onLoadContactsSetting = loadContactImportEnabled diff --git a/shared/team-building/filtered-service-tab-bar.tsx b/shared/team-building/filtered-service-tab-bar.tsx index cd11373dfeb7..e2e3aae2899e 100644 --- a/shared/team-building/filtered-service-tab-bar.tsx +++ b/shared/team-building/filtered-service-tab-bar.tsx @@ -3,17 +3,19 @@ import type * as T from '@/constants/types' import {ServiceTabBar} from './service-tab-bar' import * as TeamBuilding from '@/stores/team-building' +const getVisibleServices = (filterServices?: Array) => + filterServices + ? TeamBuilding.allServices.filter(serviceId => filterServices.includes(serviceId)) + : TeamBuilding.allServices + export const FilteredServiceTabBar = ( props: Omit, 'services'> & { filterServices?: Array } ) => { - const {selectedService, onChangeService} = props - const {servicesShown, minimalBorder, offset, filterServices} = props + const {selectedService, onChangeService, servicesShown, minimalBorder, offset, filterServices} = props + const services = getVisibleServices(filterServices) - const services = filterServices - ? TeamBuilding.allServices.filter(serviceId => filterServices.includes(serviceId)) - : TeamBuilding.allServices return services.length === 1 && services[0] === 'keybase' ? null : ( ): Array => +const deriveSelectedUsers = (teamSoFar: ReadonlySet): Array => [...teamSoFar].map(userInfo => { let username = '' let serviceId: T.TB.ServiceIdWithContact @@ -41,6 +41,135 @@ const deriveTeamSoFar = (teamSoFar: ReadonlySet): Array searchResults.get(searchString.trim())?.get(selectedService) + +const findUserById = (users: ReadonlyArray | undefined, userId: string) => + users?.find(user => user.id === userId) + +const shouldShowContactsBanner = (filterServices: ReadonlyArray | undefined) => + Kb.Styles.isMobile && (!filterServices || filterServices.includes('phone')) + +const useTeamBuildingData = (searchString: string, selectedService: T.TB.ServiceIdWithContact) => { + const {searchResults, error, rawTeamSoFar, userRecs} = TB.useTBContext( + C.useShallow(s => ({ + error: s.error, + rawTeamSoFar: s.teamSoFar, + searchResults: s.searchResults, + userRecs: s.userRecs, + })) + ) + + return { + error, + teamSoFar: deriveSelectedUsers(rawTeamSoFar), + userRecs, + userResults: getUserResults(searchResults, searchString, selectedService), + } +} + +const useTeamBuildingActions = ({ + namespace, + searchString, + selectedService, + userResults, + userRecs, + setFocusInputCounter, + setHighlightedIndex, + setSearchString, + setSelectedService, +}: { + namespace: T.TB.AllowedNamespace + searchString: string + selectedService: T.TB.ServiceIdWithContact + userResults: ReadonlyArray | undefined + userRecs: ReadonlyArray | undefined + setFocusInputCounter: React.Dispatch> + setHighlightedIndex: React.Dispatch> + setSearchString: React.Dispatch> + setSelectedService: React.Dispatch> +}) => { + const { + addUsersToTeamSoFar, + cancelTeamBuilding, + dispatchSearch, + fetchUserRecs, + finishTeamBuilding, + finishedTeamBuilding, + removeUsersFromTeamSoFar, + } = TB.useTBContext( + C.useShallow(s => ({ + addUsersToTeamSoFar: s.dispatch.addUsersToTeamSoFar, + cancelTeamBuilding: s.dispatch.cancelTeamBuilding, + dispatchSearch: s.dispatch.search, + fetchUserRecs: s.dispatch.fetchUserRecs, + finishTeamBuilding: s.dispatch.finishTeamBuilding, + finishedTeamBuilding: s.dispatch.finishedTeamBuilding, + removeUsersFromTeamSoFar: s.dispatch.removeUsersFromTeamSoFar, + })) + ) + + const search = C.useThrottledCallback( + (query: string, service: T.TB.ServiceIdWithContact, limit?: number) => { + dispatchSearch(query, service, namespace === 'chat', limit) + }, + 500 + ) + + const focusInput = () => { + setFocusInputCounter(old => old + 1) + } + + const onChangeText = (newText: string) => { + setSearchString(newText) + search(newText, selectedService) + setHighlightedIndex(0) + } + + const onAdd = (userId: string) => { + const user = findUserById(userResults, userId) ?? findUserById(userRecs, userId) + if (!user) { + logger.error(`Couldn't find Types.User to add for ${userId}`) + onChangeText('') + return + } + + onChangeText('') + addUsersToTeamSoFar([user]) + setHighlightedIndex(-1) + focusInput() + } + + const onChangeService = (service: T.TB.ServiceIdWithContact) => { + setSelectedService(service) + focusInput() + if (!T.TB.isContactServiceId(service)) { + search(searchString, service) + } + } + + return { + cancelTeamBuilding, + fetchUserRecs, + onAdd, + onChangeService, + onChangeText, + onFinishTeamBuilding: namespace === 'teams' ? finishTeamBuilding : finishedTeamBuilding, + onRemove: (userId: string) => { + removeUsersFromTeamSoFar([userId]) + }, + onSearchForMore: (len: number) => { + if (len >= 10) { + search(searchString, selectedService, len + 20) + } + }, + search, + } +} + type OwnProps = { namespace: T.TB.AllowedNamespace teamID?: string @@ -50,12 +179,7 @@ type OwnProps = { recommendedHideYourself?: boolean } -const TeamBuilding = (p: OwnProps) => { - const namespace = p.namespace - const teamID = p.teamID - const filterServices = p.filterServices - const goButtonLabel = p.goButtonLabel ?? 'Start' - +const TeamBuilding = ({namespace, teamID, filterServices, goButtonLabel = 'Start'}: OwnProps) => { const [focusInputCounter, setFocusInputCounter] = React.useState(0) const [enterInputCounter, setEnterInputCounter] = React.useState(0) const [highlightedIndex, setHighlightedIndex] = React.useState(0) @@ -70,79 +194,35 @@ const TeamBuilding = (p: OwnProps) => { setHighlightedIndex(old => (old < 1 ? 0 : old - 1)) } - const incFocusInputCounter = () => { - setFocusInputCounter(old => old + 1) - } - const onEnterKeyDown = () => { setEnterInputCounter(old => old + 1) } - const searchResults = TB.useTBContext(s => s.searchResults) - const error = TB.useTBContext(s => s.error) - const _teamSoFar = TB.useTBContext(s => s.teamSoFar) - const userRecs = TB.useTBContext(s => s.userRecs) - - const userResults: ReadonlyArray | undefined = searchResults - .get(searchString.trim()) - ?.get(selectedService) - - const teamSoFar = deriveTeamSoFar(_teamSoFar) - - const cancelTeamBuilding = TB.useTBContext(s => s.dispatch.cancelTeamBuilding) - const finishTeamBuilding = TB.useTBContext(s => s.dispatch.finishTeamBuilding) - const finishedTeamBuilding = TB.useTBContext(s => s.dispatch.finishedTeamBuilding) - const removeUsersFromTeamSoFar = TB.useTBContext(s => s.dispatch.removeUsersFromTeamSoFar) - const addUsersToTeamSoFar = TB.useTBContext(s => s.dispatch.addUsersToTeamSoFar) - const fetchUserRecs = TB.useTBContext(s => s.dispatch.fetchUserRecs) - - const _search = TB.useTBContext(s => s.dispatch.search) - const search = C.useThrottledCallback( - (query: string, service: T.TB.ServiceIdWithContact, limit?: number) => { - _search(query, service, namespace === 'chat', limit) - }, - 500 - ) + const {error, teamSoFar, userRecs, userResults} = useTeamBuildingData(searchString, selectedService) + const { + cancelTeamBuilding, + fetchUserRecs, + onAdd, + onChangeService, + onChangeText, + onFinishTeamBuilding, + onRemove, + onSearchForMore, + search, + } = useTeamBuildingActions({ + namespace, + searchString, + selectedService, + setFocusInputCounter, + setHighlightedIndex, + setSearchString, + setSelectedService, + userRecs, + userResults, + }) const onClose = cancelTeamBuilding - const onFinishTeamBuilding = namespace === 'teams' ? finishTeamBuilding : finishedTeamBuilding - const onRemove = (userId: string) => { - removeUsersFromTeamSoFar([userId]) - } - - const onChangeText = (newText: string) => { - setSearchString(newText) - search(newText, selectedService) - setHighlightedIndex(0) - } - const onClear = () => onChangeText('') - const onSearchForMore = (len: number) => { - if (len >= 10) { - search(searchString, selectedService, len + 20) - } - } - const onAdd = (userId: string) => { - const user = userResults?.filter(u => u.id === userId)[0] ?? userRecs?.filter(u => u.id === userId)[0] - - if (!user) { - logger.error(`Couldn't find Types.User to add for ${userId}`) - onChangeText('') - return - } - onChangeText('') - addUsersToTeamSoFar([user]) - setHighlightedIndex(-1) - incFocusInputCounter() - } - - const onChangeService = (service: T.TB.ServiceIdWithContact) => { - setSelectedService(service) - incFocusInputCounter() - if (!T.TB.isContactServiceId(service)) { - search(searchString, service) - } - } const waitingForCreate = C.Waiting.useAnyWaiting(C.waitingKeyChatCreating) @@ -207,7 +287,6 @@ const TeamBuilding = (p: OwnProps) => { teamSoFar={teamSoFar} onChangeText={onChangeText} onSearchForMore={onSearchForMore} - offset={offset} onFinishTeamBuilding={onFinishTeamBuilding} /> {waitingForCreate && ( @@ -235,10 +314,7 @@ const TeamBuilding = (p: OwnProps) => { ) const errorBanner = !!error && {error} - - // If there are no filterServices or if the filterServices has a phone - const showContactsBanner = Kb.Styles.isMobile && (!filterServices || filterServices.includes('phone')) - + const showContactsBanner = shouldShowContactsBanner(filterServices) return ( <> @@ -266,22 +342,19 @@ const TeamBuilding = (p: OwnProps) => { ) } -const styles = Kb.Styles.styleSheetCreate( - () => - ({ - container: Kb.Styles.platformStyles({ - common: {...Kb.Styles.globalStyles.flexOne}, - isElectron: {minHeight: 500}, - }), - waiting: { - ...Kb.Styles.globalStyles.fillAbsolute, - backgroundColor: Kb.Styles.globalColors.black_20, - }, - waitingProgress: { - height: 48, - width: 48, - }, - }) -) +const styles = Kb.Styles.styleSheetCreate(() => ({ + container: Kb.Styles.platformStyles({ + common: {...Kb.Styles.globalStyles.flexOne}, + isElectron: {minHeight: 500}, + }), + waiting: { + ...Kb.Styles.globalStyles.fillAbsolute, + backgroundColor: Kb.Styles.globalColors.black_20, + }, + waitingProgress: { + height: 48, + width: 48, + }, +})) export default TeamBuilding diff --git a/shared/team-building/list-body.tsx b/shared/team-building/list-body.tsx index e905776f41ed..c3f5aaacbb4c 100644 --- a/shared/team-building/list-body.tsx +++ b/shared/team-building/list-body.tsx @@ -16,11 +16,30 @@ import {useRoute} from '@react-navigation/native' import {useSettingsContactsState} from '@/stores/settings-contacts' import {useFollowerState} from '@/stores/followers' import {useCurrentUserState} from '@/stores/current-user' -// import {useAnimatedScrollHandler} from '@/common-adapters/reanimated' import {useColorScheme} from 'react-native' -const Suggestions = (props: Pick) => { - const {namespace, selectedService} = props +type SuggestionsProps = { + namespace: T.TB.AllowedNamespace + selectedService: T.TB.ServiceIdWithContact +} + +type ListBodyProps = { + namespace: T.TB.AllowedNamespace + searchString: string + selectedService: T.TB.ServiceIdWithContact + highlightedIndex: number + onAdd: (userId: string) => void + onRemove: (userId: string) => void + teamSoFar: ReadonlyArray + onSearchForMore: (len: number) => void + onChangeText: (newText: string) => void + onFinishTeamBuilding: () => void + enterInputCounter: number +} + +type DerivedResults = ReturnType + +const Suggestions = ({namespace, selectedService}: SuggestionsProps) => { const isDarkMode = useColorScheme() === 'dark' return ( ) ) } -function isKeybaseUserId(userId: string) { - // Only keybase user id's do not have - return !userId.includes('@') -} +const isKeybaseUserId = (userId: string) => !userId.includes('@') -function followStateHelperWithId( - me: string, - followingState: ReadonlySet, - userId: string = '' -): T.TB.FollowingState { - if (isKeybaseUserId(userId)) { - if (userId === me) { - return 'You' - } else { - return followingState.has(userId) ? 'Following' : 'NotFollowing' - } +const getFollowingState = ( + myUsername: string, + following: ReadonlySet, + userId = '' +): T.TB.FollowingState => { + if (!isKeybaseUserId(userId)) { + return 'NoState' } - return 'NoState' + + if (userId === myUsername) { + return 'You' + } + + return following.has(userId) ? 'Following' : 'NotFollowing' } const deriveSearchResults = ( - searchResults: ReadonlyArray | undefined, + users: ReadonlyArray | undefined, teamSoFar: ReadonlySet, myUsername: string, - followingState: ReadonlySet, + following: ReadonlySet, preExistingTeamMembers: ReadonlyMap -) => - searchResults?.map(info => { +) => { + const teamMemberIds = new Set([...teamSoFar].map(user => user.id)) + + return users?.map(info => { const label = info.label || '' return { contact: !!info.contact, displayLabel: formatAnyPhoneNumbers(label), - followingState: followStateHelperWithId(myUsername, followingState, info.serviceMap.keybase), - inTeam: [...teamSoFar].some(u => u.id === info.id), + followingState: getFollowingState(myUsername, following, info.serviceMap.keybase), + inTeam: teamMemberIds.has(info.id), isPreExistingTeamMember: preExistingTeamMembers.has(info.id), isYou: info.username === myUsername, key: [info.id, info.prettyName, info.label, String(!!info.contact)].join('&'), @@ -105,214 +124,313 @@ const deriveSearchResults = ( username: info.username, } }) - -// Flatten list of recommendation sections. After recommendations are organized -// in sections, we also need a flat list of all recommendations to be able to -// know how many we have in total (including "fake" "import contacts" row), and -// which one is currently highlighted, to support keyboard events. -// -// Resulting list may have nulls in place of fake rows. -const flattenRecommendations = (recommendations: Array) => { - const result: Array = [] - for (const section of recommendations) { - result.push( - ...section.data.map(rec => ('isImportButton' in rec || 'isSearchHint' in rec ? undefined : rec)) - ) - } - return result } +const toSelectableRecommendation = (rec: Types.ResultData) => + 'isImportButton' in rec || 'isSearchHint' in rec ? undefined : rec + +const flattenRecommendations = (recommendations: ReadonlyArray) => + recommendations.reduce>((results, section) => { + results.push(...section.data.map(toSelectableRecommendation)) + return results + }, []) + const alphabet = 'abcdefghijklmnopqrstuvwxyz' const aCharCode = alphabet.charCodeAt(0) const alphaSet = new Set(alphabet) const isAlpha = (letter: string) => alphaSet.has(letter) const letterToAlphaIndex = (letter: string) => letter.charCodeAt(0) - aCharCode +const createSection = ( + label: string, + shortcut: boolean, + data: Array = [] +): Types.SearchRecSection => ({ + data, + label, + shortcut, +}) + +const getRecommendationsSectionIndex = ( + rec: Types.SearchResult, + recommendationIndex: number, + numericSectionIndex: number +) => { + if (!rec.contact) { + return recommendationIndex + } + + const displayName = rec.prettyName || rec.displayLabel + const firstLetter = displayName[0]?.toLowerCase() + if (!firstLetter) { + return undefined + } + + return isAlpha(firstLetter) + ? letterToAlphaIndex(firstLetter) + recommendationIndex + 1 + : numericSectionIndex +} + // Returns array with 28 entries // 0 - "Recommendations" section // 1-26 - a-z sections // 27 - 0-9 section const sortAndSplitRecommendations = ( - results: T.Unpacked, + results: DerivedResults, showingContactsButton: boolean ): Array | undefined => { - if (!results) return undefined - - const sections: Array = [ - ...(showingContactsButton - ? [ - { - data: [{isImportButton: true as const}], - label: '', - shortcut: false, - }, - ] - : []), - - { - data: [], - label: 'Recommendations', - shortcut: false, - }, - ] - const recSectionIdx = sections.length - 1 - const numSectionIdx = recSectionIdx + 27 + if (!results) { + return undefined + } + + const sections: Array = [] + if (showingContactsButton) { + sections.push(createSection('', false, [{isImportButton: true}])) + } + sections.push(createSection('Recommendations', false)) + + const recommendationIndex = sections.length - 1 + const numericSectionIndex = recommendationIndex + 27 + results.forEach(rec => { - if (!rec.contact) { - sections[recSectionIdx]?.data.push(rec) + const sectionIndex = getRecommendationsSectionIndex(rec, recommendationIndex, numericSectionIndex) + if (sectionIndex === undefined) { return } - if (rec.prettyName || rec.displayLabel) { - // Use the first letter of the name we will display, but first normalize out - // any diacritics. - const decodedLetter = /*unidecode*/ rec.prettyName || rec.displayLabel - if (decodedLetter[0]) { - const letter = decodedLetter[0].toLowerCase() - if (isAlpha(letter)) { - // offset 1 to skip recommendations - const sectionIdx = letterToAlphaIndex(letter) + recSectionIdx + 1 - if (!sections[sectionIdx]) { - sections[sectionIdx] = { - data: [], - label: letter.toUpperCase(), - shortcut: true, - } - } - sections[sectionIdx].data.push(rec) - } else { - if (!sections[numSectionIdx]) { - sections[numSectionIdx] = { - data: [], - label: numSectionLabel, - shortcut: true, - } - } - sections[numSectionIdx].data.push(rec) - } - } + + if (!sections[sectionIndex]) { + const isNumericSection = sectionIndex === numericSectionIndex + const label = isNumericSection + ? numSectionLabel + : String.fromCharCode(aCharCode + sectionIndex - recommendationIndex - 1).toUpperCase() + sections[sectionIndex] = createSection(label, true) } + sections[sectionIndex].data.push(rec) }) + if (results.length < 5) { - sections.push({ - data: [{isSearchHint: true as const}], - label: '', - shortcut: false, - }) + sections.push(createSection('', false, [{isSearchHint: true}])) } - return sections.filter(s => s.data.length > 0) + + return sections.filter(section => section.data.length > 0) } -const emptyMap = new Map() - -export const ListBody = ( - props: Pick< - Types.Props, - | 'namespace' - | 'searchString' - | 'selectedService' - | 'highlightedIndex' - | 'onAdd' - | 'onRemove' - | 'teamSoFar' - | 'onSearchForMore' - | 'onChangeText' - | 'onFinishTeamBuilding' - > & { - offset: unknown - enterInputCounter: number - } +const getSearchResults = ( + searchResults: TB.State['searchResults'], + searchString: string, + selectedService: T.TB.ServiceIdWithContact +) => searchResults.get(searchString.trim())?.get(selectedService) + +const getSelectableResults = ( + showRecs: boolean, + recommendations: ReadonlyArray | undefined, + searchResults: DerivedResults +) => (showRecs ? flattenRecommendations(recommendations ?? []) : searchResults) + +const getHighlightedResult = ( + highlightedIndex: number, + userResults: ReturnType ) => { - const {params} = useRoute>() - const recommendedHideYourself = params.recommendedHideYourself ?? false - const teamID = params.teamID - const {searchString, selectedService} = props - const {onAdd, onRemove, teamSoFar, onSearchForMore, onChangeText} = props - const {namespace, highlightedIndex, /*offset, */ enterInputCounter, onFinishTeamBuilding} = props + if (!userResults?.length) { + return undefined + } + + return userResults[highlightedIndex % userResults.length] +} - const contactsImported = useSettingsContactsState(s => s.importEnabled) - const contactsPermissionStatus = useSettingsContactsState(s => s.permissionStatus) +const emptyMap = new Map() +const useListBodyData = ({ + searchString, + selectedService, + teamID, +}: { + searchString: string + selectedService: T.TB.ServiceIdWithContact + teamID?: T.Teams.TeamID +}) => { + const {contactsImported, contactsPermissionStatus} = useSettingsContactsState( + C.useShallow(s => ({ + contactsImported: s.importEnabled, + contactsPermissionStatus: s.permissionStatus, + })) + ) const username = useCurrentUserState(s => s.username) const following = useFollowerState(s => s.following) - const maybeTeamDetails = useTeamsState(s => (teamID ? s.teamDetails.get(teamID) : undefined)) const preExistingTeamMembers: T.Teams.TeamDetails['members'] = maybeTeamDetails?.members ?? emptyMap - const userRecs = TB.useTBContext(s => s.userRecs) - const _teamSoFar = TB.useTBContext(s => s.teamSoFar) - const _searchResults = TB.useTBContext(s => s.searchResults) - const _recommendations = deriveSearchResults(userRecs, _teamSoFar, username, following, preExistingTeamMembers) - - const userResults: ReadonlyArray | undefined = _searchResults - .get(searchString.trim()) - ?.get(selectedService) + const {allSearchResults, teamSoFar, userRecs} = TB.useTBContext( + C.useShallow(s => ({ + allSearchResults: s.searchResults, + teamSoFar: s.teamSoFar, + userRecs: s.userRecs, + })) + ) - const searchResults = deriveSearchResults(userResults, _teamSoFar, username, following, preExistingTeamMembers) + const recommendationResults = deriveSearchResults( + userRecs, + teamSoFar, + username, + following, + preExistingTeamMembers + ) + const userResults = getSearchResults(allSearchResults, searchString, selectedService) + const searchResults = deriveSearchResults( + userResults, + teamSoFar, + username, + following, + preExistingTeamMembers + ) const showResults = !!searchString - const showRecs = !searchString && !!_recommendations && selectedService === 'keybase' - - const ResultRow = namespace === 'people' ? PeopleResult : UserResult - const showLoading = !!searchString && !searchResults - + const showRecs = !searchString && !!recommendationResults && selectedService === 'keybase' const showingContactsButton = C.isMobile && contactsPermissionStatus !== 'denied' && !contactsImported - const recommendations = showRecs ? sortAndSplitRecommendations(_recommendations, showingContactsButton) : undefined + const recommendations = showRecs + ? sortAndSplitRecommendations(recommendationResults, showingContactsButton) + : undefined - const showRecPending = !searchString && !recommendations && selectedService === 'keybase' + return { + recommendations, + searchResults, + showLoading: !!searchString && !searchResults, + showRecPending: !searchString && !recommendations && selectedService === 'keybase', + showRecs, + showResults, + } +} +const useEnterKeyHandler = ({ + enterInputCounter, + highlightedIndex, + onAdd, + onChangeText, + onFinishTeamBuilding, + onRemove, + recommendations, + searchResults, + searchString, + showRecs, + teamSoFar, +}: { + enterInputCounter: number + highlightedIndex: number + onAdd: (userId: string) => void + onChangeText: (newText: string) => void + onFinishTeamBuilding: () => void + onRemove: (userId: string) => void + recommendations: ReadonlyArray | undefined + searchResults: DerivedResults + searchString: string + showRecs: boolean + teamSoFar: ReadonlyArray +}) => { const lastEnterInputCounterRef = React.useRef(enterInputCounter) + React.useEffect(() => { - if (lastEnterInputCounterRef.current !== enterInputCounter) { - lastEnterInputCounterRef.current = enterInputCounter - const userResultsToShow = showRecs ? flattenRecommendations(recommendations ?? []) : searchResults - const selectedResult = - !!userResultsToShow && userResultsToShow[highlightedIndex % userResultsToShow.length] - if (selectedResult) { - // We don't handle cases where they hit enter on someone that is already a - // team member - if (selectedResult.isPreExistingTeamMember) { - return - } - if (teamSoFar.filter(u => u.userId === selectedResult.userId).length) { - onRemove(selectedResult.userId) - onChangeText('') - } else { - onAdd(selectedResult.userId) - } - } else if (!searchString && !!teamSoFar.length) { - // They hit enter with an empty search string and a teamSoFar - // We'll Finish the team building - onFinishTeamBuilding() + if (lastEnterInputCounterRef.current === enterInputCounter) { + return + } + + lastEnterInputCounterRef.current = enterInputCounter + const selectableResults = getSelectableResults(showRecs, recommendations, searchResults) + const selectedResult = getHighlightedResult(highlightedIndex, selectableResults) + + if (selectedResult) { + if (selectedResult.isPreExistingTeamMember) { + return } + + if (teamSoFar.some(user => user.userId === selectedResult.userId)) { + onRemove(selectedResult.userId) + onChangeText('') + } else { + onAdd(selectedResult.userId) + } + return + } + + if (!searchString && teamSoFar.length) { + onFinishTeamBuilding() } }, [ enterInputCounter, - showRecs, + highlightedIndex, + onAdd, + onChangeText, + onFinishTeamBuilding, + onRemove, recommendations, searchResults, - highlightedIndex, + searchString, + showRecs, teamSoFar, - onRemove, - onChangeText, + ]) +} + +const LoadingState = ({showLoading}: {showLoading: boolean}) => ( + + {showLoading && } + +) + +const NoResults = () => ( + + Sorry, no results were found. + +) + +export const ListBody = ({ + namespace, + searchString, + selectedService, + highlightedIndex, + onAdd, + onRemove, + teamSoFar, + onSearchForMore, + onChangeText, + onFinishTeamBuilding, + enterInputCounter, +}: ListBodyProps) => { + const {params} = useRoute>() + const recommendedHideYourself = params.recommendedHideYourself ?? false + const teamID = params.teamID + const ResultRow = namespace === 'people' ? PeopleResult : UserResult + + const {recommendations, searchResults, showLoading, showRecPending, showRecs, showResults} = + useListBodyData({ + searchString, + selectedService, + teamID, + }) + + useEnterKeyHandler({ + enterInputCounter, + highlightedIndex, onAdd, - searchString, + onChangeText, onFinishTeamBuilding, - ]) + onRemove, + recommendations, + searchResults, + searchString, + showRecs, + teamSoFar, + }) if (showRecPending || showLoading) { - return ( - - {showLoading && } - - ) + return } + if (!showRecs && !showResults) { return } @@ -332,53 +450,45 @@ export const ListBody = ( ) } - const _onSearchForMore = () => { + const onEndReached = throttle(() => { onSearchForMore(searchResults?.length ?? 0) - } - - const _onEndReached = throttle(_onSearchForMore, 500) + }, 500) - return ( - <> - {searchResults?.length ? ( - - ( - - )} + return searchResults?.length ? ( + + ( + - - ) : ( - - Sorry, no results were found. - - )} - + )} + /> + + ) : ( + ) } diff --git a/shared/team-building/recs-and-recos.tsx b/shared/team-building/recs-and-recos.tsx index 014eecbda86d..e98ea6118730 100644 --- a/shared/team-building/recs-and-recos.tsx +++ b/shared/team-building/recs-and-recos.tsx @@ -1,5 +1,6 @@ import * as Kb from '@/common-adapters' import * as React from 'react' +import type * as T from '@/constants/types' import AlphabetIndex from './alphabet-index' import PeopleResult from './search-result/people-result' import UserResult from './search-result/user-result' @@ -7,6 +8,24 @@ import type * as Types from './types' import {ContactsImportButton} from './contacts' type RefType = React.RefObject | null> +type TeamSoFar = ReadonlyArray<{userId: string}> + +type TeamAlphabetIndexProps = { + recommendations?: Array + teamSoFar: TeamSoFar + sectionListRef: RefType +} + +type RecsAndRecosProps = { + highlightedIndex: number + recommendations?: Array + namespace: T.TB.AllowedNamespace + selectedService: T.TB.ServiceIdWithContact + onAdd: (userId: string) => void + onRemove: (userId: string) => void + teamSoFar: TeamSoFar + recommendedHideYourself: boolean +} export const numSectionLabel = '0-9' @@ -18,12 +37,7 @@ const SearchHintText = () => ( ) -const TeamAlphabetIndex = ( - props: Pick & { - sectionListRef: RefType - } -) => { - const {recommendations, teamSoFar, sectionListRef} = props +const TeamAlphabetIndex = ({recommendations, teamSoFar, sectionListRef}: TeamAlphabetIndexProps) => { let showNumSection = false let labels: Array = [] if (recommendations && recommendations.length > 0) { @@ -31,7 +45,7 @@ const TeamAlphabetIndex = ( labels = recommendations.filter(r => r.shortcut && r.label !== numSectionLabel).map(r => r.label) } - const _onScrollToSection = (label: string) => { + const onScrollToSection = (label: string) => { if (sectionListRef.current) { const sectionIndex = (recommendations && @@ -54,18 +68,18 @@ const TeamAlphabetIndex = ( } return ( <> - + ) } -const _listIndexToSectionAndLocalIndex = ( +const listIndexToSectionAndLocalIndex = ( highlightedIndex?: number, sections?: Types.SearchRecSection[] ): {index: number; section: Types.SearchRecSection} | undefined => { @@ -81,24 +95,13 @@ const _listIndexToSectionAndLocalIndex = ( } return } -export const RecsAndRecos = ( - props: Pick< - Types.Props, - | 'highlightedIndex' - | 'recommendations' - | 'namespace' - | 'selectedService' - | 'onAdd' - | 'onRemove' - | 'teamSoFar' - > & {recommendedHideYourself: boolean} -) => { +export const RecsAndRecos = (props: RecsAndRecosProps) => { const {highlightedIndex, recommendations, recommendedHideYourself, namespace} = props const {selectedService, onAdd, onRemove, teamSoFar} = props const sectionListRef = React.useRef>(null) const ResultRow = namespace === 'people' ? PeopleResult : UserResult - const highlightDetails = _listIndexToSectionAndLocalIndex(highlightedIndex, recommendations) + const highlightDetails = listIndexToSectionAndLocalIndex(highlightedIndex, recommendations) React.useEffect(() => { highlightedIndex >= 0 && diff --git a/shared/team-building/search-result/common-result.tsx b/shared/team-building/search-result/common-result.tsx index fa3585c1a016..3577c8b55804 100644 --- a/shared/team-building/search-result/common-result.tsx +++ b/shared/team-building/search-result/common-result.tsx @@ -33,6 +33,17 @@ export type CommonResultProps = ResultProps & { rowStyle?: Kb.Styles.StylesCrossPlatform } +type BottomRowProps = { + isKeybaseResult: boolean + username: string + isPreExistingTeamMember: boolean + keybaseUsername?: string + followingState: T.TB.FollowingState + displayLabel: string + prettyName: string + services: {[K in T.TB.ServiceIdWithContact]?: string} +} + /* * Case 1: the service is 'keybase' (isKeybaseResult = true) * @@ -48,6 +59,26 @@ export type CommonResultProps = ResultProps & { * {prettyName} if the user added it. Can fallback to username if no prettyName is set * {service icons} if the user has proofs */ +const getRowAction = (props: ResultProps) => { + if (props.isPreExistingTeamMember) { + return undefined + } + return props.inTeam ? () => props.onRemove(props.userId) : () => props.onAdd(props.userId) +} + +const FallbackResultInfo = ({displayLabel, prettyName}: Pick) => ( + <> + + {prettyName} + + {!!displayLabel && displayLabel !== prettyName && ( + + {displayLabel} + + )} + +) + const CommonResult = (props: CommonResultProps) => { /* * Regardless of the service that is being searched, if we find that a @@ -57,11 +88,10 @@ const CommonResult = (props: CommonResultProps) => { const isKeybaseResult = props.resultForService === 'keybase' const keybaseUsername: string | undefined = props.services['keybase'] const serviceUsername = props.services[props.resultForService] - const onAdd = !props.isPreExistingTeamMember ? () => props.onAdd(props.userId) : undefined - const onRemove = !props.isPreExistingTeamMember ? () => props.onRemove(props.userId) : undefined + const onClick = getRowAction(props) return ( - + { followingState={props.followingState} isKeybaseResult={isKeybaseResult} keybaseUsername={keybaseUsername} - username={serviceUsername || ''} + username={serviceUsername} /> {props.bottomRow ?? ( { keybaseUsername={keybaseUsername} prettyName={props.prettyName} services={props.services} - username={serviceUsername || ''} + username={serviceUsername} /> )} ) : ( - <> - - {props.prettyName} - - {!!props.displayLabel && props.displayLabel !== props.prettyName && ( - - {props.displayLabel} - - )} - + )} const textWithConditionalSeparator = (text: string, conditional: boolean) => `${text}${conditional ? ` ${dotSeparator}` : ''}` +const shouldOmitFirstIconMargin = ({ + displayLabel, + isKeybaseResult, + keybaseUsername, + prettyName, +}: Pick) => + !isKeybaseResult + ? !keybaseUsername && !prettyName && !displayLabel + : prettyName + ? prettyName === keybaseUsername + : !displayLabel + const Avatar = ({ resultForService, keybaseUsername, @@ -163,26 +196,17 @@ const Avatar = ({ } // If service icons are the only item present in the bottom row, then don't apply margin-left to the first icon -const ServicesIcons = (props: { +type ServicesIconsProps = { services: {[K in T.TB.ServiceIdWithContact]?: string} prettyName: string displayLabel: string isKeybaseResult: boolean keybaseUsername?: string -}) => { +} + +const ServicesIcons = (props: ServicesIconsProps) => { const serviceIds = serviceMapToArray(props.services) - // When the result is from a non-keybase service, we could have: - // 1. keybase username - // 2. pretty name or display label. prettyName can fallback to username if no prettyName is set. - // - // When the result is from the keybase service, we could have: - // 1. prettyName that matches the username - in which case it will be hidden - // 1. No prettyName and also no displayLabel - const firstIconNoMargin = !props.isKeybaseResult - ? !props.keybaseUsername && !props.prettyName && !props.displayLabel - : props.prettyName - ? props.prettyName === props.keybaseUsername - : !props.displayLabel + const firstIconNoMargin = shouldOmitFirstIconMargin(props) return ( {serviceIds.map((serviceName, index) => { @@ -244,37 +268,40 @@ const MobileScrollView = ({children}: {children: React.ReactNode}) => <>{children} ) -const BottomRow = (props: { - isKeybaseResult: boolean - username: string - isPreExistingTeamMember: boolean - keybaseUsername?: string +const KeybaseUsernameLabel = ({ + followingState, + keybaseUsername, +}: { followingState: T.TB.FollowingState - displayLabel: string - prettyName: string - services: {[K in T.TB.ServiceIdWithContact]?: string} -}) => { + keybaseUsername: string +}) => ( + <> + + {keybaseUsername} + +   + {dotSeparator} +   + +) + +const BottomRow = (props: BottomRowProps) => { const serviceUserIsAlsoKeybaseUser = !props.isKeybaseResult && props.keybaseUsername const showServicesIcons = props.isKeybaseResult || !!props.keybaseUsername - const keybaseUsernameComponent = serviceUserIsAlsoKeybaseUser ? ( - <> - - {props.keybaseUsername} - -   - {dotSeparator} -   - - ) : null return ( - {keybaseUsernameComponent} + {serviceUserIsAlsoKeybaseUser && props.keybaseUsername ? ( + + ) : null} {props.isPreExistingTeamMember ? ( {isPreExistingTeamMemberText(props.prettyName, props.username)} @@ -313,16 +340,18 @@ const Username = (props: { isKeybaseResult: boolean keybaseUsername?: string username: string -}) => ( - - {props.username} - -) +}) => { + const showFollowingState = props.isKeybaseResult && props.keybaseUsername + + return ( + + {props.username} + + ) +} export const userResultHeight = Kb.Styles.isMobile ? Kb.Styles.globalMargins.xlarge : 48 const styles = Kb.Styles.styleSheetCreate(() => ({ @@ -362,8 +391,8 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ }, })) -const followingStateToStyle = (followingState: T.TB.FollowingState) => { - return { +const followingStateToStyle = (followingState: T.TB.FollowingState) => + ({ Following: { color: Kb.Styles.globalColors.greenDark, }, @@ -376,7 +405,6 @@ const followingStateToStyle = (followingState: T.TB.FollowingState) => { You: { color: Kb.Styles.globalColors.black, }, - }[followingState] -} + })[followingState] export default CommonResult diff --git a/shared/team-building/search-result/people-result.tsx b/shared/team-building/search-result/people-result.tsx index 719314533da1..574ba1c71462 100644 --- a/shared/team-building/search-result/people-result.tsx +++ b/shared/team-building/search-result/people-result.tsx @@ -52,19 +52,13 @@ const PeopleResult = function PeopleResult(props: ResultProps) { } const resultIsMe = keybaseUsername === myUsername - const dropdown = keybaseUsername ? ( + const dropdown = ( - ) : ( - ) @@ -97,44 +91,49 @@ type DropdownProps = { onUnfollow?: () => void } +const buildMenuItems = ({ + blocked, + onAddToTeam, + onBrowsePublicFolder, + onManageBlocking, + onOpenPrivateFolder, +}: DropdownProps): Kb.MenuItems => + [ + onAddToTeam && {icon: 'iconfont-add', onClick: onAddToTeam, title: 'Add to team...'}, + onOpenPrivateFolder && { + icon: 'iconfont-folder-open', + onClick: onOpenPrivateFolder, + title: 'Open private folder', + }, + onBrowsePublicFolder && { + icon: 'iconfont-folder-public', + onClick: onBrowsePublicFolder, + title: 'Browse public folder', + }, + onManageBlocking && { + danger: true, + icon: 'iconfont-add', + onClick: onManageBlocking, + title: blocked ? 'Manage blocking' : 'Block', + }, + ].filter(Boolean) as Kb.MenuItems + const DropdownButton = (p: DropdownProps) => { - const {onAddToTeam, onOpenPrivateFolder, onBrowsePublicFolder, onManageBlocking, blocked} = p - const items: Kb.MenuItems = [ - onAddToTeam && {icon: 'iconfont-add', onClick: onAddToTeam, title: 'Add to team...'}, - onOpenPrivateFolder && { - icon: 'iconfont-folder-open', - onClick: onOpenPrivateFolder, - title: 'Open private folder', - }, - onBrowsePublicFolder && { - icon: 'iconfont-folder-public', - onClick: onBrowsePublicFolder, - title: 'Browse public folder', - }, - onManageBlocking && { - danger: true, - icon: 'iconfont-add', - onClick: onManageBlocking, - title: blocked ? 'Manage blocking' : 'Block', - }, - ].reduce((arr, i) => { - i && arr.push(i as Kb.MenuItem) - return arr - }, []) + const items = buildMenuItems(p) const makePopup = (p: Kb.Popup2Parms) => { - const {attachTo, hidePopup} = p - return ( - - ) - } + const {attachTo, hidePopup} = p + return ( + + ) + } const {showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) return ( diff --git a/shared/team-building/service-tab-bar.desktop.tsx b/shared/team-building/service-tab-bar.desktop.tsx index 35455930bfca..977b96d9925a 100644 --- a/shared/team-building/service-tab-bar.desktop.tsx +++ b/shared/team-building/service-tab-bar.desktop.tsx @@ -6,6 +6,26 @@ import type * as T from '@/constants/types' import type {Props, IconProps} from './service-tab-bar' import {useColorScheme} from 'react-native' +const getDesktopServicesLayout = ( + services: ReadonlyArray, + selectedService: T.TB.ServiceIdWithContact, + servicesShown: number, + lastSelectedUnlockedService?: T.TB.ServiceIdWithContact +) => { + const lockedServices = services.slice(0, servicesShown) + const selectedServiceIsLocked = services.indexOf(selectedService) < servicesShown + const frontServices = selectedServiceIsLocked + ? lastSelectedUnlockedService === undefined + ? services.slice(0, servicesShown + 1) + : lockedServices.concat(lastSelectedUnlockedService) + : lockedServices.concat(selectedService) + + return { + frontServices, + moreServices: difference(services, frontServices), + } +} + const ServiceIcon = (props: IconProps) => { const [hover, setHover] = React.useState(false) const isDarkMode = useColorScheme() === 'dark' @@ -72,21 +92,21 @@ const MoreNetworksButton = (props: { }) => { const {services, onChangeService} = props const makePopup = (p: Kb.Popup2Parms) => { - const {attachTo, hidePopup} = p - return ( - ({ - onClick: () => onChangeService(service), - title: service, - view: , - }))} - onHidden={hidePopup} - visible={true} - /> - ) - } + const {attachTo, hidePopup} = p + return ( + ({ + onClick: () => onChangeService(service), + title: service, + view: , + }))} + onHidden={hidePopup} + visible={true} + /> + ) + } const {showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup) @@ -135,24 +155,19 @@ export const ServiceTabBar = (props: Props) => { >() const {services, onChangeService: propsOnChangeService, servicesShown: nLocked = 3} = props const onChangeService = (service: T.TB.ServiceIdWithContact) => { - if (services.indexOf(service) >= nLocked && service !== lastSelectedUnlockedService) { - setLastSelectedUnlockedService(service) - } - propsOnChangeService(service) - } - const lockedServices = services.slice(0, nLocked) - let frontServices = new Array() - if (services.indexOf(props.selectedService) < nLocked) { - // Selected service is locked - if (lastSelectedUnlockedService === undefined) { - frontServices = services.slice(0, nLocked + 1) - } else { - frontServices = lockedServices.concat([lastSelectedUnlockedService]) + if (services.indexOf(service) >= nLocked && service !== lastSelectedUnlockedService) { + setLastSelectedUnlockedService(service) } - } else { - frontServices = lockedServices.concat([props.selectedService]) + propsOnChangeService(service) } - const moreServices = difference(services, frontServices) + + const {frontServices, moreServices} = getDesktopServicesLayout( + services, + props.selectedService, + nLocked, + lastSelectedUnlockedService + ) + return ( {frontServices.map(service => ( diff --git a/shared/team-building/service-tab-bar.native.tsx b/shared/team-building/service-tab-bar.native.tsx index e6b9d09c6336..def651c62d23 100644 --- a/shared/team-building/service-tab-bar.native.tsx +++ b/shared/team-building/service-tab-bar.native.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import * as Kb from '@/common-adapters' import {serviceIdToIconFont, serviceIdToAccentColor, serviceIdToLongLabel, serviceIdToBadge} from './shared' -import type * as T from '@/constants/types' import {ScrollView} from 'react-native' import type {Props, IconProps} from './service-tab-bar' import {useColorScheme} from 'react-native' @@ -41,7 +40,6 @@ const AnimatedScrollView = createAnimatedComponent(ScrollView) // On tablet add an additional "service" item that is only a bottom border that extends to the end of the ScrollView const TabletBottomBorderExtension = function TabletBottomBorderExtension(props: { offset?: SharedValue - servicesCount: number }) { 'use no memo' const {offset} = props @@ -159,9 +157,6 @@ export const ServiceTabBar = (props: Props) => { 'use no memo' const {onChangeService, offset, services, selectedService} = props const bounceX = useSharedValue(40) - const onClick = (service: T.TB.ServiceIdWithContact) => { - onChangeService(service) - } React.useEffect(() => { bounceX.set(0) @@ -208,13 +203,11 @@ export const ServiceTabBar = (props: Props) => { offset={offset} service={service} label={serviceIdToLongLabel(service)} - onClick={onClick} + onClick={onChangeService} isActive={selectedService === service} /> ))} - {Kb.Styles.isTablet ? ( - - ) : null} + {Kb.Styles.isTablet ? : null} ) } diff --git a/shared/team-building/shared.tsx b/shared/team-building/shared.tsx index 9303fdcff0ef..dac68d852aa8 100644 --- a/shared/team-building/shared.tsx +++ b/shared/team-building/shared.tsx @@ -68,25 +68,23 @@ const services: { }, } +const accentColors: {[K in T.TB.ServiceIdWithContact]: string} = { + email: '#3663ea', + facebook: '#3B5998', + github: '#333', + hackernews: '#FF6600', + keybase: '#3663ea', + phone: '#3663ea', + reddit: '#ff4500', + twitter: '#1DA1F2', +} + +const darkModeAccentColors: Partial = { + github: '#E7E8E8', +} + export const serviceIdToAccentColor = (service: T.TB.ServiceIdWithContact, isDarkMode: boolean): string => { - switch (service) { - case 'email': - return isDarkMode ? '#3663ea' : '#3663ea' - case 'facebook': - return isDarkMode ? '#3B5998' : '#3B5998' - case 'github': - return isDarkMode ? '#E7E8E8' : '#333' - case 'hackernews': - return isDarkMode ? '#FF6600' : '#FF6600' - case 'keybase': - return isDarkMode ? '#3663ea' : '#3663ea' - case 'phone': - return isDarkMode ? '#3663ea' : '#3663ea' - case 'reddit': - return isDarkMode ? '#ff4500' : '#ff4500' - case 'twitter': - return isDarkMode ? '#1DA1F2' : '#1DA1F2' - } + return (isDarkMode && darkModeAccentColors[service]) || accentColors[service] } export const serviceIdToIconFont = (service: T.TB.ServiceIdWithContact): IconType => services[service].icon export const serviceIdToAvatarIcon = (service: T.TB.ServiceIdWithContact): IconType => diff --git a/shared/team-building/types.tsx b/shared/team-building/types.tsx index 9eb722d6df50..6479e5608c73 100644 --- a/shared/team-building/types.tsx +++ b/shared/team-building/types.tsx @@ -33,32 +33,3 @@ export type SearchRecSection = { shortcut: boolean data: Array } - -export type Props = { - error?: string - filterServices?: Array - focusInputCounter: number - goButtonLabel?: T.TB.GoButtonLabel - highlightedIndex: number - namespace: T.TB.AllowedNamespace - onAdd: (userId: string) => void - onChangeService: (newService: T.TB.ServiceIdWithContact) => void - onChangeText: (newText: string) => void - onClear: () => void - onClose: () => void - onDownArrowKeyDown: () => void - onEnterKeyDown: () => void - onFinishTeamBuilding: () => void - onRemove: (userId: string) => void - onSearchForMore: (len: number) => void - onUpArrowKeyDown: () => void - recommendations?: Array - search: (query: string, service: T.TB.ServiceIdWithContact) => void - searchResults: Array | undefined - searchString: string - selectedService: T.TB.ServiceIdWithContact - showServiceResultCount: boolean - teamBuildingSearchResults: T.TB.SearchResults - teamID: T.Teams.TeamID | undefined - teamSoFar: Array -}