Middle Header: Introduce Add Contact and Block User buttons (#1735)

This commit is contained in:
Alexander Zinchuk 2022-03-04 16:20:23 +03:00
parent 531c2de36c
commit a215aa1083
13 changed files with 246 additions and 10 deletions

View File

@ -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),
};
}

View File

@ -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 {

View File

@ -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);

View File

@ -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) }),
},
});
});
}

View File

@ -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 {

View File

@ -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<StateProps> = ({
shouldSkipHistoryAnimations,
currentTransitionKey,
isChannel,
isUserFull,
canSubscribe,
canStartBot,
canRestartBot,
activeEmojiInteractions,
lastSyncTime,
}) => {
const {
openChat,
unpinAllMessages,
loadUser,
loadFullUser,
closeLocalTextSearch,
exitMessageSelectMode,
closePaymentModal,
@ -251,6 +257,12 @@ const MiddleColumn: FC<StateProps> = ({
}
}, [chatId, isPrivate, loadUser]);
useEffect(() => {
if (isPrivate && !isUserFull && lastSyncTime) {
loadFullUser({ userId: chatId });
}
}, [chatId, isPrivate, isUserFull, lastSyncTime, loadFullUser]);
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
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,

View File

@ -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<OwnProps & StateProps> = ({
isChatWithSelf,
isChatWithBot,
lastSyncTime,
shouldShowUserReportPanel,
shouldSkipHistoryAnimations,
currentTransitionKey,
connectionState,
@ -395,6 +399,9 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
onAllPinnedClick={handleAllPinnedClick}
/>
)}
{shouldShowUserReportPanel && <UserReportPanel key={chatId} userId={chatId} />}
<div className="header-tools">
{isAudioPlayerRendered && (
<AudioPlayer
@ -418,6 +425,8 @@ export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
audioMessage,
chat,
messagesCount,
shouldShowUserReportPanel: Boolean(user?.settings?.canAddContact || user?.settings?.canBlockContact),
isChatWithSelf: selectIsChatWithSelf(global, chatId),
isChatWithBot: chat && selectIsChatWithBot(global, chat),
lastSyncTime,

View File

@ -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;
}
}

View File

@ -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<OwnProps & StateProps> = ({ userId, user }) => {
const {
addContact,
blockContact,
reportSpam,
deleteChat,
toggleChatArchived,
} = getDispatch();
const lang = useLang();
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
const [shouldReportSpam, setShouldReportSpam] = useState<boolean>(true);
const [shouldDeleteChat, setShouldDeleteChat] = useState<boolean>(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 (
<div className="UserReportPanel">
{canAddContact && (
<Button
isText
ripple
fluid
size="tiny"
className="UserReportPanel--Button"
onClick={handleAddContact}
>
{lang('lng_new_contact_add')}
</Button>
)}
{canBlockContact && (
<Button
color="danger"
isText
ripple
fluid
size="tiny"
className="UserReportPanel--Button"
onClick={openBlockUserModal}
>
{lang('lng_new_contact_block')}
</Button>
)}
<ConfirmDialog
isOpen={isBlockUserModalOpen}
onClose={closeBlockUserModal}
title={lang('BlockUserTitle', getUserFirstOrLastName(user))}
text={lang('UserInfo.BlockConfirmationTitle', getUserFullName(user))}
isButtonsInOneRow
confirmIsDestructive
confirmLabel={lang('Block')}
confirmHandler={handleConfirmBlock}
>
{canReportSpam && (
<Checkbox
label={lang('DeleteReportSpam')}
checked={shouldReportSpam}
onCheck={setShouldReportSpam}
/>
)}
<Checkbox
label={lang('DeleteThisChat')}
checked={shouldDeleteChat}
onCheck={setShouldDeleteChat}
/>
</ConfirmDialog>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { userId }): StateProps => ({ user: selectUser(global, userId) }),
)(UserReportPanel));

View File

@ -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

View File

@ -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<MessageEntity> 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<MessageEntity> 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<int> random_id:Vector<long> 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<int> reason:ReportReason message:string = Bool;
messages.getChats#49e9528f id:Vector<long> = messages.Chats;
messages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull;

View File

@ -67,6 +67,7 @@
"messages.sendMessage",
"messages.sendMedia",
"messages.forwardMessages",
"messages.reportSpam",
"messages.report",
"messages.getChats",
"messages.getFullChat",

View File

@ -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 });