import { StackActions, useNavigation } from '@react-navigation/native' import { uniq } from 'lodash' import React, { useCallback, useEffect, useMemo, useState } from 'react' import { StyleSheet, View } from 'react-native' import { Badge, Banner, ChildNotice, Heading, Switch, TextButton } from '../../../components' import { ActionButton } from '../../../components/display/action_button' import { useApiClient } from '../../../hooks' import { useFindOrCreateServicesConversation } from '../../../hooks/services/use_find_or_create_services_conversation' import { useServicesChatConversationPayload } from '../../../hooks/services/use_services_chat_conversation_payload' import { useServicesTeams } from '../../../hooks/services/use_services_team' import { useTeamMembersForNewConversation } from '../../../hooks/services/use_team_members_for_new_conversation' import { type AvatarUpdatePayload, patchConversationAvatar, } from '../../../hooks/use_conversation_avatar_update' import { useConversationValidate } from '../../../hooks/use_conversation_validate' import { availableFeatures, useFeatures } from '../../../hooks/use_features' import { useUploadClient } from '../../../hooks/use_upload_client' import { MemberResource } from '../../../types' import { pluralize } from '../../../utils' import { Haptic } from '../../../utils/native_adapters' import { tokens } from '../../../vendor/tapestry/tokens' import { TeamFilterTypes } from '../../conversation_filter_recipients/types' import { AvatarSelectionRow } from './avatar_selection_row' import { FilterByPlan } from './filter_by_plan' import { Divider, FormList } from './form_list' type ServicesFormProps = { initialTeamIds?: number[] initialPlanId?: number teamFilterType?: TeamFilterTypes avatarSelection?: AvatarUpdatePayload } export const ServicesForm = ({ initialTeamIds, initialPlanId, teamFilterType, avatarSelection, }: ServicesFormProps) => { const styles = useStyles() const apiClient = useApiClient() const uploadClient = useUploadClient() const { featureEnabled } = useFeatures() const customAvatarsEnabled = featureEnabled(availableFeatures.custom_conversation_avatars) const safetyLockEnabled = featureEnabled(availableFeatures.conversation_safety_lock) const [selectedPlanId, setSelectedPlanId] = useState(initialPlanId) const [filterByPlan, setFilterByPlan] = useState(!!initialPlanId) const initialState = useMemo(() => uniq(initialTeamIds) || [], [initialTeamIds]) // Uniq here because services can send duplicates in the teams_i_lead response. const [selectedTeamIds, setSelectedTeamIds] = useState(initialState) // Sync with fresh props when they change (from navigation) useEffect(() => { setSelectedTeamIds(initialState) setSelectedPlanId(initialPlanId) setFilterByPlan(!!initialPlanId) }, [initialState, initialPlanId]) const removeSelection = useCallback( (teamId: number) => { setSelectedTeamIds(selectedTeamIds.filter(id => id !== teamId)) }, [selectedTeamIds] ) const activePlanId = filterByPlan ? selectedPlanId : undefined const { payload, isLoading: isLoadingPayload } = useServicesChatConversationPayload({ teamIds: selectedTeamIds, planId: activePlanId, enabled: safetyLockEnabled, }) const { warnings, validationPending } = useConversationValidate({ payload, isLoadingPayload, enabled: safetyLockEnabled, }) const { members, isError: isMemberError } = useTeamMembersForNewConversation({ teamIds: selectedTeamIds, planId: activePlanId, }) const adultMembers = useMemo(() => members.filter(member => !member.child), [members]) const navigation = useNavigation() const { mutate: createConversation, isPending, selectionHasConversation, isLoadingConversationCheck, } = useFindOrCreateServicesConversation({ teamIds: selectedTeamIds, planId: activePlanId, onSuccess: (conversation, { created }) => { navigation.getParent()?.goBack() navigation.dispatch( StackActions.push('Conversation', { conversation_id: conversation.id, }) ) if (created) { Haptic.notificationSuccess() if (avatarSelection && avatarSelection.kind !== 'clear') { patchConversationAvatar(apiClient, uploadClient, conversation.id, avatarSelection) } } }, }) return ( } /> 0 } title={selectionHasConversation ? 'Open conversation' : 'Start conversation'} onPress={createConversation} infoText="Conversation will be automatically updated if any members are added or removed from included teams." loading={isLoadingConversationCheck} warnings={warnings} /> ) } interface FormContentProps { selectedTeamIds: number[] removeSelection: (teamId: number) => void selectedPlanId?: number setSelectedPlanId: (planId: number | undefined) => void filterByPlan: boolean setFilterByPlan: (value: boolean) => void members: MemberResource[] isMemberError: boolean teamFilterType?: TeamFilterTypes avatarSelection?: AvatarUpdatePayload selectionHasConversation: boolean customAvatarsEnabled: boolean } function FormContent({ selectedTeamIds, removeSelection, selectedPlanId, setSelectedPlanId, filterByPlan, setFilterByPlan, members, isMemberError, teamFilterType, avatarSelection, selectionHasConversation, customAvatarsEnabled, }: FormContentProps) { const navigation = useNavigation() const servicesTeams = useServicesTeams() const selectedTeams = useMemo(() => { return servicesTeams.filter(team => selectedTeamIds.includes(team.value.teamId)) }, [selectedTeamIds, servicesTeams]) const styles = useStyles() const teamCountHeader = pluralize(selectedTeamIds.length, 'team') const memberCount = members.length const childMembers = useMemo(() => members.filter(member => member.child), [members]) const hasChildren = childMembers.length > 0 filterByPlan = filterByPlan && !!selectedTeamIds.length return ( To: { Haptic.impactLight() navigation.navigate('New', { screen: 'ConversationFilterRecipients', params: { source_app_name: 'Services', team_ids: selectedTeamIds, team_filter_type: teamFilterType, }, }) }} > {teamCountHeader} {selectedTeams.map(team => { const serviceTypeAcronyms = team.serviceTypeAcronyms.join(', ') const label = `${team.teamName} | ${serviceTypeAcronyms}` return ( removeSelection(team.value.teamId)} /> ) })} Filter by plan { setFilterByPlan(value) if (!value) { setSelectedPlanId(undefined) } }} disabled={!selectedTeamIds.length} /> {filterByPlan && ( )} {!selectionHasConversation && customAvatarsEnabled && ( <> )} {pluralize(memberCount, 'member')} selected {hasChildren && ( )} {isMemberError && ( )} ) } const useStyles = () => { const sectionPadding = 16 return StyleSheet.create({ formContainer: { flex: 1, }, formContent: { paddingBottom: sectionPadding, flex: 1, }, toSection: { padding: sectionPadding, gap: 8, }, toSectionRow: { flexDirection: 'row', gap: 8, alignItems: 'baseline', flexWrap: 'wrap', }, badgeRow: { flexDirection: 'row', }, groupName: { fontSize: 18, }, filterByPlanSection: { padding: sectionPadding, gap: 12, }, filterByPlanSectionLead: { flexDirection: 'row', gap: 8, justifyContent: 'space-between', alignItems: 'center', }, filterByPlanSectionRow: { flexDirection: 'row', gap: 8, }, titleInput: { fontSize: 18, }, memberSection: { padding: sectionPadding, paddingBottom: 0, }, banner: { marginTop: 16, }, teamCountHeader: { fontSize: tokens.fontSizeLg, }, }) }