import { HeaderTitle as ElementsHeaderTitle, HeaderTitleProps, PlatformPressable, } from '@react-navigation/elements' import { StackActions, StaticScreenProps, useNavigation } from '@react-navigation/native' import React, { useCallback, useEffect, useRef, useState, type Dispatch, type ReactNode, type SetStateAction, } from 'react' import { Alert, FlatList, Platform, Pressable, StyleSheet, TextInput, View, type ViewProps, type ViewStyle, } from 'react-native' import { AdultRequirementNotice, Badge, Button, ChildNotice, Heading, Icon, Person, Switch, Text, TextButton, type TextStyle, } from '../components' import { ConversationAvatar } from '../components/display/conversation_avatar' import { useHeaderRight } from '../components/display/platform_modal_header_buttons' import { ButtonAppearanceUnion } from '../components/display/utils/button_colors' import { useSuspensePaginator, useTheme } from '../hooks' import { useConversation, useConversationArchive, useConversationDelete, useConversationDisableReplies, useConversationMute, useConversationUpdate, } from '../hooks/use_conversation' import { availableFeatures, useFeatures } from '../hooks/use_features' import { useManualArchivingEnabled } from '../hooks/use_manual_archiving_enabled' import { usePreviewAvatarDiameter } from '../hooks/use_preview_avatar_diameter' import { useUnarchiveConversationAction } from '../hooks/use_unarchive_conversation_action' import { type ConversationResource, MemberResource, isDefined } from '../types' import { GroupResource } from '../types/resources/group_resource' import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../utils' import { archivedConversationMessage, isArchivedConversation } from '../utils/archived_conversation' import { genderDisplayLabel } from '../utils/gender_display_label' import { tokens } from '../vendor/tapestry/tokens' // ========================================= // ====== Factory Constants & Types ======== // ========================================= enum SectionTypes { header, hidden, members, loadingMembers, navigableSetting, setting, view, } type SectionListData = Array< | DataItem<{ title: string; badge?: ReactNode }, SectionTypes.header> | DataItem | DataItem | DataItem | DataItem | DataItem | DataItem > interface DataItem { type: TName data: T sectionOuterStyle?: ViewStyle sectionInnerStyle?: ViewStyle showBottomBorder?: boolean } // ================================= // ====== Components =============== // ================================= export type ConversationDetailsScreenProps = StaticScreenProps<{ conversation_id: number }> export function ConversationDetailsScreen({ route }: ConversationDetailsScreenProps) { const navigation = useNavigation() const styles = useStyles() const { data: conversation } = useConversation(route.params) const [title, setTitle] = useState(conversation.title ?? '') const { conversation_id } = route.params const { repliesDisabled, setRepliesDisabled } = useConversationDisableReplies(route.params) const { mutate: saveTitle } = useConversationUpdate(route.params) const { mutate: deleteConversation } = useConversationDelete(route.params) const { setArchived, isPending: isArchiving } = useConversationArchive(route.params) const unarchiveAction = useUnarchiveConversationAction(conversation) const { featureEnabled } = useFeatures() const granularNotificationsEnabled = featureEnabled(availableFeatures.granular_notifications_ui) const customAvatarsEnabled = featureEnabled(availableFeatures.custom_conversation_avatars) const manualArchivingEnabled = useManualArchivingEnabled() const showGenderFilter = featureEnabled(availableFeatures.gender_specific_conversations) && !!conversation.genderOption const genderLabel = conversation.genderOption ? genderDisplayLabel(conversation.genderOption) : '' const { muted, setMuted } = useConversationMute({ conversation_id }) const trimmedTitle = title.trim() const emptyTitle = trimmedTitle === '' const notificationLevelDescription = conversation?.conversationMembership?.notificationLevelDescription const archived = isArchivedConversation(conversation) const canUpdate = conversation.memberAbility?.canUpdate || false const canSaveTitle = canUpdate && !emptyTitle const canDelete = conversation.memberAbility?.canDelete || false const canArchive = manualArchivingEnabled && (conversation.memberAbility?.canArchive || false) const isLeader = conversation.memberAbility?.leader || false const isDisabled = conversation.disabled ?? false const showAdultRequirementNotice = isDisabled && conversation.disabledReason === 'safety_check' const { data: members, fetchNextPage: fetchNextPageOfMembers, isFetchingNextPage: isLoadingMembers, } = useSuspensePaginator({ url: `/me/conversations/${route.params.conversation_id}/members`, data: { include: ['person'], fields: { Member: ['avatar', 'name', 'first_name', 'last_name', 'child', 'badges'], }, }, }) const memberSectionStyleMap = { first: styles.sectionInnerFirstMember, middle: styles.sectionInnerMiddleMiddle, last: styles.sectionInnerLastMember, } const memberItems = members.map((data, index) => { const isFirst = index === 0 const isLast = index === members.length - 1 const memberPosition = isFirst ? 'first' : isLast ? 'last' : 'middle' const memberSectionStyle = memberSectionStyleMap[memberPosition] return { type: SectionTypes.members, data, sectionInnerStyle: memberSectionStyle, } }) const loadingMemberItems = isLoadingMembers ? [ { type: SectionTypes.loadingMembers, data: {}, sectionInnerStyle: styles.sectionInnerFirstMember, }, ] : [] const childMembers = members.filter(member => member.child) const hasChildren = childMembers.length > 0 const teamsGroup = conversation.groups ?.filter(g => g.sourceAppName === 'Services') ?.filter(g => ['Team', 'PlanTeam'].includes(g.sourceType)) ?.filter(g => g.name !== null) || [] const hasTeams = teamsGroup.length > 0 const badge = conversation.badges?.[0] const resourceType = badge?.pcoResourceType || '' const productName = badge?.appName const name = badge?.text || undefined const handleDelete = useCallback(() => { Alert.alert( 'Delete conversation', `All messages in ${title} will be deleted for everyone. This action cannot be undone.`, [ { text: 'Cancel', style: 'cancel', }, { text: 'Delete', style: 'destructive', onPress: () => { deleteConversation() navigation.dispatch(StackActions.popToTop()) }, }, ] ) }, [deleteConversation, navigation, title]) const handleArchive = useCallback(() => { Alert.alert( 'Archive conversation', 'This conversation will be archived for everyone. Only its leaders and creator can open and unarchive it.', [ { text: 'Cancel', style: 'cancel', }, { text: 'Archive', onPress: () => setArchived(true), }, ] ) }, [setArchived]) const HeaderTitle = useCallback( (props: HeaderTitleProps) => { return ( ) }, [name, productName, resourceType, styles.headerTitleBadge, styles.headerTitleContainer] ) useHeaderRight({ icon: 'general.check', accessibilityLabel: 'Done', onPress: () => { if (canSaveTitle) saveTitle({ title: trimmedTitle }) navigation.goBack() }, }) useEffect(() => { navigation.setOptions({ headerTitle: HeaderTitle }) }, [HeaderTitle, navigation]) const listData = [ { type: canUpdate && customAvatarsEnabled ? SectionTypes.view : SectionTypes.hidden, data: { children: ( navigation.navigate('AvatarPicker', { conversation_id })} /> ), }, sectionOuterStyle: styles.sectionOuterAvatarCard, sectionInnerStyle: styles.sectionInnerAvatarCard, }, { type: SectionTypes.header, data: { title: 'Basic info' }, showBottomBorder: true, sectionInnerStyle: styles.sectionInnerHeaderWithBottomBorder, }, { type: SectionTypes.view, data: { children: ( ), }, sectionInnerStyle: styles.sectionInnerTitleInput, }, { type: SectionTypes.header, data: { title: 'Settings' }, showBottomBorder: true, sectionInnerStyle: styles.sectionInnerHeaderWithBottomBorder, }, archived ? { type: SectionTypes.view, data: { children: ( <> {archivedConversationMessage(conversation)} {unarchiveAction && (