Get countries and phone codes from server (#1422)
This commit is contained in:
parent
4a6dc47be0
commit
00daafc654
8
src/@types/global.d.ts
vendored
8
src/@types/global.d.ts
vendored
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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[];
|
||||||
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
]),
|
]),
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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, [
|
||||||
|
|||||||
@ -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']),
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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, [
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -15,6 +15,10 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
serverTimeOffset: 0,
|
serverTimeOffset: 0,
|
||||||
|
|
||||||
authRememberMe: true,
|
authRememberMe: true,
|
||||||
|
countryList: {
|
||||||
|
phoneCodes: [],
|
||||||
|
general: [],
|
||||||
|
},
|
||||||
|
|
||||||
blocked: {
|
blocked: {
|
||||||
ids: [],
|
ids: [],
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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 };
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user