import { useCallback, useLayoutEffect, useMemo, useReducer, useRef, useState, } from 'react' import {LayoutAnimation, TextInput, View} from 'react-native' import {moderateProfile, type ModerationOpts} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useSession} from '#/state/session' import {type ListMethods} from '#/view/com/util/List' import {android, atoms as a, native, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {canBeMessaged} from '#/components/dms/util' import * as TextField from '#/components/forms/TextField' import * as Toggle from '#/components/forms/Toggle' import {useInteractionState} from '#/components/hooks/useInteractionState' import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon, ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon, } from '#/components/icons/Arrow' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass' import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import {IS_NATIVE, IS_WEB} from '#/env' import type * as bsky from '#/types/bsky' import {ChatProfileTabs} from './ChatProfileTabs' type NewGroupChatItem = { type: 'newGroupChat' key: string } type LabelItem = { type: 'label' key: string message: string } export type ProfileItem = { type: 'profile' key: string profile: bsky.profile.AnyProfileView } type EmptyItem = { type: 'empty' key: string message: string } type PlaceholderItem = { type: 'placeholder' key: string } type ErrorItem = { type: 'error' key: string } type Item = | NewGroupChatItem | LabelItem | ProfileItem | EmptyItem | PlaceholderItem | ErrorItem enum ChatState { NEW_CHAT, NEW_GROUP_CHAT, GROUP_NAME, } export type State = { chatState: ChatState screenTitle: string groupChatDids: string[] groupChatProfiles: bsky.profile.AnyProfileView[] groupName: string } export type Action = | { type: 'startNewGroupChat' screenTitle: string } | { type: 'setDids' groupChatDids: string[] groupChatProfiles: bsky.profile.AnyProfileView[] } | { type: 'removeDids' groupChatDids: string[] groupChatProfiles: bsky.profile.AnyProfileView[] } | { type: 'startNameGroup' screenTitle: string } | { type: 'nameGroup' groupName: string } | { type: 'goBackFromNewGroupChat' screenTitle: string } | { type: 'goBackFromGroupName' screenTitle: string } function reducer(state: State, action: Action): State { switch (action.type) { case 'startNewGroupChat': { return { ...state, chatState: ChatState.NEW_GROUP_CHAT, screenTitle: action.screenTitle, groupChatDids: [], groupChatProfiles: [], groupName: '', } } case 'setDids': { return { ...state, groupChatDids: action.groupChatDids, groupChatProfiles: action.groupChatProfiles, } } case 'removeDids': { return { ...state, groupChatDids: action.groupChatDids, groupChatProfiles: action.groupChatProfiles, } } case 'startNameGroup': { return { ...state, chatState: ChatState.GROUP_NAME, screenTitle: action.screenTitle, } } case 'nameGroup': { return { ...state, groupName: action.groupName, } } case 'goBackFromNewGroupChat': { return { ...state, chatState: ChatState.NEW_CHAT, screenTitle: action.screenTitle, groupChatDids: [], groupChatProfiles: [], groupName: '', } } case 'goBackFromGroupName': { return { ...state, chatState: ChatState.NEW_GROUP_CHAT, screenTitle: action.screenTitle, groupName: '', } } } } export function InitiateChatFlow({ title, onSelectChat, onSelectGroupChat, }: { title: string onSelectChat: (did: string) => void onSelectGroupChat: (dids: string[], groupName: string) => void }) { const t = useTheme() const {t: l} = useLingui() const moderationOpts = useModerationOpts() const control = Dialog.useDialogContext() const [headerHeight, setHeaderHeight] = useState(0) const [footerHeight, setFooterHeight] = useState(0) const listRef = useRef(null) const {currentAccount} = useSession() const inputRef = useRef(null) const [searchText, setSearchText] = useState('') const { data: results, isError, isFetching, } = useActorAutocompleteQuery(searchText, true, 12) const {data: follows} = useProfileFollowsQuery(currentAccount?.did) const [ {chatState, screenTitle, groupChatDids, groupChatProfiles, groupName}, dispatch, ] = useReducer(reducer, { chatState: ChatState.NEW_CHAT, screenTitle: title, groupChatDids: [], groupChatProfiles: [], groupName: '', }) const newGroupChatTitle = l`New group chat` const groupNameTitle = l`Group name` const onRemoveDid = useCallback( (did: string) => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) dispatch({ type: 'removeDids', groupChatDids: groupChatDids.filter(d => d !== did), groupChatProfiles: groupChatProfiles.filter( profile => profile.did !== did, ), }) }, [groupChatDids, groupChatProfiles], ) const items = useMemo(() => { let _items: Item[] = [] if (isError) { _items.push({ type: 'empty', key: 'empty', message: l`We’re having network issues, try again`, }) } else if (chatState === ChatState.GROUP_NAME) { _items = groupChatProfiles.map(profile => ({ type: 'profile', key: profile.did, profile, })) _items.unshift({ type: 'label', key: 'members', message: l`New group chat with:`, }) } else if (searchText.length) { if (results?.length) { for (const profile of results) { if (profile.did === currentAccount?.did) continue _items.push({ type: 'profile', key: profile.did, profile, }) } _items = _items.sort(item => { return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1 }) } } else { const placeholders: Item[] = Array(10) .fill(0) .map((__, i) => ({ type: 'placeholder', key: i + '', })) if (follows) { for (const page of follows.pages) { for (const profile of page.follows) { _items.push({ type: 'profile', key: profile.did, profile, }) } } _items = _items.sort(item => { return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1 }) } else { _items.push(...placeholders) } } if ( searchText === '' && (chatState === ChatState.NEW_CHAT || chatState === ChatState.NEW_GROUP_CHAT) ) { _items.unshift({ type: 'label', key: 'suggested', message: l`Suggested`, }) } if (chatState === ChatState.NEW_CHAT && searchText === '') { _items.unshift({type: 'newGroupChat', key: 'newGroupChat'}) } return _items }, [ isError, chatState, searchText, l, groupChatProfiles, results, currentAccount?.did, follows, ]) if (searchText && !isFetching && !items.length && !isError) { items.push({type: 'empty', key: 'empty', message: l`No results`}) } const handlePressBack = useCallback(() => { switch (chatState) { case ChatState.NEW_CHAT: control.close() break case ChatState.NEW_GROUP_CHAT: LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) dispatch({type: 'goBackFromNewGroupChat', screenTitle: title}) setSearchText('') break case ChatState.GROUP_NAME: LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) dispatch({type: 'goBackFromGroupName', screenTitle: newGroupChatTitle}) break } }, [chatState, control, newGroupChatTitle, title]) const handlePressNewGroupChat = useCallback(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) dispatch({type: 'startNewGroupChat', screenTitle: newGroupChatTitle}) }, [newGroupChatTitle]) const handlePressNext = useCallback(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) dispatch({type: 'startNameGroup', screenTitle: groupNameTitle}) setSearchText('') }, [groupNameTitle]) const handlePressConfirm = useCallback(() => { onSelectGroupChat(groupChatDids, groupName) }, [groupChatDids, groupName, onSelectGroupChat]) const setGroupName = (newGroupName: string) => { dispatch({type: 'nameGroup', groupName: newGroupName}) } const renderItems = useCallback( ({item}: {item: Item}) => { switch (item.type) { case 'newGroupChat': { return ( ) } case 'label': { return