New Contact: Allow to rename and add phone number (#1777)
This commit is contained in:
parent
1c50550079
commit
6d063c2132
@ -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",
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
16
src/components/main/NewContactModal.async.tsx
Normal file
16
src/components/main/NewContactModal.async.tsx
Normal 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);
|
||||||
38
src/components/main/NewContactModal.scss
Normal file
38
src/components/main/NewContactModal.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
235
src/components/main/NewContactModal.tsx
Normal file
235
src/components/main/NewContactModal.tsx
Normal 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));
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -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' |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user