Profile Management: Add Contact (#1519)

This commit is contained in:
Alexander Zinchuk 2021-10-28 15:51:00 +03:00
parent d82cdce9ef
commit 394b91533b
10 changed files with 117 additions and 11 deletions

View File

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

View File

@ -149,6 +149,27 @@ export function updateContact({
}));
}
export function addContact({
id,
accessHash,
phoneNumber = '',
firstName = '',
lastName = '',
}: {
id: number;
accessHash?: string;
phoneNumber?: string;
firstName?: string;
lastName?: string;
}) {
return invokeRequest(new GramJs.contacts.AddContact({
id: buildInputEntity(id, accessHash) as GramJs.InputUser,
firstName,
lastName,
phone: phoneNumber,
}), true);
}
export async function deleteUser({
id,
accessHash,

View File

@ -9,9 +9,13 @@ import { IAnchorPosition } from '../../types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { disableScrolling, enableScrolling } from '../../util/scrollLock';
import { selectChat, selectNotifySettings, selectNotifyExceptions } from '../../modules/selectors';
import {
selectChat, selectNotifySettings, selectNotifyExceptions, selectUser,
} from '../../modules/selectors';
import { pick } from '../../util/iteratees';
import { isChatPrivate, getCanDeleteChat, selectIsChatMuted } from '../../modules/helpers';
import {
isChatPrivate, getCanDeleteChat, selectIsChatMuted, getCanAddContact,
} from '../../modules/helpers';
import useShowTransition from '../../hooks/useShowTransition';
import useLang from '../../hooks/useLang';
@ -23,7 +27,7 @@ import DeleteChatModal from '../common/DeleteChatModal';
import './HeaderMenuContainer.scss';
type DispatchProps = Pick<GlobalActions, (
'updateChatMutedState' | 'enterMessageSelectMode' | 'sendBotCommand' | 'restartBot' | 'openLinkedChat'
'updateChatMutedState' | 'enterMessageSelectMode' | 'sendBotCommand' | 'restartBot' | 'openLinkedChat' | 'addContact'
)>;
export type OwnProps = {
@ -48,6 +52,7 @@ type StateProps = {
chat?: ApiChat;
isPrivate?: boolean;
isMuted?: boolean;
canAddContact?: boolean;
canDeleteChat?: boolean;
hasLinkedChat?: boolean;
};
@ -68,6 +73,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
isMuted,
canDeleteChat,
hasLinkedChat,
canAddContact,
onSubscribeChannel,
onSearchClick,
onClose,
@ -77,6 +83,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
sendBotCommand,
restartBot,
openLinkedChat,
addContact,
}) => {
const [isMenuOpen, setIsMenuOpen] = useState(true);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -117,6 +124,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
closeMenu();
}, [chatId, closeMenu, openLinkedChat]);
const handleAddContactClick = useCallback(() => {
addContact({ userId: chatId });
closeMenu();
}, [addContact, chatId, closeMenu]);
const handleSubscribe = useCallback(() => {
onSubscribeChannel();
closeMenu();
@ -173,6 +185,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
{lang(isChannel ? 'Subscribe' : 'Join Group')}
</MenuItem>
)}
{canAddContact && (
<MenuItem
icon="add-user"
onClick={handleAddContactClick}
>
{lang('AddContact')}
</MenuItem>
)}
{IS_SINGLE_COLUMN_LAYOUT && canSearch && (
<MenuItem
icon="search"
@ -233,11 +253,15 @@ export default memo(withGlobal<OwnProps>(
if (!chat || chat.isRestricted) {
return {};
}
const isPrivate = isChatPrivate(chat.id);
const user = isPrivate ? selectUser(global, chatId) : undefined;
const canAddContact = user && getCanAddContact(user);
return {
chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
isPrivate: isChatPrivate(chat.id),
isPrivate,
canAddContact,
canDeleteChat: getCanDeleteChat(chat),
hasLinkedChat: Boolean(chat?.fullInfo?.linkedChatId),
};
@ -248,5 +272,6 @@ export default memo(withGlobal<OwnProps>(
'sendBotCommand',
'restartBot',
'openLinkedChat',
'addContact',
]),
)(HeaderMenuContainer));

View File

@ -16,8 +16,14 @@ import {
selectCurrentStickerSearch,
selectCurrentTextSearch,
selectIsChatWithSelf,
selectUser,
} from '../../modules/selectors';
import { isChatAdmin, isChatChannel, isChatPrivate } from '../../modules/helpers';
import {
getCanAddContact,
isChatAdmin,
isChatChannel,
isChatPrivate,
} from '../../modules/helpers';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useLang from '../../hooks/useLang';
@ -44,8 +50,10 @@ type OwnProps = {
};
type StateProps = {
canAddContact?: boolean;
canManage?: boolean;
isChannel?: boolean;
userId?: number;
messageSearchQuery?: string;
stickerSearchQuery?: string;
gifSearchQuery?: string;
@ -53,7 +61,7 @@ type StateProps = {
type DispatchProps = Pick<GlobalActions, (
'setLocalTextSearchQuery' | 'setStickerSearchQuery' | 'setGifSearchQuery' |
'searchTextMessagesLocal' | 'toggleManagement' | 'openHistoryCalendar'
'searchTextMessagesLocal' | 'toggleManagement' | 'openHistoryCalendar' | 'addContact'
)>;
const COLUMN_CLOSE_DELAY_MS = 300;
@ -94,6 +102,8 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
isAddingChatMembers,
profileState,
managementScreen,
canAddContact,
userId,
canManage,
isChannel,
onClose,
@ -107,6 +117,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
toggleManagement,
openHistoryCalendar,
shouldSkipAnimation,
addContact,
}) => {
// eslint-disable-next-line no-null/no-null
const backButtonRef = useRef<HTMLDivElement>(null);
@ -127,6 +138,10 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
setGifSearchQuery({ query });
}, [setGifSearchQuery]);
const handleAddContact = useCallback(() => {
addContact({ userId });
}, [addContact, userId]);
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen);
useEffect(() => {
@ -263,6 +278,17 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
<>
<h3>Profile</h3>
<section className="tools">
{canAddContact && (
<Button
round
color="translucent"
size="smaller"
ariaLabel={lang('AddContact')}
onClick={handleAddContact}
>
<i className="icon-add-user" />
</Button>
)}
{canManage && (
<Button
round
@ -323,10 +349,13 @@ export default memo(withGlobal<OwnProps>(
const { query: gifSearchQuery } = selectCurrentGifSearch(global) || {};
const chat = chatId ? selectChat(global, chatId) : undefined;
const isChannel = chat && isChatChannel(chat);
const user = isProfile && chatId && isChatPrivate(chatId) ? selectUser(global, chatId) : undefined;
const canAddContact = user && getCanAddContact(user);
const canManage = Boolean(
!isManagement
&& isProfile
&& !canAddContact
&& chat
&& !selectIsChatWithSelf(global, chat.id)
// chat.isCreator is for Basic Groups
@ -335,7 +364,9 @@ export default memo(withGlobal<OwnProps>(
return {
canManage,
canAddContact,
isChannel,
userId: user?.id,
messageSearchQuery,
stickerSearchQuery,
gifSearchQuery,
@ -348,5 +379,6 @@ export default memo(withGlobal<OwnProps>(
'searchTextMessagesLocal',
'toggleManagement',
'openHistoryCalendar',
'addContact',
]),
)(RightHeader));

View File

@ -504,7 +504,7 @@ export type ActionTypes = (
'acceptInviteConfirmation' |
// users
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadCountryList' | 'loadTopUsers' | 'loadContactList' |
'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'updateContact' |
'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' |
'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
// Channel / groups creation
'createChannel' | 'createGroupChat' | 'resetChatCreation' |

View File

@ -36,5 +36,5 @@ if (DEBUG) {
document.addEventListener('dblclick', () => {
// eslint-disable-next-line no-console
console.log('GLOBAL STATE', getGlobal());
console.warn('GLOBAL STATE', getGlobal());
});

View File

@ -969,6 +969,7 @@ contacts.getBlocked#f57c350f offset:int limit:int = contacts.Blocked;
contacts.search#11f812d8 q:string limit:int = contacts.Found;
contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer;
contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers;
contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates;
messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;
messages.getDialogs#a0ee3b73 flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:int = messages.Dialogs;
messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;

View File

@ -970,6 +970,7 @@ contacts.getBlocked#f57c350f offset:int limit:int = contacts.Blocked;
contacts.search#11f812d8 q:string limit:int = contacts.Found;
contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer;
contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers;
contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates;
messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;
messages.getDialogs#a0ee3b73 flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:int = messages.Dialogs;
messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;

View File

@ -6,7 +6,7 @@ import { ApiUser } from '../../../api/types';
import { ManagementProgress } from '../../../types';
import { debounce, throttle } from '../../../util/schedulers';
import { buildCollectionByKey } from '../../../util/iteratees';
import { buildCollectionByKey, pick } from '../../../util/iteratees';
import { isChatPrivate } from '../../helpers';
import { callApi } from '../../../api/gramjs';
import { selectChat, selectUser } from '../../selectors';
@ -150,7 +150,19 @@ async function updateContact(
setGlobal(updateManagementProgress(getGlobal(), ManagementProgress.InProgress));
const result = await callApi('updateContact', { phone: user.phoneNumber, firstName, lastName });
let result;
if (user.phoneNumber) {
result = await callApi('updateContact', { phone: user.phoneNumber, firstName, lastName });
} else {
const { id, accessHash } = user;
result = await callApi('addContact', {
id,
accessHash,
phoneNumber: '',
firstName,
lastName,
});
}
if (result) {
setGlobal(updateUser(
@ -217,6 +229,16 @@ addReducer('setUserSearchQuery', (global, actions, payload) => {
});
});
addReducer('addContact', (global, actions, payload) => {
const { userId } = payload!;
const user = selectUser(global, userId);
if (!user) {
return;
}
void callApi('addContact', pick(user, ['id', 'accessHash', 'firstName', 'lastName', 'phoneNumber']));
});
async function searchUsers(query: string) {
const result = await callApi('searchChats', { query });

View File

@ -183,6 +183,10 @@ export function isUserBot(user: ApiUser) {
return user.type === 'userTypeBot';
}
export function getCanAddContact(user: ApiUser) {
return !user.isContact && !isUserBot(user);
}
export function sortUserIds(
userIds: number[],
usersById: Record<number, ApiUser>,