New Contact: Allow to rename and add phone number (#1777)

This commit is contained in:
Alexander Zinchuk 2022-03-19 21:20:02 +01:00
parent 1c50550079
commit 6d063c2132
18 changed files with 436 additions and 39 deletions

View File

@ -2,6 +2,9 @@
"extends": [ "extends": [
"stylelint-config-recommended-scss" "stylelint-config-recommended-scss"
], ],
"ignoreFiles": [
"dist/*.css"
],
"plugins": [ "plugins": [
"stylelint-declaration-block-no-ignored-properties", "stylelint-declaration-block-no-ignored-properties",
"stylelint-high-performance-animation", "stylelint-high-performance-animation",

View File

@ -29,7 +29,7 @@ export {
export { export {
fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers, fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers,
addContact, updateContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam, updateContact, importContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam,
} from './users'; } from './users';
export { export {

View File

@ -17,8 +17,7 @@ import {
import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users'; import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiPhoto } from '../apiBuilders/common'; import { buildApiPhoto } from '../apiBuilders/common';
import localDb from '../localDb'; import { addEntitiesWithPhotosToLocalDb, addPhotoToLocalDb, addUserToLocalDb } from '../helpers';
import { addEntitiesWithPhotosToLocalDb, addPhotoToLocalDb } from '../helpers';
import { buildApiPeerId } from '../apiBuilders/peers'; import { buildApiPeerId } from '../apiBuilders/peers';
let onUpdate: OnApiUpdate; let onUpdate: OnApiUpdate;
@ -115,7 +114,7 @@ export async function fetchContactList() {
result.users.forEach((user) => { result.users.forEach((user) => {
if (user instanceof GramJs.User) { if (user instanceof GramJs.User) {
localDb.users[buildApiPeerId(user.id, 'user')] = user; addUserToLocalDb(user, true);
} }
}); });
@ -135,14 +134,14 @@ export async function fetchUsers({ users }: { users: ApiUser[] }) {
result.forEach((user) => { result.forEach((user) => {
if (user instanceof GramJs.User) { if (user instanceof GramJs.User) {
localDb.users[buildApiPeerId(user.id, 'user')] = user; addUserToLocalDb(user, true);
} }
}); });
return buildApiUsersAndStatuses(result); return buildApiUsersAndStatuses(result);
} }
export function updateContact({ export async function importContact({
phone, phone,
firstName, firstName,
lastName, lastName,
@ -151,33 +150,42 @@ export function updateContact({
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
}) { }) {
return invokeRequest(new GramJs.contacts.ImportContacts({ const result = await invokeRequest(new GramJs.contacts.ImportContacts({
contacts: [buildInputContact({ contacts: [buildInputContact({
phone: phone || '', phone: phone || '',
firstName: firstName || '', firstName: firstName || '',
lastName: lastName || '', lastName: lastName || '',
})], })],
}), true); }));
if (result instanceof GramJs.contacts.ImportedContacts && result.users.length) {
addUserToLocalDb(result.users[0]);
}
return result?.imported.length ? buildApiPeerId(result.imported[0].userId, 'user') : undefined;
} }
export function addContact({ export function updateContact({
id, id,
accessHash, accessHash,
phoneNumber = '', phoneNumber = '',
firstName = '', firstName = '',
lastName = '', lastName = '',
shouldSharePhoneNumber = false,
}: { }: {
id: string; id: string;
accessHash?: string; accessHash?: string;
phoneNumber?: string; phoneNumber?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
shouldSharePhoneNumber?: boolean;
}) { }) {
return invokeRequest(new GramJs.contacts.AddContact({ return invokeRequest(new GramJs.contacts.AddContact({
id: buildInputEntity(id, accessHash) as GramJs.InputUser, id: buildInputEntity(id, accessHash) as GramJs.InputUser,
firstName, firstName,
lastName, lastName,
phone: phoneNumber, phone: phoneNumber,
...(shouldSharePhoneNumber && { addPhonePrivacyException: shouldSharePhoneNumber }),
}), true); }), true);
} }

View File

@ -5,6 +5,7 @@ export { default as Dialogs } from '../components/main/Dialogs';
export { default as Notifications } from '../components/main/Notifications'; export { default as Notifications } from '../components/main/Notifications';
export { default as SafeLinkModal } from '../components/main/SafeLinkModal'; export { default as SafeLinkModal } from '../components/main/SafeLinkModal';
export { default as HistoryCalendar } from '../components/main/HistoryCalendar'; export { default as HistoryCalendar } from '../components/main/HistoryCalendar';
export { default as NewContactModal } from '../components/main/NewContactModal';
export { default as CalendarModal } from '../components/common/CalendarModal'; export { default as CalendarModal } from '../components/common/CalendarModal';
export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal'; export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal';

View File

@ -10,11 +10,13 @@ import { throttle } from '../../../util/schedulers';
import { filterUsersByName, sortUserIds } from '../../../global/helpers'; import { filterUsersByName, sortUserIds } from '../../../global/helpers';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import PrivateChatInfo from '../../common/PrivateChatInfo'; import PrivateChatInfo from '../../common/PrivateChatInfo';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import FloatingActionButton from '../../ui/FloatingActionButton';
export type OwnProps = { export type OwnProps = {
filter: string; filter: string;
@ -43,8 +45,11 @@ const ContactList: FC<OwnProps & StateProps> = ({
const { const {
loadContactList, loadContactList,
openChat, openChat,
openNewContactDialog,
} = getActions(); } = getActions();
const lang = useLang();
// Due to the parent Transition, this component never gets unmounted, // Due to the parent Transition, this component never gets unmounted,
// that's why we use throttled API call on every update. // that's why we use throttled API call on every update.
useEffect(() => { useEffect(() => {
@ -91,6 +96,13 @@ const ContactList: FC<OwnProps & StateProps> = ({
) : ( ) : (
<Loading key="loading" /> <Loading key="loading" />
)} )}
<FloatingActionButton
isShown
onClick={openNewContactDialog}
ariaLabel={lang('CreateNewContact')}
>
<i className="icon-add-user-filled" />
</FloatingActionButton>
</InfiniteScroll> </InfiniteScroll>
); );
}; };

View File

@ -48,6 +48,7 @@ import HistoryCalendar from './HistoryCalendar.async';
import GroupCall from '../calls/group/GroupCall.async'; import GroupCall from '../calls/group/GroupCall.async';
import ActiveCallHeader from '../calls/ActiveCallHeader.async'; import ActiveCallHeader from '../calls/ActiveCallHeader.async';
import CallFallbackConfirm from '../calls/CallFallbackConfirm.async'; import CallFallbackConfirm from '../calls/CallFallbackConfirm.async';
import NewContactModal from './NewContactModal.async';
import './Main.scss'; import './Main.scss';
@ -73,6 +74,8 @@ type StateProps = {
wasTimeFormatSetManually?: boolean; wasTimeFormatSetManually?: boolean;
isCallFallbackConfirmOpen: boolean; isCallFallbackConfirmOpen: boolean;
addedSetIds?: string[]; addedSetIds?: string[];
newContactUserId?: string;
newContactByPhoneNumber?: boolean;
}; };
const NOTIFICATION_INTERVAL = 1000; const NOTIFICATION_INTERVAL = 1000;
@ -104,6 +107,8 @@ const Main: FC<StateProps> = ({
wasTimeFormatSetManually, wasTimeFormatSetManually,
isCallFallbackConfirmOpen, isCallFallbackConfirmOpen,
addedSetIds, addedSetIds,
newContactUserId,
newContactByPhoneNumber,
}) => { }) => {
const { const {
sync, sync,
@ -330,6 +335,11 @@ const Main: FC<StateProps> = ({
<ActiveCallHeader groupCallId={activeGroupCallId} /> <ActiveCallHeader groupCallId={activeGroupCallId} />
</> </>
)} )}
<NewContactModal
isOpen={Boolean(newContactUserId || newContactByPhoneNumber)}
userId={newContactUserId}
isByPhoneNumber={newContactByPhoneNumber}
/>
<DownloadManager /> <DownloadManager />
<CallFallbackConfirm isOpen={isCallFallbackConfirmOpen} /> <CallFallbackConfirm isOpen={isCallFallbackConfirmOpen} />
<UnreadCount isForAppBadge /> <UnreadCount isForAppBadge />
@ -388,6 +398,8 @@ export default memo(withGlobal(
wasTimeFormatSetManually, wasTimeFormatSetManually,
isCallFallbackConfirmOpen: Boolean(global.groupCalls.isFallbackConfirmOpen), isCallFallbackConfirmOpen: Boolean(global.groupCalls.isFallbackConfirmOpen),
addedSetIds: global.stickers.added.setIds, addedSetIds: global.stickers.added.setIds,
newContactUserId: global.newContact?.userId,
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
}; };
}, },
)(Main)); )(Main));

View File

@ -0,0 +1,16 @@
import React, { FC, memo } from '../../lib/teact/teact';
import { Bundles } from '../../util/moduleLoader';
import { OwnProps } from './NewContactModal';
import useModuleLoader from '../../hooks/useModuleLoader';
const NewContactModalAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const NewContactModal = useModuleLoader(Bundles.Extra, 'NewContactModal', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return NewContactModal ? <NewContactModal {...props} /> : undefined;
};
export default memo(NewContactModalAsync);

View File

@ -0,0 +1,38 @@
.NewContactModal {
.modal-dialog {
max-width: 28rem;
}
&__new-contact {
display: flex;
&-fieldset {
flex: 1;
margin-inline-start: 1rem;
}
}
&__profile {
display: flex;
align-items: center;
margin-bottom: 2rem;
&-info {
margin-inline-start: 1rem;
}
}
&__user-status {
color: var(--color-text-secondary);
}
&__phone-number {
font-size: 1.5rem;
margin-bottom: 0;
}
&__help-text {
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
}

View File

@ -0,0 +1,235 @@
import React, {
FC, memo, useCallback, useEffect, useRef, useState,
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import { ApiCountryCode, ApiUser, ApiUserStatus } from '../../api/types';
import { IS_TOUCH_ENV } from '../../util/environment';
import { getUserStatus } from '../../global/helpers';
import { selectUser, selectUserStatus } from '../../global/selectors';
import renderText from '../common/helpers/renderText';
import { formatPhoneNumberWithCode } from '../../util/phoneNumber';
import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import Modal from '../ui/Modal';
import Avatar from '../common/Avatar';
import InputText from '../ui/InputText';
import Checkbox from '../ui/Checkbox';
import Button from '../ui/Button';
import './NewContactModal.scss';
const ANIMATION_DURATION = 200;
export type OwnProps = {
isOpen: boolean;
userId?: string;
isByPhoneNumber?: boolean;
};
type StateProps = {
user?: ApiUser;
userStatus?: ApiUserStatus;
phoneCodeList: ApiCountryCode[];
serverTimeOffset?: number;
};
const NewContactModal: FC<OwnProps & StateProps> = ({
isOpen,
userId,
isByPhoneNumber,
user,
userStatus,
phoneCodeList,
serverTimeOffset,
}) => {
const { updateContact, importContact, closeNewContactDialog } = getActions();
const lang = useLang();
const renderingUser = useCurrentOrPrev(user);
const renderingIsByPhoneNumber = useCurrentOrPrev(isByPhoneNumber);
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null);
const [isShown, markIsShown, unmarkIsShown] = useFlag();
const [firstName, setFirstName] = useState<string>(renderingUser?.firstName ?? '');
const [lastName, setLastName] = useState<string>(renderingUser?.lastName ?? '');
const [phone, setPhone] = useState<string>(renderingUser?.phoneNumber ?? '');
const [shouldSharePhoneNumber, setShouldSharePhoneNumber] = useState<boolean>(true);
const canBeSubmitted = Boolean(firstName && (!isByPhoneNumber || phone));
useEffect(() => {
if (isOpen) {
markIsShown();
setFirstName(renderingUser?.firstName ?? '');
setLastName(renderingUser?.lastName ?? '');
setPhone(renderingUser?.phoneNumber ?? '');
setShouldSharePhoneNumber(true);
}
}, [isOpen, markIsShown, renderingUser?.firstName, renderingUser?.lastName, renderingUser?.phoneNumber]);
useEffect(() => {
if (!IS_TOUCH_ENV && isShown) {
setTimeout(() => { inputRef.current?.focus(); }, ANIMATION_DURATION);
}
}, [isShown]);
const handleFirstNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setFirstName(e.target.value);
}, []);
const handlePhoneChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setPhone(formatPhoneNumberWithCode(phoneCodeList, e.target.value));
}, [phoneCodeList]);
const handleLastNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setLastName(e.target.value);
}, []);
const handleClose = useCallback(() => {
closeNewContactDialog();
setFirstName('');
setLastName('');
setPhone('');
}, [closeNewContactDialog]);
const handleSubmit = useCallback(() => {
if (isByPhoneNumber || !userId) {
importContact({
firstName,
lastName,
phoneNumber: phone,
});
} else {
updateContact({
userId,
firstName,
lastName,
shouldSharePhoneNumber,
});
}
}, [firstName, importContact, isByPhoneNumber, lastName, phone, shouldSharePhoneNumber, updateContact, userId]);
if (!isOpen && !isShown) {
return undefined;
}
function renderAddContact() {
return (
<>
<div className="NewContactModal__profile" dir={lang.isRtl ? 'rtl' : undefined}>
<Avatar size="jumbo" user={renderingUser} text={`${firstName} ${lastName}`} />
<div className="NewContactModal__profile-info">
<p className="NewContactModal__phone-number">
{renderingUser?.phoneNumber
? formatPhoneNumberWithCode(phoneCodeList, renderingUser.phoneNumber)
: lang('MobileHidden')}
</p>
<span className="NewContactModal__user-status" dir="auto">
{getUserStatus(lang, renderingUser!, userStatus, serverTimeOffset!)}
</span>
</div>
</div>
<InputText
ref={inputRef}
value={firstName}
label={lang('FirstName')}
tabIndex={0}
onChange={handleFirstNameChange}
/>
<InputText
value={lastName}
label={lang('LastName')}
tabIndex={0}
onChange={handleLastNameChange}
/>
<p className="NewContactModal__help-text">
{renderText(lang('NewContact.Phone.Hidden.Text', renderingUser?.firstName), ['emoji', 'simple_markdown'])}
</p>
<Checkbox
checked={shouldSharePhoneNumber}
tabIndex={0}
onCheck={setShouldSharePhoneNumber}
label={lang('lng_new_contact_share')}
/>
<p className="NewContactModal__help-text">
{renderText(lang('AddContact.SharedContactExceptionInfo', renderingUser?.firstName))}
</p>
</>
);
}
function renderCreateContact() {
return (
<div className="NewContactModal__new-contact" dir={lang.isRtl ? 'rtl' : undefined}>
<Avatar size="jumbo" text={`${firstName} ${lastName}`} />
<div className="NewContactModal__new-contact-fieldset">
<InputText
ref={inputRef}
value={phone}
inputMode="tel"
label={lang('lng_contact_phone')}
tabIndex={0}
onChange={handlePhoneChange}
/>
<InputText
value={firstName}
label={lang('FirstName')}
tabIndex={0}
onChange={handleFirstNameChange}
/>
<InputText
value={lastName}
label={lang('LastName')}
tabIndex={0}
onChange={handleLastNameChange}
/>
</div>
</div>
);
}
return (
<Modal
className="NewContactModal"
title={lang('NewContact')}
isOpen={isOpen}
onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown}
>
{renderingUser && renderAddContact()}
{renderingIsByPhoneNumber && renderCreateContact()}
<div className="dialog-buttons">
<Button
isText
className="confirm-dialog-button"
onClick={handleClose}
>
{lang('Cancel')}
</Button>
<Button
isText
className="confirm-dialog-button"
disabled={!canBeSubmitted}
onClick={handleSubmit}
>
{lang('Done')}
</Button>
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { userId }): StateProps => {
return {
user: userId ? selectUser(global, userId) : undefined,
userStatus: userId ? selectUserStatus(global, userId) : undefined,
serverTimeOffset: global.serverTimeOffset,
phoneCodeList: global.countryList.phoneCodes,
};
},
)(NewContactModal));

