Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Repo Notes

- This repo uses React Compiler. Assume React Compiler patterns are enabled when editing React code, and avoid adding `useMemo`/`useCallback` by default unless they are clearly needed for correctness or compatibility with existing code.
- When a component reads multiple adjacent values from the same store hook, prefer a consolidated selector with `C.useShallow(...)` instead of multiple separate subscriptions.
31 changes: 22 additions & 9 deletions shared/team-building/contacts.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 7 additions & 5 deletions shared/team-building/filtered-service-tab-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T.TB.ServiceIdWithContact>) =>
filterServices
? TeamBuilding.allServices.filter(serviceId => filterServices.includes(serviceId))
: TeamBuilding.allServices

export const FilteredServiceTabBar = (
props: Omit<React.ComponentPropsWithoutRef<typeof ServiceTabBar>, 'services'> & {
filterServices?: Array<T.TB.ServiceIdWithContact>
}
) => {
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 : (
<ServiceTabBar
services={services}
Expand Down
235 changes: 158 additions & 77 deletions shared/team-building/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {serviceIdToSearchPlaceholder} from './shared'
import {FilteredServiceTabBar} from './filtered-service-tab-bar'
import {useSharedValue} from '@/common-adapters/reanimated'

const deriveTeamSoFar = (teamSoFar: ReadonlySet<T.TB.User>): Array<T.TB.SelectedUser> =>
const deriveSelectedUsers = (teamSoFar: ReadonlySet<T.TB.User>): Array<T.TB.SelectedUser> =>
[...teamSoFar].map(userInfo => {
let username = ''
let serviceId: T.TB.ServiceIdWithContact
Expand All @@ -41,6 +41,139 @@ const deriveTeamSoFar = (teamSoFar: ReadonlySet<T.TB.User>): Array<T.TB.Selected
}
})

const getUserResults = (
searchResults: T.TB.SearchResults,
searchString: string,
selectedService: T.TB.ServiceIdWithContact
) => searchResults.get(searchString.trim())?.get(selectedService)

const findUserById = (users: ReadonlyArray<T.TB.User> | undefined, userId: string) =>
users?.find(user => user.id === userId)

const shouldShowContactsBanner = (
filterServices: ReadonlyArray<T.TB.ServiceIdWithContact> | 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<T.TB.User> | undefined
userRecs: ReadonlyArray<T.TB.User> | undefined
setFocusInputCounter: React.Dispatch<React.SetStateAction<number>>
setHighlightedIndex: React.Dispatch<React.SetStateAction<number>>
setSearchString: React.Dispatch<React.SetStateAction<string>>
setSelectedService: React.Dispatch<React.SetStateAction<T.TB.ServiceIdWithContact>>
}) => {
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
Expand All @@ -50,12 +183,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)
Expand All @@ -70,79 +198,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<T.TB.User> | 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,
userResults,
userRecs,
setFocusInputCounter,
setHighlightedIndex,
setSearchString,
setSelectedService,
})

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)

Expand Down Expand Up @@ -235,10 +319,7 @@ const TeamBuilding = (p: OwnProps) => {
)

const errorBanner = !!error && <Kb.Banner color="red">{error}</Kb.Banner>

// 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 (
<>
Expand Down
Loading