From a215aa1083224322534a4551f8c4a5d6439093b8 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 4 Mar 2022 16:20:23 +0300 Subject: [PATCH] Middle Header: Introduce Add Contact and Block User buttons (#1735) --- src/api/gramjs/apiBuilders/users.ts | 19 +++- src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/methods/users.ts | 9 ++ src/api/gramjs/updater.ts | 10 +- src/api/types/users.ts | 8 ++ src/components/middle/MiddleColumn.tsx | 22 +++- src/components/middle/MiddleHeader.tsx | 12 +- src/components/middle/UserReportPanel.scss | 36 ++++++ src/components/middle/UserReportPanel.tsx | 124 +++++++++++++++++++++ src/global/types.ts | 2 +- src/lib/gramjs/tl/apiTl.js | 1 + src/lib/gramjs/tl/static/api.json | 1 + src/modules/actions/api/users.ts | 10 ++ 13 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 src/components/middle/UserReportPanel.scss create mode 100644 src/components/middle/UserReportPanel.tsx diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 04a58eb51..9379e0ec2 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -1,13 +1,13 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { - ApiBotCommand, ApiUser, ApiUserStatus, ApiUserType, + ApiBotCommand, ApiUser, ApiUserSettings, ApiUserStatus, ApiUserType, } from '../../types'; import { buildApiPeerId } from './peers'; export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUser { const { fullUser: { - about, commonChatsCount, pinnedMsgId, botInfo, blocked, + about, commonChatsCount, pinnedMsgId, botInfo, blocked, settings, }, users, } = mtpUserFull; @@ -16,6 +16,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse return { ...user, + settings: buildApiUserSettings(settings), fullInfo: { bio: about, commonChatsCount, @@ -111,3 +112,17 @@ export function buildApiUsersAndStatuses(mtpUsers: GramJs.TypeUser[]) { return { users, userStatusesById }; } + +export function buildApiUserSettings({ + autoarchived, + reportSpam, + addContact, + blockContact, +}: GramJs.PeerSettings): ApiUserSettings { + return { + isAutoArchived: Boolean(autoarchived), + canReportSpam: Boolean(reportSpam), + canAddContact: Boolean(addContact), + canBlockContact: Boolean(blockContact), + }; +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 1eb173a74..31f920d0c 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -28,7 +28,7 @@ export { export { fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers, - addContact, updateContact, deleteContact, fetchProfilePhotos, fetchCommonChats, + addContact, updateContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam, } from './users'; export { diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 6214a8d17..ce4dcbfb2 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -51,6 +51,7 @@ export async function fetchFullUser({ '@type': 'updateUser', id, user: { + settings: userWithFullInfo.settings, fullInfo: userWithFullInfo.fullInfo, }, }); @@ -247,6 +248,14 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) { }; } +export function reportSpam(user: ApiUser) { + const { id, accessHash } = user; + + return invokeRequest(new GramJs.messages.ReportSpam({ + peer: buildInputPeer(id, accessHash), + }), true); +} + function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice | GramJs.messages.Chats)) { if ('chats' in result) { addEntitiesWithPhotosToLocalDb(result.chats); diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 136943269..93d6c4587 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -23,7 +23,7 @@ import { buildApiChatFromPreview, buildApiChatFolder, } from './apiBuilders/chats'; -import { buildApiUser, buildApiUserStatus } from './apiBuilders/users'; +import { buildApiUser, buildApiUserSettings, buildApiUserStatus } from './apiBuilders/users'; import { buildMessageFromUpdate, isMessageWithMedia, @@ -771,11 +771,12 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { }); } else if (update instanceof GramJs.UpdatePeerSettings) { // eslint-disable-next-line @typescript-eslint/naming-convention - const { _entities } = update; + const { _entities, settings } = update; if (!_entities) { return; } + if (_entities?.length) { _entities .filter((e) => e instanceof GramJs.User && !e.contact) @@ -797,7 +798,10 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { onUpdate({ '@type': 'updateUser', id: user.id, - user, + user: { + ...user, + ...(settings && { settings: buildApiUserSettings(settings) }), + }, }); }); } diff --git a/src/api/types/users.ts b/src/api/types/users.ts index c723bef7b..e76b53409 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -26,6 +26,14 @@ export interface ApiUser { // Obtained from GetFullUser / UserFullInfo fullInfo?: ApiUserFullInfo; + settings?: ApiUserSettings; +} + +export interface ApiUserSettings { + isAutoArchived?: boolean; + canReportSpam?: boolean; + canAddContact?: boolean; + canBlockContact?: boolean; } export interface ApiUserFullInfo { diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 39da6a54d..75f4c88cd 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -42,6 +42,7 @@ import { selectIsUserBlocked, selectPinnedIds, selectTheme, + selectUser, } from '../../modules/selectors'; import { getCanPostInChat, getMessageSendingRestrictionReason, isChatChannel, isChatSuperGroup, isUserId, @@ -106,10 +107,12 @@ type StateProps = { currentTransitionKey: number; messageLists?: GlobalMessageList[]; isChannel?: boolean; + isUserFull?: boolean; canSubscribe?: boolean; canStartBot?: boolean; canRestartBot?: boolean; activeEmojiInteractions?: ActiveEmojiInteraction[]; + lastSyncTime?: number; }; const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined; @@ -147,15 +150,18 @@ const MiddleColumn: FC = ({ shouldSkipHistoryAnimations, currentTransitionKey, isChannel, + isUserFull, canSubscribe, canStartBot, canRestartBot, activeEmojiInteractions, + lastSyncTime, }) => { const { openChat, unpinAllMessages, loadUser, + loadFullUser, closeLocalTextSearch, exitMessageSelectMode, closePaymentModal, @@ -251,6 +257,12 @@ const MiddleColumn: FC = ({ } }, [chatId, isPrivate, loadUser]); + useEffect(() => { + if (isPrivate && !isUserFull && lastSyncTime) { + loadFullUser({ userId: chatId }); + } + }, [chatId, isPrivate, isUserFull, lastSyncTime, loadFullUser]); + const handleDragEnter = useCallback((e: React.DragEvent) => { if (IS_TOUCH_ENV) { return; @@ -540,7 +552,9 @@ export default memo(withGlobal( const { messageLists } = global.messages; const currentMessageList = selectCurrentMessageList(global); - const { isLeftColumnShown, chats: { listIds }, activeEmojiInteractions } = global; + const { + isLeftColumnShown, chats: { listIds }, activeEmojiInteractions, lastSyncTime, + } = global; const state: StateProps = { theme, @@ -559,6 +573,7 @@ export default memo(withGlobal( animationLevel: global.settings.byKey.animationLevel, currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1), activeEmojiInteractions, + lastSyncTime, }; if (!currentMessageList || !listIds.active) { @@ -566,8 +581,10 @@ export default memo(withGlobal( } const { chatId, threadId, type: messageListType } = currentMessageList; + const isPrivate = isUserId(chatId); const chat = selectChat(global, chatId); const bot = selectChatBot(global, chatId); + const user = isPrivate ? selectUser(global, chatId) : undefined; const pinnedIds = selectPinnedIds(global, chatId); const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer; @@ -588,7 +605,8 @@ export default memo(withGlobal( chatId, threadId, messageListType, - isPrivate: isUserId(chatId), + isPrivate, + isUserFull: Boolean(user?.settings), canPost: !isPinnedMessageList && (!chat || canPost) && !isBotNotStarted, isPinnedMessageList, isScheduledMessageList, diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 7e6974c5b..56145fe97 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -19,7 +19,7 @@ import { } from '../../config'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../util/environment'; import { - getChatTitle, getMessageKey, getSenderTitle, isUserId, + getChatTitle, getMessageKey, getPrivateChatUserId, getSenderTitle, isUserId, } from '../../modules/helpers'; import { selectAllowedMessageActions, @@ -35,6 +35,7 @@ import { selectScheduledIds, selectThreadInfo, selectThreadTopMessageId, + selectUser, } from '../../modules/selectors'; import useEnsureMessage from '../../hooks/useEnsureMessage'; import useWindowSize from '../../hooks/useWindowSize'; @@ -53,6 +54,7 @@ import HeaderActions from './HeaderActions'; import HeaderPinnedMessage from './HeaderPinnedMessage'; import AudioPlayer from './AudioPlayer'; import GroupCallTopPane from '../calls/group/GroupCallTopPane'; +import UserReportPanel from './UserReportPanel'; import './MiddleHeader.scss'; @@ -81,6 +83,7 @@ type StateProps = { isChatWithSelf?: boolean; isChatWithBot?: boolean; lastSyncTime?: number; + shouldShowUserReportPanel?: boolean; shouldSkipHistoryAnimations?: boolean; currentTransitionKey: number; connectionState?: GlobalState['connectionState']; @@ -106,6 +109,7 @@ const MiddleHeader: FC = ({ isChatWithSelf, isChatWithBot, lastSyncTime, + shouldShowUserReportPanel, shouldSkipHistoryAnimations, currentTransitionKey, connectionState, @@ -395,6 +399,9 @@ const MiddleHeader: FC = ({ onAllPinnedClick={handleAllPinnedClick} /> )} + + {shouldShowUserReportPanel && } +
{isAudioPlayerRendered && ( ( (global, { chatId, threadId, messageListType }): StateProps => { const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global; const chat = selectChat(global, chatId); + const userId = chat && getPrivateChatUserId(chat); + const user = userId ? selectUser(global, userId) : undefined; const { typingStatus } = chat || {}; @@ -446,6 +455,7 @@ export default memo(withGlobal( audioMessage, chat, messagesCount, + shouldShowUserReportPanel: Boolean(user?.settings?.canAddContact || user?.settings?.canBlockContact), isChatWithSelf: selectIsChatWithSelf(global, chatId), isChatWithBot: chat && selectIsChatWithBot(global, chat), lastSyncTime, diff --git a/src/components/middle/UserReportPanel.scss b/src/components/middle/UserReportPanel.scss new file mode 100644 index 000000000..24d775a98 --- /dev/null +++ b/src/components/middle/UserReportPanel.scss @@ -0,0 +1,36 @@ +.UserReportPanel { + position: absolute; + left: 0; + right: 0; + top: 100%; + + display: flex; + align-items: center; + margin-left: auto; + background: var(--color-background); + padding: 0.375rem 0.8125rem 0.25rem 1rem; + box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow), inset 0 0.125rem 0.125rem var(--color-light-shadow); + transform: translate3d(0, 0, 0); + transition: opacity 0.15s ease, transform var(--layer-transition); + + body.animation-level-1 & { + .ripple-container { + display: none; + } + } + + @media (min-width: 1276px) { + transform: translate3d(0, 0, 0); + transition: opacity 0.15s ease, transform var(--layer-transition); + + #Main.right-column-open & { + padding-right: calc(var(--right-column-width) + 1rem); + } + } + + .UserReportPanel--Button { + margin-left: 0.25rem; + flex: 1 1 50%; + white-space: nowrap; + } +} diff --git a/src/components/middle/UserReportPanel.tsx b/src/components/middle/UserReportPanel.tsx new file mode 100644 index 000000000..d9f060390 --- /dev/null +++ b/src/components/middle/UserReportPanel.tsx @@ -0,0 +1,124 @@ +import React, { FC, memo, useCallback, useState } from '../../lib/teact/teact'; +import { withGlobal, getDispatch } from '../../lib/teact/teactn'; + +import { ApiUser } from '../../api/types'; + +import { selectUser } from '../../modules/selectors'; +import { getUserFirstOrLastName, getUserFullName } from '../../modules/helpers'; +import useLang from '../../hooks/useLang'; +import useFlag from '../../hooks/useFlag'; + +import Button from '../ui/Button'; +import ConfirmDialog from '../ui/ConfirmDialog'; +import Checkbox from '../ui/Checkbox'; + +import './UserReportPanel.scss'; + +type OwnProps = { + userId: string; +}; + +type StateProps = { + user?: ApiUser; +}; + +const UserReportPanel: FC = ({ userId, user }) => { + const { + addContact, + blockContact, + reportSpam, + deleteChat, + toggleChatArchived, + } = getDispatch(); + + const lang = useLang(); + const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag(); + const [shouldReportSpam, setShouldReportSpam] = useState(true); + const [shouldDeleteChat, setShouldDeleteChat] = useState(true); + const { settings, accessHash } = user || {}; + const { + isAutoArchived, canReportSpam, canAddContact, canBlockContact, + } = settings || {}; + const handleAddContact = useCallback(() => { + addContact({ userId }); + if (isAutoArchived) { + toggleChatArchived({ chatId: userId }); + } + }, [addContact, isAutoArchived, toggleChatArchived, userId]); + + const handleConfirmBlock = useCallback(() => { + closeBlockUserModal(); + blockContact({ contactId: userId, accessHash }); + if (canReportSpam && shouldReportSpam) { + reportSpam({ userId }); + } + if (shouldDeleteChat) { + deleteChat({ chatId: userId }); + } + }, [ + accessHash, blockContact, closeBlockUserModal, deleteChat, reportSpam, canReportSpam, shouldDeleteChat, + shouldReportSpam, userId, + ]); + + if (!settings) { + return; + } + + + return ( +
+ {canAddContact && ( + + )} + {canBlockContact && ( + + )} + + {canReportSpam && ( + + )} + + +
+ ); +}; + +export default memo(withGlobal( + (global, { userId }): StateProps => ({ user: selectUser(global, userId) }), +)(UserReportPanel)); diff --git a/src/global/types.ts b/src/global/types.ts index 1a6fa88af..ef2475d07 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -561,7 +561,7 @@ export type ActionTypes = ( // users 'loadFullUser' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' | 'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' | - 'deleteContact' | 'loadUser' | 'setUserSearchQuery' | 'loadCommonChats' | + 'deleteContact' | 'loadUser' | 'setUserSearchQuery' | 'loadCommonChats' | 'reportSpam' | // chat creation 'createChannel' | 'createGroupChat' | 'resetChatCreation' | // settings diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index d20e4193a..2517a973b 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1021,6 +1021,7 @@ messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action messages.sendMessage#d9d75a4 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.sendMedia#e25ff8e0 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.forwardMessages#cc30290b flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; messages.getChats#49e9528f id:Vector = messages.Chats; messages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 915097b4d..a4e0918b4 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -67,6 +67,7 @@ "messages.sendMessage", "messages.sendMedia", "messages.forwardMessages", + "messages.reportSpam", "messages.report", "messages.getChats", "messages.getFullChat", diff --git a/src/modules/actions/api/users.ts b/src/modules/actions/api/users.ts index eabd09e4a..0504f0adf 100644 --- a/src/modules/actions/api/users.ts +++ b/src/modules/actions/api/users.ts @@ -271,6 +271,16 @@ addReducer('addContact', (global, actions, payload) => { void callApi('addContact', pick(user, ['id', 'accessHash', 'firstName', 'lastName', 'phoneNumber'])); }); +addReducer('reportSpam', (global, actions, payload) => { + const { userId } = payload!; + const user = selectUser(global, userId); + if (!user) { + return; + } + + void callApi('reportSpam', user); +}); + async function searchUsers(query: string) { const result = await callApi('searchChats', { query });