Get countries and phone codes from server (#1422)

This commit is contained in:
Alexander Zinchuk 2021-09-03 18:31:24 +03:00
parent 4a6dc47be0
commit 00daafc654
22 changed files with 306 additions and 118 deletions

View File

@ -22,14 +22,6 @@ type AnyFunction = (...args: any) => any;
type AnyToVoidFunction = (...args: any) => void; type AnyToVoidFunction = (...args: any) => void;
type NoneToVoidFunction = () => void; type NoneToVoidFunction = () => void;
type Country = {
id: string;
name: string;
flag: string;
code: string;
phoneFormat: RegExp;
};
type EmojiCategory = { type EmojiCategory = {
id: string; id: string;
name: string; name: string;

View File

@ -1,11 +1,11 @@
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import { ApiSession, ApiWallpaper } from '../../types'; import { ApiCountry, ApiSession, ApiWallpaper } from '../../types';
import { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types'; import { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types';
import { buildApiDocument } from './messages'; import { buildApiDocument } from './messages';
import { getApiChatIdFromMtpPeer } from './chats'; import { getApiChatIdFromMtpPeer } from './chats';
import { pick } from '../../../util/iteratees'; import { flatten, pick } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
export function buildApiWallpaper(wallpaper: GramJs.TypeWallPaper): ApiWallpaper | undefined { export function buildApiWallpaper(wallpaper: GramJs.TypeWallPaper): ApiWallpaper | undefined {
@ -113,3 +113,41 @@ export function buildApiNotifyException(
...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }), ...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }),
}; };
} }
function buildApiCountry(country: GramJs.help.Country, code?: GramJs.help.CountryCode) {
const {
hidden, iso2, defaultName, name,
} = country;
const { countryCode, prefixes, patterns } = code || {};
return {
isHidden: hidden,
iso2,
defaultName,
name,
countryCode,
prefixes,
patterns,
};
}
export function buildApiCountryList(countries: GramJs.help.Country[]) {
const listByCode = flatten(countries
.filter((country) => !country.hidden)
.map((country) => (
country.countryCodes.map((code) => buildApiCountry(country, code))
))).sort((a: ApiCountry, b: ApiCountry) => (
a.name ? a.name.localeCompare(b.name!) : a.defaultName.localeCompare(b.defaultName)
));
const generalList = countries
.filter((country) => !country.hidden)
.map((country) => buildApiCountry(country))
.sort((a, b) => (
a.name ? a.name.localeCompare(b.name!) : a.defaultName.localeCompare(b.defaultName)
));
return {
phoneCodes: listByCode,
general: generalList,
};
}

View File

@ -66,6 +66,11 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[GramJs/client] CONNECTING'); console.log('[GramJs/client] CONNECTING');
// eslint-disable-next-line no-restricted-globals
(self as any).invoke = invokeRequest;
// eslint-disable-next-line no-restricted-globals
(self as any).GramJs = GramJs;
} }
try { try {
@ -95,11 +100,6 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
console.log('>>> FINISH INIT API'); console.log('>>> FINISH INIT API');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('[GramJs/client] CONNECTED'); console.log('[GramJs/client] CONNECTED');
// eslint-disable-next-line no-restricted-globals
(self as any).invoke = invokeRequest;
// eslint-disable-next-line no-restricted-globals
(self as any).GramJs = GramJs;
} }
onAuthReady(); onAuthReady();

View File

@ -26,7 +26,7 @@ export {
} from './messages'; } from './messages';
export { export {
fetchFullUser, fetchNearestCountry, fetchFullUser, fetchNearestCountry, fetchCountryList,
fetchTopUsers, fetchContactList, fetchUsers, fetchTopUsers, fetchContactList, fetchUsers,
updateContact, deleteUser, fetchProfilePhotos, updateContact, deleteUser, fetchProfilePhotos,
} from './users'; } from './users';

View File