View File

@ -35,7 +35,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
chatId, className, chat, user, settings, currentUserId, chatId, className, chat, user, settings, currentUserId,
}) => { }) => {
const { const {
addContact, openAddContactDialog,
blockContact, blockContact,
reportSpam, reportSpam,
deleteChat, deleteChat,
@ -57,11 +57,11 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
const isBasicGroup = chat && isChatBasicGroup(chat); const isBasicGroup = chat && isChatBasicGroup(chat);
const handleAddContact = useCallback(() => { const handleAddContact = useCallback(() => {
addContact({ chatId }); openAddContactDialog({ userId: chatId });
if (isAutoArchived) { if (isAutoArchived) {
toggleChatArchived({ chatId }); toggleChatArchived({ chatId });
} }
}, [addContact, isAutoArchived, toggleChatArchived, chatId]); }, [openAddContactDialog, isAutoArchived, toggleChatArchived, chatId]);
const handleConfirmBlock = useCallback(() => { const handleConfirmBlock = useCallback(() => {
closeBlockUserModal(); closeBlockUserModal();
@ -103,7 +103,6 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
{canAddContact && ( {canAddContact && (
<Button <Button
isText isText
ripple
fluid fluid
size="tiny" size="tiny"
className="UserReportPanel--Button" className="UserReportPanel--Button"

View File

@ -91,7 +91,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
joinGroupCall, joinGroupCall,
createGroupCall, createGroupCall,
openLinkedChat, openLinkedChat,
addContact, openAddContactDialog,
openCallFallbackConfirm, openCallFallbackConfirm,
toggleStatistics, toggleStatistics,
} = getActions(); } = getActions();
@ -150,9 +150,9 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
}, [chatId, closeMenu, openLinkedChat]); }, [chatId, closeMenu, openLinkedChat]);
const handleAddContactClick = useCallback(() => { const handleAddContactClick = useCallback(() => {
addContact({ userId: chatId }); openAddContactDialog({ userId: chatId });
closeMenu(); closeMenu();
}, [addContact, chatId, closeMenu]); }, [openAddContactDialog, chatId, closeMenu]);
const handleSubscribe = useCallback(() => { const handleSubscribe = useCallback(() => {
onSubscribeChannel(); onSubscribeChannel();

View File

@ -133,7 +133,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
searchTextMessagesLocal, searchTextMessagesLocal,
toggleManagement, toggleManagement,
openHistoryCalendar, openHistoryCalendar,
addContact, openAddContactDialog,
toggleStatistics, toggleStatistics,
setEditingExportedInvite, setEditingExportedInvite,
deleteExportedChatInvite, deleteExportedChatInvite,
@ -171,8 +171,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
}, [setGifSearchQuery]); }, [setGifSearchQuery]);
const handleAddContact = useCallback(() => { const handleAddContact = useCallback(() => {
addContact({ userId }); openAddContactDialog({ userId });
}, [addContact, userId]); }, [openAddContactDialog, userId]);
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen); const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen);

View File

@ -17,6 +17,7 @@ type OwnProps = {
subLabel?: string; subLabel?: string;
checked: boolean; checked: boolean;
disabled?: boolean; disabled?: boolean;
tabIndex?: number;
round?: boolean; round?: boolean;
blocking?: boolean; blocking?: boolean;
isLoading?: boolean; isLoading?: boolean;
@ -32,6 +33,7 @@ const Checkbox: FC<OwnProps> = ({
label, label,
subLabel, subLabel,
checked, checked,
tabIndex,
disabled, disabled,
round, round,
blocking, blocking,
@ -67,6 +69,7 @@ const Checkbox: FC<OwnProps> = ({
value={value} value={value}
checked={checked} checked={checked}
disabled={disabled} disabled={disabled}
tabIndex={tabIndex}
onChange={handleChange} onChange={handleChange}
/> />
<div className="Checkbox-main"> <div className="Checkbox-main">

View File

@ -19,6 +19,7 @@ type OwnProps = {
placeholder?: string; placeholder?: string;
autoComplete?: string; autoComplete?: string;
maxLength?: number; maxLength?: number;
tabIndex?: number;
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
onChange?: (e: ChangeEvent<HTMLInputElement>) => void; onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onInput?: (e: FormEvent<HTMLInputElement>) => void; onInput?: (e: FormEvent<HTMLInputElement>) => void;
@ -42,6 +43,7 @@ const InputText: FC<OwnProps> = ({
autoComplete, autoComplete,
inputMode, inputMode,
maxLength, maxLength,
tabIndex,
onChange, onChange,
onInput, onInput,
onKeyPress, onKeyPress,
@ -70,6 +72,7 @@ const InputText: FC<OwnProps> = ({
id={id} id={id}
dir="auto" dir="auto"
value={value || ''} value={value || ''}
tabIndex={tabIndex}
placeholder={placeholder} placeholder={placeholder}
maxLength={maxLength} maxLength={maxLength}
autoComplete={autoComplete} autoComplete={autoComplete}

View File

@ -6,15 +6,16 @@ import { ApiUser } from '../../../api/types';
import { ManagementProgress } from '../../../types'; import { ManagementProgress } from '../../../types';
import { debounce, throttle } from '../../../util/schedulers'; import { debounce, throttle } from '../../../util/schedulers';
import { buildCollectionByKey, pick, unique } from '../../../util/iteratees'; import { buildCollectionByKey, unique } from '../../../util/iteratees';
import { isUserBot, isUserId } from '../../helpers'; import { isUserBot, isUserId } from '../../helpers';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors'; import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
import { import {
addChats, addUsers, replaceUserStatuses, updateChat, updateManagementProgress, updateUser, updateUsers, addChats, addUsers, closeNewContactDialog, replaceUserStatuses, updateChat, updateManagementProgress, updateUser,
updateUserSearch, updateUserSearchFetchingStatus, updateUsers, updateUserSearch, updateUserSearchFetchingStatus,
} from '../../reducers'; } from '../../reducers';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import * as langProvider from '../../../util/langProvider';
const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true); const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true);
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
@ -105,10 +106,10 @@ addActionHandler('loadCommonChats', async (global) => {
addActionHandler('updateContact', (global, actions, payload) => { addActionHandler('updateContact', (global, actions, payload) => {
const { const {
userId, isMuted, firstName, lastName, userId, isMuted = false, firstName, lastName, shouldSharePhoneNumber,
} = payload!; } = payload;
void updateContact(userId, isMuted, firstName, lastName); void updateContact(userId, isMuted, firstName, lastName, shouldSharePhoneNumber);
}); });
addActionHandler('deleteContact', (global, actions, payload) => { addActionHandler('deleteContact', (global, actions, payload) => {
@ -168,8 +169,9 @@ async function updateContact(
isMuted: boolean, isMuted: boolean,
firstName: string, firstName: string,
lastName?: string, lastName?: string,
shouldSharePhoneNumber?: boolean,
) { ) {
const global = getGlobal(); let global = getGlobal();
const user = selectUser(global, userId); const user = selectUser(global, userId);
if (!user) { if (!user) {
return; return;
@ -180,22 +182,24 @@ async function updateContact(
setGlobal(updateManagementProgress(getGlobal(), ManagementProgress.InProgress)); setGlobal(updateManagementProgress(getGlobal(), ManagementProgress.InProgress));
let result; let result;
if (user.phoneNumber) { if (!user.isContact && user.phoneNumber) {
result = await callApi('updateContact', { phone: user.phoneNumber, firstName, lastName }); result = await callApi('importContact', { phone: user.phoneNumber, firstName, lastName });
} else { } else {
const { id, accessHash } = user; const { id, accessHash } = user;
result = await callApi('addContact', { result = await callApi('updateContact', {
id, id,
accessHash, accessHash,
phoneNumber: '', phoneNumber: '',
firstName, firstName,
lastName, lastName,
shouldSharePhoneNumber,
}); });
} }
global = getGlobal();
if (result) { if (result) {
setGlobal(updateUser( setGlobal(updateUser(
getGlobal(), global,
user.id, user.id,
{ {
firstName, firstName,
@ -204,7 +208,9 @@ async function updateContact(
)); ));
} }
setGlobal(updateManagementProgress(getGlobal(), ManagementProgress.Complete)); global = updateManagementProgress(global, ManagementProgress.Complete);
global = closeNewContactDialog(global);
setGlobal(global);
} }
async function deleteContact(userId: string) { async function deleteContact(userId: string) {
@ -257,14 +263,22 @@ addActionHandler('setUserSearchQuery', (global, actions, payload) => {
}); });
}); });
addActionHandler('addContact', (global, actions, payload) => { addActionHandler('importContact', async (global, actions, payload) => {
const { userId } = payload!; const { phoneNumber: phone, firstName, lastName } = payload!;
const user = selectUser(global, userId);
if (!user) { const result = await callApi('importContact', { phone, firstName, lastName });
return;
if (result) {
actions.openChat({ id: result });
return closeNewContactDialog(getGlobal());
} }
void callApi('addContact', pick(user, ['id', 'accessHash', 'firstName', 'lastName', 'phoneNumber'])); actions.showNotification({
message: langProvider.getTranslation('Contacts.PhoneNumber.NotRegistred'),
});
return undefined;
}); });
addActionHandler('reportSpam', (global, actions, payload) => { addActionHandler('reportSpam', (global, actions, payload) => {

View File

@ -1,6 +1,6 @@
import { addActionHandler } from '../../index'; import { addActionHandler } from '../../index';
import { updateUserSearch } from '../../reducers'; import { closeNewContactDialog, updateUserSearch } from '../../reducers';
addActionHandler('setUserSearchQuery', (global, actions, payload) => { addActionHandler('setUserSearchQuery', (global, actions, payload) => {
const { query } = payload!; const { query } = payload!;
@ -12,3 +12,25 @@ addActionHandler('setUserSearchQuery', (global, actions, payload) => {
query, query,
}); });
}); });
addActionHandler('openAddContactDialog', (global, actions, payload) => {
const { userId } = payload!;
return {
...global,
newContact: { userId },
};
});
addActionHandler('openNewContactDialog', (global) => {
return {
...global,
newContact: {
isByPhoneNumber: true,
},
};
});
addActionHandler('closeNewContactDialog', (global) => {
return closeNewContactDialog(global);
});

View File

@ -210,3 +210,10 @@ export function addUserStatuses(global: GlobalState, newById: Record<string, Api
return global; return global;
} }
export function closeNewContactDialog(global: GlobalState): GlobalState {
return {
...global,
newContact: undefined,
};
}

View File

@ -510,6 +510,11 @@ export type GlobalState = {
statistics: { statistics: {
byChatId: Record<string, ApiStatistics>; byChatId: Record<string, ApiStatistics>;
}; };
newContact?: {
userId?: string;
isByPhoneNumber?: boolean;
};
}; };
export interface ActionPayloads { export interface ActionPayloads {
@ -578,6 +583,25 @@ export interface ActionPayloads {
setAudioPlayerOrigin: { setAudioPlayerOrigin: {
origin: AudioOrigin; origin: AudioOrigin;
}; };
// Users
openAddContactDialog: {
userId?: string;
};
openNewContactDialog: undefined;
closeNewContactDialog: undefined;
importContact: {
phoneNumber: string;
firstName: string;
lastName?: string;
};
updateContact: {
userId: string;
firstName: string;
lastName?: string;
isMuted?: boolean;
shouldSharePhoneNumber?: boolean;
};
} }
export type NonTypedActionNames = ( export type NonTypedActionNames = (
@ -641,7 +665,7 @@ export type NonTypedActionNames = (
'acceptInviteConfirmation' | 'acceptInviteConfirmation' |
// users // users
'loadFullUser' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' | 'loadFullUser' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' |
'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' | 'loadCurrentUser' | 'updateProfile' | 'checkUsername' |
'deleteContact' | 'loadUser' | 'setUserSearchQuery' | 'loadCommonChats' | 'reportSpam' | 'deleteContact' | 'loadUser' | 'setUserSearchQuery' | 'loadCommonChats' | 'reportSpam' |
// chat creation // chat creation
'createChannel' | 'createGroupChat' | 'resetChatCreation' | 'createChannel' | 'createGroupChat' | 'resetChatCreation' |