// Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactChild } from 'react'; import React from 'react'; import { LeftPaneHelper } from './LeftPaneHelper'; import type { Row } from '../ConversationList'; import { RowType } from '../ConversationList'; import { SearchInput } from '../SearchInput'; import type { LocalizerType } from '../../types/Util'; import type { ShowConversationType } from '../../state/ducks/conversations'; import type { ParsedE164Type } from '../../util/libphonenumberInstance'; import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance'; import type { UUIDFetchStateType } from '../../util/uuidFetchState'; import type { CountryDataType } from '../../util/getCountryData'; import { isFetchingByE164 } from '../../util/uuidFetchState'; import { drop } from '../../util/drop'; import type { LookupConversationWithoutServiceIdActionsType } from '../../util/lookupConversationWithoutServiceId'; import { Spinner } from '../Spinner'; import { Button } from '../Button'; import { CountryCodeSelect } from '../CountryCodeSelect'; export type LeftPaneFindByPhoneNumberPropsType = { searchTerm: string; regionCode: string | undefined; uuidFetchState: UUIDFetchStateType; selectedRegion: string; countries: ReadonlyArray<CountryDataType>; }; type DoLookupActionsType = Readonly<{ showInbox: () => void; showConversation: ShowConversationType; }> & LookupConversationWithoutServiceIdActionsType; export class LeftPaneFindByPhoneNumberHelper extends LeftPaneHelper<LeftPaneFindByPhoneNumberPropsType> { private readonly searchTerm: string; private readonly phoneNumber: ParsedE164Type | undefined; private readonly regionCode: string | undefined; private readonly uuidFetchState: UUIDFetchStateType; private readonly countries: ReadonlyArray<CountryDataType>; private readonly selectedRegion: string; constructor({ searchTerm, regionCode, uuidFetchState, countries, selectedRegion, }: Readonly<LeftPaneFindByPhoneNumberPropsType>) { super(); this.searchTerm = searchTerm; this.uuidFetchState = uuidFetchState; this.regionCode = regionCode; this.countries = countries; this.selectedRegion = selectedRegion; this.phoneNumber = parseAndFormatPhoneNumber( this.searchTerm, selectedRegion || regionCode ); } override getHeaderContents({ i18n, startComposing, }: Readonly<{ i18n: LocalizerType; startComposing: () => void; }>): ReactChild { const backButtonLabel = i18n('icu:setGroupMetadata__back-button'); return ( <div className="module-left-pane__header__contents"> <button aria-label={backButtonLabel} className="module-left-pane__header__contents__back-button" disabled={this.isFetching()} onClick={this.getBackAction({ startComposing })} title={backButtonLabel} type="button" /> <div className="module-left-pane__header__contents__text"> {i18n('icu:LeftPaneFindByHelper__title--findByPhoneNumber')} </div> </div> ); } override getBackAction({ startComposing, }: { startComposing: () => void; }): undefined | (() => void) { return this.isFetching() ? undefined : startComposing; } override getSearchInput({ i18n, onChangeComposeSearchTerm, onChangeComposeSelectedRegion, ...lookupActions }: Readonly<{ i18n: LocalizerType; onChangeComposeSearchTerm: ( event: React.ChangeEvent<HTMLInputElement> ) => unknown; onChangeComposeSelectedRegion: (newRegion: string) => void; }> & DoLookupActionsType): ReactChild { const placeholder = i18n( 'icu:LeftPaneFindByHelper__placeholder--findByPhoneNumber' ); return ( <div className="LeftPaneFindByPhoneNumberHelper__container"> <CountryCodeSelect countries={this.countries} i18n={i18n} defaultRegion={this.regionCode ?? ''} value={this.selectedRegion} onChange={onChangeComposeSelectedRegion} /> <SearchInput hasSearchIcon={false} disabled={this.isFetching()} i18n={i18n} moduleClassName="LeftPaneFindByPhoneNumberHelper__search-input" onChange={onChangeComposeSearchTerm} placeholder={placeholder} ref={focusRef} value={this.searchTerm} onKeyDown={ev => { if (ev.key === 'Enter') { drop(this.doLookup(lookupActions)); } }} /> </div> ); } override getFooterContents({ i18n, ...lookupActions }: Readonly<{ i18n: LocalizerType; }> & DoLookupActionsType): ReactChild { return ( <Button disabled={this.isLookupDisabled()} onClick={() => drop(this.doLookup(lookupActions))} > {this.isFetching() ? ( <span aria-label={i18n('icu:loading')} role="status"> <Spinner size="20px" svgSize="small" direction="on-avatar" /> </span> ) : ( i18n('icu:next2') )} </Button> ); } getRowCount(): number { return 1; } getRow(): Row { // This puts a blank row for the footer. return { type: RowType.Blank }; } // This is deliberately unimplemented because these keyboard shortcuts shouldn't work in // the composer. The same is true for the "in direction" function below. getConversationAndMessageAtIndex( ..._args: ReadonlyArray<unknown> ): undefined { return undefined; } getConversationAndMessageInDirection( ..._args: ReadonlyArray<unknown> ): undefined { return undefined; } shouldRecomputeRowHeights(_old: unknown): boolean { return false; } private async doLookup({ lookupConversationWithoutServiceId, showUserNotFoundModal, setIsFetchingUUID, showInbox, showConversation, }: DoLookupActionsType): Promise<void> { if (!this.phoneNumber || this.isLookupDisabled()) { return; } const conversationId = await lookupConversationWithoutServiceId({ showUserNotFoundModal, setIsFetchingUUID, type: 'e164', e164: this.phoneNumber.e164, phoneNumber: this.searchTerm, }); if (conversationId != null) { showConversation({ conversationId }); showInbox(); } } private isFetching(): boolean { if (this.phoneNumber != null) { return isFetchingByE164(this.uuidFetchState, this.phoneNumber.e164); } return false; } private isLookupDisabled(): boolean { if (this.isFetching()) { return true; } return !this.phoneNumber?.isValid; } } function focusRef(el: HTMLElement | null) { if (el) { el.focus(); } }