@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import { import {
OnApiUpdate, ApiUser, ApiChat, ApiPhoto, OnApiUpdate, ApiUser, ApiChat, ApiPhoto,
} from '../../types'; } from '../../types';
import { LangCode } from '../../../types';
import { PROFILE_PHOTOS_LIMIT } from '../../../config'; import { PROFILE_PHOTOS_LIMIT } from '../../../config';
import { invokeRequest } from './client'; import { invokeRequest } from './client';
@ -18,6 +19,7 @@ import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiPhoto } from '../apiBuilders/common';
import localDb from '../localDb'; import localDb from '../localDb';
import { addPhotoToLocalDb } from '../helpers'; import { addPhotoToLocalDb } from '../helpers';
import { buildApiCountryList } from '../apiBuilders/misc';
let onUpdate: OnApiUpdate; let onUpdate: OnApiUpdate;
@ -60,6 +62,17 @@ export async function fetchNearestCountry() {
return dcInfo?.country; return dcInfo?.country;
} }
export async function fetchCountryList({ langCode = 'en' }: { langCode?: LangCode }) {
const countryList = await invokeRequest(new GramJs.help.GetCountriesList({
langCode,
}));
if (!(countryList instanceof GramJs.help.CountriesList)) {
return undefined;
}
return buildApiCountryList(countryList.countries);
}
export async function fetchTopUsers({ hash = 0 }: { hash?: number }) { export async function fetchTopUsers({ hash = 0 }: { hash?: number }) {
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({ const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
hash, hash,

View File

@ -91,3 +91,16 @@ export type ApiInviteInfo = {
isChannel?: boolean; isChannel?: boolean;
participantsCount?: number; participantsCount?: number;
}; };
export interface ApiCountry {
isHidden?: boolean;
iso2: string;
defaultName: string;
name?: string;
}
export interface ApiCountryCode extends ApiCountry {
countryCode: string;
prefixes?: string[];
patterns?: string[];
}

View File

@ -10,12 +10,13 @@ import { withGlobal } from '../../lib/teact/teactn';
import { GlobalActions, GlobalState } from '../../global/types'; import { GlobalActions, GlobalState } from '../../global/types';
import { LangCode } from '../../types'; import { LangCode } from '../../types';
import { ApiCountryCode } from '../../api/types';
import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/environment'; import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/environment';
import { preloadImage } from '../../util/files'; import { preloadImage } from '../../util/files';
import preloadFonts from '../../util/fonts'; import preloadFonts from '../../util/fonts';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import { formatPhoneNumber, getCountryById, getCountryFromPhoneNumber } from '../../util/phoneNumber'; import { formatPhoneNumber, getCountryCodesByIso, getCountryFromPhoneNumber } from '../../util/phoneNumber';
import { setLanguage } from '../../util/langProvider'; import { setLanguage } from '../../util/langProvider';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
@ -30,13 +31,16 @@ import CountryCodeInput from './CountryCodeInput';
type StateProps = Pick<GlobalState, ( type StateProps = Pick<GlobalState, (
'connectionState' | 'authState' | 'connectionState' | 'authState' |
'authPhoneNumber' | 'authIsLoading' | 'authIsLoadingQrCode' | 'authError' | 'authRememberMe' | 'authNearestCountry' 'authPhoneNumber' | 'authIsLoading' |
'authIsLoadingQrCode' | 'authError' |
'authRememberMe' | 'authNearestCountry'
)> & { )> & {
language?: LangCode; language?: LangCode;
phoneCodeList: ApiCountryCode[];
}; };
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
'setAuthPhoneNumber' | 'setAuthRememberMe' | 'loadNearestCountry' | 'clearAuthError' | 'goToAuthQrCode' | 'setAuthPhoneNumber' | 'setAuthRememberMe' | 'loadNearestCountry' | 'loadCountryList' | 'clearAuthError' |
'setSettingOption' 'goToAuthQrCode' | 'setSettingOption'
)>; )>;
const MIN_NUMBER_LENGTH = 7; const MIN_NUMBER_LENGTH = 7;
@ -52,10 +56,12 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
authError, authError,
authRememberMe, authRememberMe,
authNearestCountry, authNearestCountry,
phoneCodeList,
language, language,
setAuthPhoneNumber, setAuthPhoneNumber,
setAuthRememberMe, setAuthRememberMe,
loadNearestCountry, loadNearestCountry,
loadCountryList,
clearAuthError, clearAuthError,
goToAuthQrCode, goToAuthQrCode,
setSettingOption, setSettingOption,
@ -66,13 +72,13 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
const suggestedLanguage = getSuggestedLanguage(); const suggestedLanguage = getSuggestedLanguage();
const continueText = useLangString(suggestedLanguage, 'ContinueOnThisLanguage'); const continueText = useLangString(suggestedLanguage, 'ContinueOnThisLanguage');
const [country, setCountry] = useState<Country | undefined>(); const [country, setCountry] = useState<ApiCountryCode | undefined>();
const [phoneNumber, setPhoneNumber] = useState<string | undefined>(); const [phoneNumber, setPhoneNumber] = useState<string | undefined>();
const [isTouched, setIsTouched] = useState(false); const [isTouched, setIsTouched] = useState(false);
const [lastSelection, setLastSelection] = useState<[number, number] | undefined>(); const [lastSelection, setLastSelection] = useState<[number, number] | undefined>();
const [isLoading, markIsLoading, unmarkIsLoading] = useFlag(); const [isLoading, markIsLoading, unmarkIsLoading] = useFlag();
const fullNumber = country ? `${country.code} ${phoneNumber || ''}` : phoneNumber; const fullNumber = country ? `+${country.countryCode} ${phoneNumber || ''}` : phoneNumber;
const canSubmit = fullNumber && fullNumber.replace(/[^\d]+/g, '').length >= MIN_NUMBER_LENGTH; const canSubmit = fullNumber && fullNumber.replace(/[^\d]+/g, '').length >= MIN_NUMBER_LENGTH;
useEffect(() => { useEffect(() => {
@ -88,31 +94,36 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
}, [connectionState, authNearestCountry, loadNearestCountry]); }, [connectionState, authNearestCountry, loadNearestCountry]);
useEffect(() => { useEffect(() => {
if (authNearestCountry && !country && !isTouched) { if (connectionState === 'connectionStateReady') {
setCountry(getCountryById(authNearestCountry)); loadCountryList({ langCode: language });
} }
}, [country, authNearestCountry, isTouched]); }, [connectionState, language, loadCountryList]);
useEffect(() => {
if (authNearestCountry && phoneCodeList && !country && !isTouched) {
setCountry(getCountryCodesByIso(phoneCodeList, authNearestCountry)[0]);
}
}, [country, authNearestCountry, isTouched, phoneCodeList]);
const parseFullNumber = useCallback((newFullNumber: string) => { const parseFullNumber = useCallback((newFullNumber: string) => {
if (!newFullNumber.length) { if (!newFullNumber.length) {
setPhoneNumber(''); setPhoneNumber('');
} }
const suggestedCountry = getCountryFromPhoneNumber(newFullNumber); const suggestedCountry = phoneCodeList && getCountryFromPhoneNumber(phoneCodeList, newFullNumber);
// Any phone numbers should be allowed, in some cases ignoring formatting // Any phone numbers should be allowed, in some cases ignoring formatting
const selectedCountry = !country const selectedCountry = !country
|| (suggestedCountry && suggestedCountry.id !== country.id) || (suggestedCountry && suggestedCountry.iso2 !== country.iso2)
|| (!suggestedCountry && newFullNumber.length) || (!suggestedCountry && newFullNumber.length)
? suggestedCountry ? suggestedCountry
: country; : country;
if (!country || !selectedCountry || (selectedCountry && selectedCountry.code !== country.code)) { if (!country || !selectedCountry || (selectedCountry && selectedCountry.iso2 !== country.iso2)) {
setCountry(selectedCountry); setCountry(selectedCountry);
} }
setPhoneNumber(formatPhoneNumber(newFullNumber, selectedCountry)); setPhoneNumber(formatPhoneNumber(newFullNumber, selectedCountry));
}, [country]); }, [phoneCodeList, country]);
const handleLangChange = useCallback(() => { const handleLangChange = useCallback(() => {
markIsLoading(); markIsLoading();
@ -144,6 +155,11 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
}); });
}, []); }, []);
const handleCountryChange = useCallback((value: ApiCountryCode) => {
setCountry(value);
setPhoneNumber('');
}, []);
const handlePhoneNumberChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handlePhoneNumberChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
if (authError) { if (authError) {
clearAuthError(); clearAuthError();
@ -169,7 +185,7 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
IS_SAFARI && country && fullNumber !== undefined IS_SAFARI && country && fullNumber !== undefined
&& value.length - fullNumber.length > 1 && !isJustPastedRef.current && value.length - fullNumber.length > 1 && !isJustPastedRef.current
); );
parseFullNumber(shouldFixSafariAutoComplete ? `${country!.code} ${value}` : value); parseFullNumber(shouldFixSafariAutoComplete ? `${country!.countryCode} ${value}` : value);
}, [authError, clearAuthError, country, fullNumber, parseFullNumber]); }, [authError, clearAuthError, country, fullNumber, parseFullNumber]);
const handleKeepSessionChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleKeepSessionChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
@ -201,7 +217,7 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
id="sign-in-phone-code" id="sign-in-phone-code"
value={country} value={country}
isLoading={!authNearestCountry && !country} isLoading={!authNearestCountry && !country}
onChange={setCountry} onChange={handleCountryChange}
/> />
<InputText <InputText
ref={inputRef} ref={inputRef}
@ -244,6 +260,7 @@ export default memo(withGlobal(
(global): StateProps => { (global): StateProps => {
const { const {
settings: { byKey: { language } }, settings: { byKey: { language } },
countryList: { phoneCodes: phoneCodeList },
} = global; } = global;
return { return {
@ -258,6 +275,7 @@ export default memo(withGlobal(
'authNearestCountry', 'authNearestCountry',
]), ]),
language, language,
phoneCodeList,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [
@ -265,6 +283,7 @@ export default memo(withGlobal(
'setAuthRememberMe', 'setAuthRememberMe',
'clearAuthError', 'clearAuthError',
'loadNearestCountry', 'loadNearestCountry',
'loadCountryList',
'goToAuthQrCode', 'goToAuthQrCode',
'setSettingOption', 'setSettingOption',
]), ]),

View File

@ -1,13 +1,17 @@
import React, { import React, {
FC, useState, memo, useCallback, useRef, FC, useState, memo, useCallback, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
import { ApiCountryCode } from '../../api/types';
import { ANIMATION_END_DELAY } from '../../config'; import { ANIMATION_END_DELAY } from '../../config';
import { countryList } from '../../util/phoneNumber';
import searchWords from '../../util/searchWords'; import searchWords from '../../util/searchWords';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { isoToEmoji } from '../../util/emoji';
import useOnChange from '../../hooks/useOnChange';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
@ -15,48 +19,59 @@ import Spinner from '../ui/Spinner';
import './CountryCodeInput.scss'; import './CountryCodeInput.scss';
type StateProps = {
phoneCodeList: ApiCountryCode[];
};
type OwnProps = { type OwnProps = {
id: string; id: string;
value?: Country; value?: ApiCountryCode;
isLoading?: boolean; isLoading?: boolean;
onChange: (value: Country) => void; onChange: (value: ApiCountryCode) => void;
}; };
const MENU_HIDING_DURATION = 200 + ANIMATION_END_DELAY; const MENU_HIDING_DURATION = 200 + ANIMATION_END_DELAY;
const SELECT_TIMEOUT = 50; const SELECT_TIMEOUT = 50;
const CountryCodeInput: FC<OwnProps> = ({ const CountryCodeInput: FC<OwnProps & StateProps> = ({
id, id,
value, value,
isLoading, isLoading,
onChange, onChange,
phoneCodeList,
}) => { }) => {
const lang = useLang(); const lang = useLang();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [filter, setFilter] = useState<string | undefined>(); const [filter, setFilter] = useState<string | undefined>();
const [filteredList, setFilteredList] = useState(countryList); const [filteredList, setFilteredList] = useState<ApiCountryCode[]>([]);
function updateFilter(filterValue?: string) { const updateFilter = useCallback((filterValue?: string) => {
setFilter(filterValue); setFilter(filterValue);
setFilteredList(getFilteredList(filterValue)); setFilteredList(getFilteredList(phoneCodeList, filterValue));
} }, [phoneCodeList]);
useOnChange(([prevPhoneCodeList]) => {
if (prevPhoneCodeList?.length === 0 && phoneCodeList.length > 0) {
updateFilter(filter);
}
}, [phoneCodeList, updateFilter]);
const handleChange = useCallback((e: React.SyntheticEvent<HTMLElement>) => { const handleChange = useCallback((e: React.SyntheticEvent<HTMLElement>) => {
const { countryId } = (e.currentTarget.firstElementChild as HTMLDivElement).dataset; const { countryCode } = (e.currentTarget.firstElementChild as HTMLDivElement).dataset;
const country = countryList.find((c) => c.id === countryId); const country = phoneCodeList.find((c) => c.countryCode === countryCode);
if (country) { if (country) {
onChange(country); onChange(country);
} }
setTimeout(() => updateFilter(undefined), MENU_HIDING_DURATION); setTimeout(() => updateFilter(undefined), MENU_HIDING_DURATION);
}, [onChange]); }, [phoneCodeList, onChange, updateFilter]);
const handleInput = useCallback((e: React.FormEvent<HTMLInputElement>) => { const handleInput = useCallback((e: React.FormEvent<HTMLInputElement>) => {
updateFilter(e.currentTarget.value); updateFilter(e.currentTarget.value);
}, []); }, [updateFilter]);
const handleInputKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => { const handleInputKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.keyCode !== 8) { if (e.keyCode !== 8) {
@ -69,7 +84,7 @@ const CountryCodeInput: FC<OwnProps> = ({
} }
updateFilter(target.value); updateFilter(target.value);
}, [filter, value]); }, [filter, updateFilter, value]);
const CodeInput: FC<{ onTrigger: () => void; isOpen?: boolean }> = ({ onTrigger, isOpen }) => { const CodeInput: FC<{ onTrigger: () => void; isOpen?: boolean }> = ({ onTrigger, isOpen }) => {
const handleTrigger = () => { const handleTrigger = () => {
@ -87,9 +102,12 @@ const CountryCodeInput: FC<OwnProps> = ({
formEl.scrollTo({ top: formEl.scrollHeight, behavior: 'smooth' }); formEl.scrollTo({ top: formEl.scrollHeight, behavior: 'smooth' });
}; };
const inputValue = filter !== undefined const handleCodeInput = (e: React.FormEvent<HTMLInputElement>) => {
? filter handleInput(e);
: (value?.name) || ''; handleTrigger();
};
const inputValue = filter ?? (value?.name || value?.defaultName || '');
return ( return (
<div className={buildClassName('input-group', value && 'touched')}> <div className={buildClassName('input-group', value && 'touched')}>
@ -102,7 +120,7 @@ const CountryCodeInput: FC<OwnProps> = ({
autoComplete="off" autoComplete="off"
onClick={handleTrigger} onClick={handleTrigger}
onFocus={handleTrigger} onFocus={handleTrigger}
onInput={handleInput} onInput={handleCodeInput}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
/> />
<label>{lang('Login.SelectCountry.Title')}</label> <label>{lang('Login.SelectCountry.Title')}</label>
@ -120,18 +138,19 @@ const CountryCodeInput: FC<OwnProps> = ({
className="CountryCodeInput" className="CountryCodeInput"
trigger={CodeInput} trigger={CodeInput}
> >
{filteredList.map((country: Country) => ( {filteredList
<MenuItem .map((country: ApiCountryCode) => (
key={country.id} <MenuItem
className={value && country.id === value.id ? 'selected' : ''} key={country.countryCode}
onClick={handleChange} className={value && country.iso2 === value.iso2 ? 'selected' : ''}
> onClick={handleChange}
<span data-country-id={country.id} /> >
<span className="country-flag">{renderText(country.flag, ['hq_emoji'])}</span> <span data-country-code={country.countryCode} />
<span className="country-name">{country.name}</span> <span className="country-flag">{renderText(isoToEmoji(country.iso2), ['hq_emoji'])}</span>
<span className="country-code">{country.code}</span> <span className="country-name">{country.name || country.defaultName}</span>
</MenuItem> <span className="country-code">{country.countryCode}</span>
))} </MenuItem>
))}
{!filteredList.length && ( {!filteredList.length && (
<MenuItem <MenuItem
key="no-results" key="no-results"
@ -145,10 +164,19 @@ const CountryCodeInput: FC<OwnProps> = ({
); );
}; };
function getFilteredList(filter = ''): Country[] { function getFilteredList(countryList: ApiCountryCode[], filter = ''): ApiCountryCode[] {
return filter.length const filtered = filter.length
? countryList.filter((country) => searchWords(country.name, filter)) ? countryList.filter((country) => (
: countryList; searchWords(country.defaultName, filter) || (country.name && searchWords(country.name, filter))
)) : countryList;
return filtered;
} }
export default memo(CountryCodeInput); export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { countryList: { phoneCodes: phoneCodeList } } = global;
return {
phoneCodeList,
};
},
)(CountryCodeInput));

View File

@ -4,7 +4,7 @@ import React, {
import { withGlobal } from '../../lib/teact/teactn'; import { withGlobal } from '../../lib/teact/teactn';
import { GlobalActions, GlobalState } from '../../global/types'; import { GlobalActions, GlobalState } from '../../global/types';
import { ApiChat, ApiUser } from '../../api/types'; import { ApiChat, ApiCountryCode, ApiUser } from '../../api/types';
import { import {
selectChat, selectNotifyExceptions, selectNotifySettings, selectUser, selectChat, selectNotifyExceptions, selectNotifySettings, selectUser,
@ -31,6 +31,7 @@ type StateProps = {
chat?: ApiChat; chat?: ApiChat;
canInviteUsers?: boolean; canInviteUsers?: boolean;
isMuted?: boolean; isMuted?: boolean;
phoneCodeList: ApiCountryCode[];
} & Pick<GlobalState, 'lastSyncTime'>; } & Pick<GlobalState, 'lastSyncTime'>;
type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'updateChatMutedState' | 'showNotification'>; type DispatchProps = Pick<GlobalActions, 'loadFullUser' | 'updateChatMutedState' | 'showNotification'>;
@ -42,6 +43,7 @@ const ChatExtra: FC<OwnProps & StateProps & DispatchProps> = ({
forceShowSelf, forceShowSelf,
canInviteUsers, canInviteUsers,
isMuted, isMuted,
phoneCodeList,
loadFullUser, loadFullUser,
showNotification, showNotification,
updateChatMutedState, updateChatMutedState,
@ -75,7 +77,7 @@ const ChatExtra: FC<OwnProps & StateProps & DispatchProps> = ({
showNotification({ message: `${entity} was copied` }); showNotification({ message: `${entity} was copied` });
} }
const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneNumber); const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneCodeList, phoneNumber);
const link = getChatLink(chat); const link = getChatLink(chat);
const description = (fullInfo?.bio) || getChatDescription(chat); const description = (fullInfo?.bio) || getChatDescription(chat);
@ -135,7 +137,7 @@ const ChatExtra: FC<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatOrUserId }): StateProps => { (global, { chatOrUserId }): StateProps => {
const { lastSyncTime } = global; const { lastSyncTime, countryList: { phoneCodes: phoneCodeList } } = global;
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
const user = isChatPrivate(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined; const user = isChatPrivate(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined;
@ -147,7 +149,7 @@ export default memo(withGlobal<OwnProps>(
); );
return { return {
lastSyncTime, chat, user, canInviteUsers, isMuted, lastSyncTime, phoneCodeList, chat, user, canInviteUsers, isMuted,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -4,7 +4,7 @@ import React, {
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types'; import { GlobalActions } from '../../../global/types';
import { ApiChat, ApiUser } from '../../../api/types'; import { ApiChat, ApiCountryCode, ApiUser } from '../../../api/types';
import { SettingsScreens } from '../../../types'; import { SettingsScreens } from '../../../types';
import { CHAT_HEIGHT_PX } from '../../../config'; import { CHAT_HEIGHT_PX } from '../../../config';
@ -33,6 +33,7 @@ type StateProps = {
chatsByIds: Record<number, ApiChat>; chatsByIds: Record<number, ApiChat>;
usersByIds: Record<number, ApiUser>; usersByIds: Record<number, ApiUser>;
blockedIds: number[]; blockedIds: number[];
phoneCodeList: ApiCountryCode[];
}; };
type DispatchProps = Pick<GlobalActions, 'unblockContact'>; type DispatchProps = Pick<GlobalActions, 'unblockContact'>;
@ -44,6 +45,7 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps & DispatchProps> = (
chatsByIds, chatsByIds,
usersByIds, usersByIds,
blockedIds, blockedIds,
phoneCodeList,
unblockContact, unblockContact,
}) => { }) => {
const handleUnblockClick = useCallback((contactId: number) => { const handleUnblockClick = useCallback((contactId: number) => {
@ -83,7 +85,7 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps & DispatchProps> = (
<div className="contact-info" dir="auto"> <div className="contact-info" dir="auto">
<h3 dir="auto">{renderText((isPrivate ? getUserFullName(user) : getChatTitle(lang, chat!)) || '')}</h3> <h3 dir="auto">{renderText((isPrivate ? getUserFullName(user) : getChatTitle(lang, chat!)) || '')}</h3>
{user?.phoneNumber && ( {user?.phoneNumber && (
<div className="contact-phone" dir="auto">{formatPhoneNumberWithCode(user.phoneNumber)}</div> <div className="contact-phone" dir="auto">{formatPhoneNumberWithCode(phoneCodeList, user.phoneNumber)}</div>
)} )}
{user && !user.phoneNumber && user.username && ( {user && !user.phoneNumber && user.username && (
<div className="contact-username" dir="auto">@{user.username}</div> <div className="contact-username" dir="auto">@{user.username}</div>
@ -142,12 +144,16 @@ export default memo(withGlobal<OwnProps>(
blocked: { blocked: {
ids, ids,
}, },
countryList: {
phoneCodes: phoneCodeList,
},
} = global; } = global;
return { return {
chatsByIds, chatsByIds,
usersByIds, usersByIds,
blockedIds: ids, blockedIds: ids,
phoneCodeList,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['unblockContact']), (setGlobal, actions): DispatchProps => pick(actions, ['unblockContact']),

View File

@ -64,7 +64,7 @@ type StateProps = {
type DispatchProps = Pick<GlobalActions, ( type DispatchProps = Pick<GlobalActions, (
'loadAnimatedEmojis' | 'loadNotificationSettings' | 'loadNotificationExceptions' | 'updateIsOnline' | 'loadAnimatedEmojis' | 'loadNotificationSettings' | 'loadNotificationExceptions' | 'updateIsOnline' |
'loadTopInlineBots' | 'loadEmojiKeywords' | 'openStickerSetShortName' 'loadTopInlineBots' | 'loadEmojiKeywords' | 'openStickerSetShortName' | 'loadCountryList'
)>; )>;
const NOTIFICATION_INTERVAL = 1000; const NOTIFICATION_INTERVAL = 1000;
@ -95,6 +95,7 @@ const Main: FC<StateProps & DispatchProps> = ({
updateIsOnline, updateIsOnline,
loadTopInlineBots, loadTopInlineBots,
loadEmojiKeywords, loadEmojiKeywords,
loadCountryList,
openStickerSetShortName, openStickerSetShortName,
}) => { }) => {
if (DEBUG && !DEBUG_isLogged) { if (DEBUG && !DEBUG_isLogged) {
@ -116,10 +117,12 @@ const Main: FC<StateProps & DispatchProps> = ({
if (language !== BASE_EMOJI_KEYWORD_LANG) { if (language !== BASE_EMOJI_KEYWORD_LANG) {
loadEmojiKeywords({ language }); loadEmojiKeywords({ language });
} }
loadCountryList({ langCode: language });
} }
}, [ }, [
lastSyncTime, loadAnimatedEmojis, loadNotificationExceptions, loadNotificationSettings, updateIsOnline, lastSyncTime, loadAnimatedEmojis, loadNotificationExceptions, loadNotificationSettings, updateIsOnline,
loadTopInlineBots, loadEmojiKeywords, language, loadTopInlineBots, loadEmojiKeywords, loadCountryList, language,
]); ]);
useEffect(() => { useEffect(() => {
@ -298,6 +301,6 @@ export default memo(withGlobal(
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [
'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline', 'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline',
'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName', 'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName', 'loadCountryList',
]), ]),
)(Main)); )(Main));

View File

@ -2,7 +2,7 @@ import React, { FC, useCallback } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
import { GlobalActions } from '../../../global/types'; import { GlobalActions } from '../../../global/types';
import { ApiUser, ApiContact } from '../../../api/types'; import { ApiUser, ApiContact, ApiCountryCode } from '../../../api/types';
import { selectUser } from '../../../modules/selectors'; import { selectUser } from '../../../modules/selectors';
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
@ -19,12 +19,13 @@ type OwnProps = {
type StateProps = { type StateProps = {
user?: ApiUser; user?: ApiUser;
phoneCodeList: ApiCountryCode[];
}; };
type DispatchProps = Pick<GlobalActions, 'openUserInfo'>; type DispatchProps = Pick<GlobalActions, 'openUserInfo'>;
const Contact: FC<OwnProps & StateProps & DispatchProps> = ({ const Contact: FC<OwnProps & StateProps & DispatchProps> = ({
contact, user, openUserInfo, contact, user, openUserInfo, phoneCodeList,
}) => { }) => {
const { const {
firstName, firstName,
@ -45,7 +46,7 @@ const Contact: FC<OwnProps & StateProps & DispatchProps> = ({
<Avatar size="large" user={user} text={firstName || lastName} /> <Avatar size="large" user={user} text={firstName || lastName} />
<div className="contact-info"> <div className="contact-info">
<div className="contact-name">{firstName} {lastName}</div> <div className="contact-name">{firstName} {lastName}</div>
<div className="contact-phone">{formatPhoneNumberWithCode(phoneNumber)}</div> <div className="contact-phone">{formatPhoneNumberWithCode(phoneCodeList, phoneNumber)}</div>
</div> </div>
</div> </div>
); );
@ -53,8 +54,10 @@ const Contact: FC<OwnProps & StateProps & DispatchProps> = ({
export default withGlobal<OwnProps>( export default withGlobal<OwnProps>(
(global, { contact }): StateProps => { (global, { contact }): StateProps => {
const { countryList: { phoneCodes: phoneCodeList } } = global;
return { return {
user: selectUser(global, contact.userId), user: selectUser(global, contact.userId),
phoneCodeList,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -4,7 +4,7 @@ import React, {
import { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer'; import { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { countryList } from '../../util/phoneNumber'; import countryList from '../../util/countries';
import InputText from '../ui/InputText'; import InputText from '../ui/InputText';
import Checkbox from '../ui/Checkbox'; import Checkbox from '../ui/Checkbox';

View File

@ -5,7 +5,7 @@ import React, {
import { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer'; import { FormState, FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation'; import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import { countryList } from '../../util/phoneNumber'; import countryList from '../../util/countries';
import InputText from '../ui/InputText'; import InputText from '../ui/InputText';
import Select from '../ui/Select'; import Select from '../ui/Select';

View File

@ -15,6 +15,10 @@ export const INITIAL_STATE: GlobalState = {
serverTimeOffset: 0, serverTimeOffset: 0,
authRememberMe: true, authRememberMe: true,
countryList: {
phoneCodes: [],
general: [],
},
blocked: { blocked: {
ids: [], ids: [],

View File

@ -19,6 +19,8 @@ import {
ApiSession, ApiSession,
ApiNewPoll, ApiNewPoll,
ApiInviteInfo, ApiInviteInfo,
ApiCountryCode,
ApiCountry,
} from '../api/types'; } from '../api/types';
import { import {
FocusDirection, FocusDirection,
@ -96,6 +98,10 @@ export type GlobalState = {
token: string; token: string;
expires: number; expires: number;
}; };
countryList: {
phoneCodes: ApiCountryCode[];
general: ApiCountry[];
};
contactList?: { contactList?: {
hash: number; hash: number;
@ -428,7 +434,6 @@ export type GlobalState = {
historyCalendarSelectedAt?: number; historyCalendarSelectedAt?: number;
openedStickerSetShortName?: string; openedStickerSetShortName?: string;
// TODO To be removed in August 2021
shouldShowContextMenuHint?: boolean; shouldShowContextMenuHint?: boolean;
}; };
@ -479,8 +484,9 @@ export type ActionTypes = (
'togglePreHistoryHidden' | 'updateChatDefaultBannedRights' | 'updateChatMemberBannedRights' | 'updateChatAdmin' | 'togglePreHistoryHidden' | 'updateChatDefaultBannedRights' | 'updateChatMemberBannedRights' | 'updateChatAdmin' |
'acceptInviteConfirmation' | 'acceptInviteConfirmation' |
// users // users
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' | 'loadCurrentUser' | 'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadCountryList' | 'loadTopUsers' | 'loadContactList' |
'updateProfile' | 'checkUsername' | 'updateContact' | 'deleteUser' | 'loadUser' | 'setUserSearchQuery' | 'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'updateContact' |
'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
// Channel / groups creation // Channel / groups creation
'createChannel' | 'createGroupChat' | 'resetChatCreation' | 'createChannel' | 'createGroupChat' | 'resetChatCreation' |
// settings // settings

View File

@ -1,5 +1,5 @@
import useReducer, { StateReducer, Dispatch } from '../useReducer'; import useReducer, { StateReducer, Dispatch } from '../useReducer';
import { countryList } from '../../util/phoneNumber'; import countryList from '../../util/countries';
export type FormState = { export type FormState = {
streetLine1: string; streetLine1: string;

View File

@ -1054,6 +1054,7 @@ help.getConfig#c4f9186b = Config;
help.getNearestDc#1fb33026 = NearestDc; help.getNearestDc#1fb33026 = NearestDc;
help.getSupport#9cdf08cd = help.Support; help.getSupport#9cdf08cd = help.Support;
help.acceptTermsOfService#ee72f79a id:DataJSON = Bool; help.acceptTermsOfService#ee72f79a id:DataJSON = Bool;
help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList;
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages; channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;

View File

@ -1055,6 +1055,7 @@ help.getConfig#c4f9186b = Config;
help.getNearestDc#1fb33026 = NearestDc; help.getNearestDc#1fb33026 = NearestDc;
help.getSupport#9cdf08cd = help.Support; help.getSupport#9cdf08cd = help.Support;
help.acceptTermsOfService#ee72f79a id:DataJSON = Bool; help.acceptTermsOfService#ee72f79a id:DataJSON = Bool;
help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList;
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages; channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;

View File

@ -180,6 +180,21 @@ addReducer('loadNearestCountry', (global) => {
})(); })();
}); });
addReducer('loadCountryList', (global, actions, payload = {}) => {
let { langCode } = payload;
if (!langCode) langCode = global.settings.byKey.language;
(async () => {
const countryList = await callApi('fetchCountryList', { langCode });
if (!countryList) return;
setGlobal({
...getGlobal(),
countryList,
});
})();
});
addReducer('setDeviceToken', (global, actions, deviceToken) => { addReducer('setDeviceToken', (global, actions, deviceToken) => {
setGlobal({ setGlobal({
...global, ...global,

View File

@ -4,6 +4,8 @@ import EMOJI_REGEX from '../lib/twemojiRegex';
// https://github.com/iamcal/emoji-data/issues/136 // https://github.com/iamcal/emoji-data/issues/136
const EXCLUDE_EMOJIS = ['female_sign', 'male_sign', 'medical_symbol']; const EXCLUDE_EMOJIS = ['female_sign', 'male_sign', 'medical_symbol'];
const ISO_FLAGS_OFFSET = 127397;
export type EmojiRawData = typeof import('emoji-data-ios/emoji-data.json'); export type EmojiRawData = typeof import('emoji-data-ios/emoji-data.json');
export type EmojiModule = { default: EmojiRawData }; export type EmojiModule = { default: EmojiRawData };
@ -17,7 +19,7 @@ const EMOJI_EXCEPTIONS: [string | RegExp, string][] = [
[/\u{1f3f3}\u200d\u{1f308}/gu, '\u{1f3f3}\ufe0f\u200d\u{1f308}'], // 🏳🌈 [/\u{1f3f3}\u200d\u{1f308}/gu, '\u{1f3f3}\ufe0f\u200d\u{1f308}'], // 🏳🌈
[/\u{1f3f3}\u200d\u26a7\ufe0f/gu, '\u{1f3f3}\ufe0f\u200d\u26a7\ufe0f'], // 🏳 [/\u{1f3f3}\u200d\u26a7\ufe0f/gu, '\u{1f3f3}\ufe0f\u200d\u26a7\ufe0f'], // 🏳
[/\u{1f937}\u200d\u2642/gu, '\u{1f937}\u200d\u2642\ufe0f'], // 🤷 [/\u{1f937}\u200d\u2642/gu, '\u{1f937}\u200d\u2642\ufe0f'], // 🤷
] ];
function unifiedToNative(unified: string) { function unifiedToNative(unified: string) {
const unicodes = unified.split('-'); const unicodes = unified.split('-');
@ -29,7 +31,8 @@ function unifiedToNative(unified: string) {
export function fixNonStandardEmoji(text: string) { export function fixNonStandardEmoji(text: string) {
// Non-standard sequences typically parsed as separate emojis, so no need to fix text without any // Non-standard sequences typically parsed as separate emojis, so no need to fix text without any
if (!text.match(EMOJI_REGEX)) return text; if (!text.match(EMOJI_REGEX)) return text;
for (let [regex, replacement] of EMOJI_EXCEPTIONS) { // eslint-disable-next-line no-restricted-syntax
for (const [regex, replacement] of EMOJI_EXCEPTIONS) {
text = text.replace(regex, replacement); text = text.replace(regex, replacement);
} }
@ -90,3 +93,11 @@ export function uncompressEmoji(data: EmojiRawData): EmojiData {
return emojiData; return emojiData;
} }
export function isoToEmoji(iso: string) {
const code = iso.toUpperCase();
if (!/^[A-Z]{2}$/.test(code)) return iso;
const codePoints = [...code].map((c) => c.codePointAt(0)! + ISO_FLAGS_OFFSET);
return String.fromCodePoint(...codePoints);
}

View File

@ -1,57 +1,90 @@
import countryList, { defaultPhoneNumberFormat } from './countries'; import { ApiCountryCode } from '../api/types';
import { flatten } from './iteratees';
export function getCountryById(id: string) { const PATTERN_PLACEHOLDER = 'X';
return countryList.find((c) => c.id === id) as Country; const DEFAULT_PATTERN = 'XXX XXX XXX XXX';
export function getCountryCodesByIso(phoneCodeList: ApiCountryCode[], iso: string) {
return phoneCodeList.filter((country) => country.iso2 === iso);
} }
// Empty groups are used to preserve 5 callback arguments for `replace` method export function getCountryFromPhoneNumber(phoneCodeList: ApiCountryCode[], input: string = '') {
function getPhoneNumberFormat(country?: Country) {
return country ? country.phoneFormat : defaultPhoneNumberFormat;
}
export function getCountryFromPhoneNumber(input: string) {
let phoneNumber = input.replace(/[^\d+]+/g, ''); let phoneNumber = input.replace(/[^\d+]+/g, '');
if (!phoneNumber.startsWith('+')) { if (phoneNumber.startsWith('+')) {
phoneNumber = `+${phoneNumber}`; phoneNumber = phoneNumber.substr(1);
} }
const possibleCountries = countryList const possibleCountries = phoneCodeList
.filter((country: Country) => phoneNumber.startsWith(country.code)) .filter((country) => phoneNumber.startsWith(country.countryCode));
const codesWithPrefix: { code: string; country: ApiCountryCode }[] = flatten(possibleCountries
.map((country) => (country.prefixes || ['']).map((prefix) => {
return {
code: `${country.countryCode}${prefix}`,
country,
};
})));
const bestMatches = codesWithPrefix
.filter(({ code }) => phoneNumber.startsWith(code))
.sort((a, b) => a.code.length - b.code.length); .sort((a, b) => a.code.length - b.code.length);
return possibleCountries[possibleCountries.length - 1]; return bestMatches[bestMatches.length - 1]?.country;
} }
export function formatPhoneNumber(input: string, country?: Country) { export function formatPhoneNumber(input: string, country?: ApiCountryCode) {
let phoneNumber = input.replace(/[^\d]+/g, ''); let phoneNumber = input.replace(/[^\d]+/g, '');
if (country) { if (country) {
phoneNumber = phoneNumber.substr(country.code.length - 1); phoneNumber = phoneNumber.substr(country.countryCode.length);
} else if (input.startsWith('+')) { } else if (input.startsWith('+')) {
return input; return input;
} }
const pattern = getBestPattern(phoneNumber, country?.patterns);
phoneNumber = phoneNumber.replace(getPhoneNumberFormat(country), (_, p1, p2, p3, p4, p5) => { const result: string[] = []; // Result character array
const separator = country && country.id === 'GB' ? ' ' : '-'; let j = 0; // Position inside pattern
for (let i = 0; i < phoneNumber.length; i++) {
while (pattern[j] !== PATTERN_PLACEHOLDER && j < pattern.length) {
result.push(pattern[j]);
if (pattern[j] === phoneNumber[i]) { // If pattern contains digits, move input position too
i++;
if (i === phoneNumber.length) break; // But don't overdo it, or it will insert full pattern unexpectedly
}
j++;
}
let output = ''; result.push(phoneNumber[i]); // For placeholder characters, setting current input digit
if (p1) output = `${p1}`; j++;
if (p2) output += ` ${p2}`; }
if (p3) output += `${separator}${p3}`;
if (p4) output += `${separator}${p4}`;
if (p5) output += `${separator}${p5}`;
return output;
});
return phoneNumber; return result.join('');
} }
export function formatPhoneNumberWithCode(phoneNumber: string) { function getBestPattern(numberWithoutCode: string, patterns?: string[]) {
if (!patterns || patterns.length === 0) return DEFAULT_PATTERN;
if (patterns.length === 1) return patterns[0];
const defaultPattern = patterns.find((pattern) => pattern.startsWith(PATTERN_PLACEHOLDER)) || DEFAULT_PATTERN;
const bestMatches = patterns.filter((pattern) => {
const stripped = pattern.replace(/[^\dX]+/g, '');
if (stripped.startsWith(PATTERN_PLACEHOLDER)) return false; // Don't consider default number format here
for (let i = 0; i < numberWithoutCode.length; i++) {
if (i > stripped.length - 1 || (stripped[i] !== PATTERN_PLACEHOLDER && stripped[i] !== numberWithoutCode[i])) {
return false;
}
}
return true;
});
// Playing it safe: if not sure, use default for that region
return bestMatches.length === 1 ? bestMatches[0] : defaultPattern;
}
export function formatPhoneNumberWithCode(phoneCodeList: ApiCountryCode[], phoneNumber: string) {
const numberWithPlus = phoneNumber.startsWith('+') ? phoneNumber : `+${phoneNumber}`; const numberWithPlus = phoneNumber.startsWith('+') ? phoneNumber : `+${phoneNumber}`;
const country = getCountryFromPhoneNumber(numberWithPlus); const country = getCountryFromPhoneNumber(phoneCodeList, numberWithPlus);
if (!country) { if (!country) {
return numberWithPlus; return numberWithPlus;
} }
return `${country.code} ${formatPhoneNumber(numberWithPlus, country)}`; return `+${country.countryCode} ${formatPhoneNumber(numberWithPlus, country)}`;
} }
export { countryList };