Message Reports: Implement reporting for messages and stories (#5052)

This commit is contained in:
Alexander Zinchuk 2024-11-02 21:11:57 +04:00
parent f7e553734b
commit 01dc204fa7
29 changed files with 796 additions and 214 deletions

View File

@ -16,6 +16,7 @@ import type {
ApiMessageActionStarGift, ApiMessageActionStarGift,
ApiMessageEntity, ApiMessageEntity,
ApiMessageForwardInfo, ApiMessageForwardInfo,
ApiMessageReportResult,
ApiNewPoll, ApiNewPoll,
ApiPeer, ApiPeer,
ApiPhoto, ApiPhoto,
@ -1246,3 +1247,33 @@ export function buildApiQuickReply(reply: GramJs.TypeQuickReply): ApiQuickReply
topMessageId: topMessage, topMessageId: topMessage,
}; };
} }
export function buildApiReportResult(
result: GramJs.TypeReportResult,
): ApiMessageReportResult {
if (result instanceof GramJs.ReportResultReported) {
return {
type: 'reported',
};
}
if (result instanceof GramJs.ReportResultAddComment) {
return {
type: 'comment',
isOptional: result.optional,
option: serializeBytes(result.option),
};
}
const title = result.title;
const options = result.options.map((option) => ({
text: option.text,
option: serializeBytes(option.option),
}));
return {
type: 'selectOption',
title,
options,
};
}

View File

@ -8,6 +8,7 @@ import type {
ApiChat, ApiChat,
ApiClickSponsoredMessage, ApiClickSponsoredMessage,
ApiContact, ApiContact,
ApiError,
ApiFormattedText, ApiFormattedText,
ApiGlobalMessageSearchType, ApiGlobalMessageSearchType,
ApiInputReplyInfo, ApiInputReplyInfo,
@ -19,7 +20,6 @@ import type {
ApiPeer, ApiPeer,
ApiPoll, ApiPoll,
ApiReaction, ApiReaction,
ApiReportReason,
ApiSendMessageAction, ApiSendMessageAction,
ApiSticker, ApiSticker,
ApiStory, ApiStory,
@ -36,6 +36,7 @@ import {
GIF_MIME_TYPE, GIF_MIME_TYPE,
MAX_INT_32, MAX_INT_32,
MENTION_UNREAD_SLICE, MENTION_UNREAD_SLICE,
MESSAGE_ID_REQUIRED_ERROR,
PINNED_MESSAGES_LIMIT, PINNED_MESSAGES_LIMIT,
REACTION_UNREAD_SLICE, REACTION_UNREAD_SLICE,
SUPPORTED_PHOTO_CONTENT_TYPES, SUPPORTED_PHOTO_CONTENT_TYPES,
@ -47,13 +48,17 @@ import { compact, split } from '../../../util/iteratees';
import { getMessageKey } from '../../../util/keys/messageKey'; import { getMessageKey } from '../../../util/keys/messageKey';
import { getServerTimeOffset } from '../../../util/serverTime'; import { getServerTimeOffset } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform'; import { interpolateArray } from '../../../util/waveform';
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats'; import {
buildApiChatFromPreview,
buildApiSendAsPeerId,
} from '../apiBuilders/chats';
import { buildApiFormattedText } from '../apiBuilders/common'; import { buildApiFormattedText } from '../apiBuilders/common';
import { buildMessageMediaContent, buildMessageTextContent, buildWebPage } from '../apiBuilders/messageContent'; import { buildMessageMediaContent, buildMessageTextContent, buildWebPage } from '../apiBuilders/messageContent';
import { import {
buildApiFactCheck, buildApiFactCheck,
buildApiMessage, buildApiMessage,
buildApiQuickReply, buildApiQuickReply,
buildApiReportResult,
buildApiSponsoredMessage, buildApiSponsoredMessage,
buildApiThreadInfo, buildApiThreadInfo,
buildLocalForwardedMessage, buildLocalForwardedMessage,
@ -901,18 +906,45 @@ export async function deleteSavedHistory({
} }
export async function reportMessages({ export async function reportMessages({
peer, messageIds, description, peer, messageIds, description, option,
}: { }: {
peer: ApiPeer; messageIds: number[]; reason: ApiReportReason; description?: string; peer: ApiPeer; messageIds: number[]; description: string; option: string;
}) { }) {
const result = await invokeRequest(new GramJs.messages.Report({ try {
peer: buildInputPeer(peer.id, peer.accessHash), const result = await invokeRequest(new GramJs.messages.Report({
id: messageIds, peer: buildInputPeer(peer.id, peer.accessHash),
option: Buffer.alloc(0), id: messageIds,
message: description, option: deserializeBytes(option),
})); message: description,
}), { shouldThrow: true });
return result; if (!result) return undefined;
return { result: buildApiReportResult(result), error: undefined };
} catch (err: any) {
const errorMessage = (err as ApiError).message;
if (errorMessage === MESSAGE_ID_REQUIRED_ERROR) {
return {
result: undefined,
error: errorMessage,
};
}
throw err;
}
}
export function reportChannelSpam({
peer, chat, messageIds,
}: {
peer: ApiPeer; chat: ApiChat; messageIds: number[];
}) {
return invokeRequest(new GramJs.channels.ReportSpam({
participant: buildInputPeer(peer.id, peer.accessHash),
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
id: messageIds,
}));
} }
export async function sendMessageAction({ export async function sendMessageAction({

View File

@ -2,16 +2,17 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiInputPrivacyRules } from '../../../types'; import type { ApiInputPrivacyRules } from '../../../types';
import type { import type {
ApiError,
ApiPeer, ApiPeer,
ApiPeerStories, ApiPeerStories,
ApiReaction, ApiReaction,
ApiReportReason,
ApiStealthMode, ApiStealthMode,
ApiTypeStory, ApiTypeStory,
} from '../../types'; } from '../../types';
import { STORY_LIST_LIMIT } from '../../../config'; import { MESSAGE_ID_REQUIRED_ERROR, STORY_LIST_LIMIT } from '../../../config';
import { buildCollectionByCallback } from '../../../util/iteratees'; import { buildCollectionByCallback } from '../../../util/iteratees';
import { buildApiReportResult } from '../apiBuilders/messages';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { import {
buildApiPeerStories, buildApiPeerStories,
@ -25,7 +26,7 @@ import {
buildInputPrivacyRules, buildInputPrivacyRules,
buildInputReaction, buildInputReaction,
} from '../gramjsBuilders'; } from '../gramjsBuilders';
import { addStoryToLocalDb } from '../helpers'; import { addStoryToLocalDb, deserializeBytes } from '../helpers';
import { invokeRequest } from './client'; import { invokeRequest } from './client';
export async function fetchAllStories({ export async function fetchAllStories({
@ -328,19 +329,37 @@ export async function fetchStoryLink({ peer, storyId }: { peer: ApiPeer ; storyI
return result.link; return result.link;
} }
export function reportStory({ export async function reportStory({
peer, peer,
storyId, storyId,
description, description,
option,
}: { }: {
peer: ApiPeer; storyId: number; reason: ApiReportReason; description?: string; peer: ApiPeer; storyId: number; description: string; option: string;
}) { }) {
return invokeRequest(new GramJs.stories.Report({ try {
peer: buildInputPeer(peer.id, peer.accessHash), const result = await invokeRequest(new GramJs.stories.Report({
id: [storyId], peer: buildInputPeer(peer.id, peer.accessHash),
option: Buffer.alloc(0), id: [storyId],
message: description, option: deserializeBytes(option),
})); message: description,
}), { shouldThrow: true });
if (!result) return undefined;
return { result: buildApiReportResult(result), error: undefined };
} catch (err: any) {
const errorMessage = (err as ApiError).message;
if (errorMessage === MESSAGE_ID_REQUIRED_ERROR) {
return {
result: undefined,
error: errorMessage,
};
}
throw err;
}
} }
export function editStoryPrivacy({ export function editStoryPrivacy({

View File

@ -871,6 +871,21 @@ export interface ApiMessageThreadInfo extends ApiBaseThreadInfo {
export type ApiThreadInfo = ApiCommentsInfo | ApiMessageThreadInfo; export type ApiThreadInfo = ApiCommentsInfo | ApiMessageThreadInfo;
export type ApiMessageReportResult = {
type: 'reported';
} | {
type: 'comment';
isOptional?: boolean;
option: string;
} | {
type: 'selectOption';
title: string;
options: {
text: string;
option: string;
}[];
};
export type ApiMessageOutgoingStatus = 'read' | 'succeeded' | 'pending' | 'failed'; export type ApiMessageOutgoingStatus = 'read' | 'succeeded' | 'pending' | 'failed';
export type ApiSponsoredMessage = { export type ApiSponsoredMessage = {

BIN
src/assets/tgs/Report.tgs Normal file

Binary file not shown.

View File

@ -29,6 +29,7 @@ export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal'; export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
export { default as VerificationMonetizationModal } from '../components/common/VerificationMonetizationModal'; export { default as VerificationMonetizationModal } from '../components/common/VerificationMonetizationModal';
export { default as ReportAdModal } from '../components/modals/reportAd/ReportAdModal'; export { default as ReportAdModal } from '../components/modals/reportAd/ReportAdModal';
export { default as ReportModal } from '../components/modals/reportModal/ReportModal';
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';
export { default as PinMessageModal } from '../components/common/PinMessageModal'; export { default as PinMessageModal } from '../components/common/PinMessageModal';

View File

@ -96,7 +96,7 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
const { const {
deleteMessages, deleteMessages,
deleteScheduledMessages, deleteScheduledMessages,
reportMessages, reportChannelSpam,
deleteChatMember, deleteChatMember,
updateChatMemberBannedRights, updateChatMemberBannedRights,
closeDeleteMessageModal, closeDeleteMessageModal,
@ -231,10 +231,10 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
if (isSchedule) { if (isSchedule) {
deleteScheduledMessages({ messageIds }); deleteScheduledMessages({ messageIds });
} else if (!isOwn && (chosenSpanOption || chosenDeleteOption || chosenBanOption) && (isGroup || isSuperGroup)) { } else if (!isOwn && (chosenSpanOption || chosenDeleteOption || chosenBanOption) && (isGroup || isSuperGroup)) {
if (chosenSpanOption) { if (chosenSpanOption && sender) {
const filteredMessageIdList = filterMessageIdByUserId(chosenSpanOption, messageIdList!); const filteredMessageIdList = filterMessageIdByUserId(chosenSpanOption, messageIdList!);
if (filteredMessageIdList && filteredMessageIdList.length) { if (filteredMessageIdList && filteredMessageIdList.length) {
reportMessages({ messageIds: filteredMessageIdList, reason: 'spam', description: '' }); reportChannelSpam({ participantId: sender.id, chatId: chat.id, messageIds: filteredMessageIdList });
} }
} }

View File

@ -5,8 +5,6 @@ import { getActions } from '../../global';
import type { ApiPhoto, ApiReportReason } from '../../api/types'; import type { ApiPhoto, ApiReportReason } from '../../api/types';
import buildClassName from '../../util/buildClassName';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
@ -17,55 +15,28 @@ import RadioGroup from '../ui/RadioGroup';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
subject?: 'peer' | 'messages' | 'media' | 'story';
peerId?: string; peerId?: string;
photo?: ApiPhoto; photo?: ApiPhoto;
messageIds?: number[];
storyId?: number;
onClose: () => void; onClose: () => void;
onCloseAnimationEnd?: () => void; onCloseAnimationEnd?: () => void;
}; };
const ReportModal: FC<OwnProps> = ({ const ReportAvatarModal: FC<OwnProps> = ({
isOpen, isOpen,
subject = 'messages',
peerId, peerId,
photo, photo,
messageIds,
storyId,
onClose, onClose,
onCloseAnimationEnd, onCloseAnimationEnd,
}) => { }) => {
const { const { reportProfilePhoto } = getActions();
reportMessages,
reportPeer,
reportProfilePhoto,
reportStory,
exitMessageSelectMode,
} = getActions();
const [selectedReason, setSelectedReason] = useState<ApiReportReason>('spam'); const [selectedReason, setSelectedReason] = useState<ApiReportReason>('spam');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const handleReport = useLastCallback(() => { const handleReport = useLastCallback(() => {
switch (subject) { reportProfilePhoto({
case 'messages': chatId: peerId, photo, reason: selectedReason, description,
reportMessages({ messageIds: messageIds!, reason: selectedReason, description }); });
exitMessageSelectMode();
break;
case 'peer':
reportPeer({ chatId: peerId, reason: selectedReason, description });
break;
case 'media':
reportProfilePhoto({
chatId: peerId, photo, reason: selectedReason, description,
});
break;
case 'story':
reportStory({
peerId: peerId!, storyId: storyId!, reason: selectedReason, description,
});
}
onClose(); onClose();
}); });
@ -90,18 +61,11 @@ const ReportModal: FC<OwnProps> = ({
{ value: 'other', label: lang('lng_report_reason_other') }, { value: 'other', label: lang('lng_report_reason_other') },
], [lang]); ], [lang]);
if ( if (!peerId || !photo) {
(subject === 'messages' && !messageIds)
|| (subject === 'peer' && !peerId)
|| (subject === 'media' && (!peerId || !photo))
|| (subject === 'story' && (!storyId || !peerId))
) {
return undefined; return undefined;
} }
const title = subject === 'messages' const title = lang('ReportPeer.Report');
? lang('lng_report_message_title')
: lang('ReportPeer.Report');
return ( return (
<Modal <Modal
@ -109,7 +73,7 @@ const ReportModal: FC<OwnProps> = ({
onClose={onClose} onClose={onClose}
onEnter={isOpen ? handleReport : undefined} onEnter={isOpen ? handleReport : undefined}
onCloseAnimationEnd={onCloseAnimationEnd} onCloseAnimationEnd={onCloseAnimationEnd}
className={buildClassName('narrow', subject === 'story' && 'component-theme-dark')} className="narrow"
title={title} title={title}
> >
<RadioGroup <RadioGroup
@ -133,4 +97,4 @@ const ReportModal: FC<OwnProps> = ({
); );
}; };
export default memo(ReportModal); export default memo(ReportAvatarModal);

View File

@ -19,6 +19,7 @@ import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs
import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs'; import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs';
import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs'; import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs';
import ReadTime from '../../../assets/tgs/ReadTime.tgs'; import ReadTime from '../../../assets/tgs/ReadTime.tgs';
import Report from '../../../assets/tgs/Report.tgs';
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs'; import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs'; import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
import Experimental from '../../../assets/tgs/settings/Experimental.tgs'; import Experimental from '../../../assets/tgs/settings/Experimental.tgs';
@ -62,4 +63,5 @@ export const LOCAL_TGS_URLS = {
Fragment, Fragment,
StarReactionEffect, StarReactionEffect,
StarReaction, StarReaction,
Report,
}; };

View File

@ -69,7 +69,6 @@ import DeleteChatModal from '../../common/DeleteChatModal';
import FullNameTitle from '../../common/FullNameTitle'; import FullNameTitle from '../../common/FullNameTitle';
import StarIcon from '../../common/icons/StarIcon'; import StarIcon from '../../common/icons/StarIcon';
import LastMessageMeta from '../../common/LastMessageMeta'; import LastMessageMeta from '../../common/LastMessageMeta';
import ReportModal from '../../common/ReportModal';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import ChatFolderModal from '../ChatFolderModal.async'; import ChatFolderModal from '../ChatFolderModal.async';
import MuteChatModal from '../MuteChatModal.async'; import MuteChatModal from '../MuteChatModal.async';
@ -170,17 +169,16 @@ const Chat: FC<OwnProps & StateProps> = ({
openForumPanel, openForumPanel,
closeForumPanel, closeForumPanel,
setShouldCloseRightColumn, setShouldCloseRightColumn,
reportMessages,
} = getActions(); } = getActions();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag(); const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag();
const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag(); const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag();
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag(); const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag();
const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag();
const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag(); const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag();
const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag();
const { isForum, isForumAsMessages } = chat || {}; const { isForum, isForumAsMessages } = chat || {};
@ -275,8 +273,8 @@ const Chat: FC<OwnProps & StateProps> = ({
}); });
const handleReport = useLastCallback(() => { const handleReport = useLastCallback(() => {
markRenderReportModal(); if (!chat) return;
openReportModal(); reportMessages({ chatId: chat.id, messageIds: [] });
}); });
const contextActions = useChatContextActions({ const contextActions = useChatContextActions({
@ -432,15 +430,6 @@ const Chat: FC<OwnProps & StateProps> = ({
chatId={chatId} chatId={chatId}
/> />
)} )}
{shouldRenderReportModal && (
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
onCloseAnimationEnd={unmarkRenderReportModal}
peerId={chatId}
subject="peer"
/>
)}
</ListItem> </ListItem>
); );
}; };

View File

@ -53,7 +53,7 @@ import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck'; import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck';
import { useMediaProps } from './hooks/useMediaProps'; import { useMediaProps } from './hooks/useMediaProps';
import ReportModal from '../common/ReportModal'; import ReportAvatarModal from '../common/ReportAvatarModal';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ShowTransition from '../ui/ShowTransition'; import ShowTransition from '../ui/ShowTransition';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
@ -446,10 +446,9 @@ const MediaViewer = ({
onCloseMediaViewer={handleClose} onCloseMediaViewer={handleClose}
onForward={handleForward} onForward={handleForward}
/> />
<ReportModal <ReportAvatarModal
isOpen={isReportAvatarModalOpen} isOpen={isReportAvatarModalOpen}
onClose={closeReportAvatarModal} onClose={closeReportAvatarModal}
subject="media"
photo={avatar} photo={avatar}
peerId={avatarOwner?.id} peerId={avatarOwner?.id}
/> />

View File

@ -94,7 +94,7 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
}) => { }) => {
const { const {
deleteMessages, deleteMessages,
reportMessages, reportChannelSpam,
deleteChatMember, deleteChatMember,
deleteScheduledMessages, deleteScheduledMessages,
exitMessageSelectMode, exitMessageSelectMode,
@ -227,6 +227,18 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
}); });
}); });
const handleReportSpam = useLastCallback((userMessagesMap: Record<string, number[]>) => {
Object.entries(userMessagesMap).forEach(([userId, messageIdList]) => {
if (messageIdList.length) {
reportChannelSpam({
participantId: userId,
chatId: chat!.id,
messageIds: messageIdList,
});
}
});
});
const handleDeleteMessages = useLastCallback((filteredMessageIdList: number[]) => { const handleDeleteMessages = useLastCallback((filteredMessageIdList: number[]) => {
if (filteredMessageIdList && filteredMessageIdList.length) { if (filteredMessageIdList && filteredMessageIdList.length) {
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true }); deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
@ -257,10 +269,18 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
} else if (!isSenderOwner && shouldShowOptions) { } else if (!isSenderOwner && shouldShowOptions) {
if (chosenSpanOption) { if (chosenSpanOption) {
const userIdList = chosenSpanOption.filter((option) => !Number.isNaN(Number(option))); const userIdList = chosenSpanOption.filter((option) => !Number.isNaN(Number(option)));
const filteredMessageIdList = filterMessageIdByUserId(userIdList, selectedMessageIds!); const userMessagesMap = selectedMessageIds!.reduce<Record<string, number[]>>((acc, msgId) => {
if (filteredMessageIdList?.length) { const sender = selectSenderFromMessage(getGlobal(), chat.id, msgId);
reportMessages({ messageIds: filteredMessageIdList, reason: 'spam', description: '' }); if (sender && userIdList.includes(sender.id)) {
} if (!acc[sender.id]) {
acc[sender.id] = [];
}
acc[sender.id].push(Number(msgId));
}
return acc;
}, {});
handleReportSpam(userMessagesMap);
} }
if (chosenDeleteOption) { if (chosenDeleteOption) {

View File

@ -49,7 +49,6 @@ import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
import DeleteChatModal from '../common/DeleteChatModal'; import DeleteChatModal from '../common/DeleteChatModal';
import ReportModal from '../common/ReportModal';
import MuteChatModal from '../left/MuteChatModal.async'; import MuteChatModal from '../left/MuteChatModal.async';
import Menu from '../ui/Menu'; import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
@ -109,8 +108,8 @@ type StateProps = {
isForum?: boolean; isForum?: boolean;
isForumAsMessages?: true; isForumAsMessages?: true;
canAddContact?: boolean; canAddContact?: boolean;
canReportChat?: boolean;
canDeleteChat?: boolean; canDeleteChat?: boolean;
canReportChat?: boolean;
canGift?: boolean; canGift?: boolean;
canCreateTopic?: boolean; canCreateTopic?: boolean;
canEditTopic?: boolean; canEditTopic?: boolean;
@ -143,6 +142,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
isChatInfoShown, isChatInfoShown,
canStartBot, canStartBot,
canSubscribe, canSubscribe,
canReportChat,
canSearch, canSearch,
canCall, canCall,
canMute, canMute,
@ -156,7 +156,6 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
chat, chat,
isPrivate, isPrivate,
isMuted, isMuted,
canReportChat,
canDeleteChat, canDeleteChat,
canGift, canGift,
hasLinkedChat, hasLinkedChat,
@ -203,13 +202,13 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
unblockUser, unblockUser,
setViewForumAsMessages, setViewForumAsMessages,
openBoostModal, openBoostModal,
reportMessages,
} = getActions(); } = getActions();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const [isMenuOpen, setIsMenuOpen] = useState(true); const [isMenuOpen, setIsMenuOpen] = useState(true);
const [shouldCloseFast, setShouldCloseFast] = useState(false); const [shouldCloseFast, setShouldCloseFast] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const [isMuteModalOpen, setIsMuteModalOpen] = useState(false); const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag();
const { x, y } = anchor; const { x, y } = anchor;
@ -219,18 +218,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
(!isChatInfoShown && isForum) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION, (!isChatInfoShown && isForum) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION,
); );
const handleReport = useLastCallback(() => { const closeMuteModal = useLastCallback(() => {
setIsMenuOpen(false); setIsMuteModalOpen(false);
setIsReportModalOpen(true);
});
const closeReportModal = useLastCallback(() => {
setIsReportModalOpen(false);
onClose(); onClose();
}); });
const closeMuteModal = useLastCallback(() => { const handleReport = useLastCallback(() => {
setIsMuteModalOpen(false); setIsMenuOpen(false);
reportMessages({ chatId, messageIds: [] });
onClose(); onClose();
}); });
@ -725,14 +720,6 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
chatId={chat.id} chatId={chat.id}
/> />
)} )}
{canReportChat && chat?.id && (
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
subject="peer"
peerId={chat.id}
/>
)}
</div> </div>
</Portal> </Portal>
); );
@ -749,8 +736,8 @@ export default memo(withGlobal<OwnProps>(
const canAddContact = user && getCanAddContact(user); const canAddContact = user && getCanAddContact(user);
const isMainThread = threadId === MAIN_THREAD_ID; const isMainThread = threadId === MAIN_THREAD_ID;
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const canReportChat = isMainThread && (isChatChannel(chat) || isChatGroup(chat) || (user && !user.isSelf));
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {}; const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
const canReportChat = isMainThread && !user && (isChatChannel(chat) || isChatGroup(chat));
const chatBot = !isSystemBot(chatId) ? selectBot(global, chatId) : undefined; const chatBot = !isSystemBot(chatId) ? selectBot(global, chatId) : undefined;
const userFullInfo = isPrivate ? selectUserFullInfo(global, chatId) : undefined; const userFullInfo = isPrivate ? selectUserFullInfo(global, chatId) : undefined;
@ -778,8 +765,8 @@ export default memo(withGlobal<OwnProps>(
isForum: chat?.isForum, isForum: chat?.isForum,
isForumAsMessages: chat?.isForumAsMessages, isForumAsMessages: chat?.isForumAsMessages,
canAddContact, canAddContact,
canReportChat,
canDeleteChat: getCanDeleteChat(chat), canDeleteChat: getCanDeleteChat(chat),
canReportChat,
canGift, canGift,
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId), hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
botCommands: chatBot ? userFullInfo?.botInfo?.commands : undefined, botCommands: chatBot ? userFullInfo?.botInfo?.commands : undefined,

View File

@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useEffect } from '../../lib/teact/teact'; import React, { memo, useEffect } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiChat } from '../../api/types';
import type { MessageListType } from '../../global/types'; import type { MessageListType } from '../../global/types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
@ -9,7 +10,7 @@ import {
selectCanDeleteSelectedMessages, selectCanDeleteSelectedMessages,
selectCanDownloadSelectedMessages, selectCanDownloadSelectedMessages,
selectCanForwardMessages, selectCanForwardMessages,
selectCanReportSelectedMessages, selectCanReportSelectedMessages, selectCurrentChat,
selectCurrentMessageList, selectHasProtectedMessage, selectCurrentMessageList, selectHasProtectedMessage,
selectSelectedMessagesCount, selectSelectedMessagesCount,
selectTabState, selectTabState,
@ -23,7 +24,6 @@ import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useCopySelectedMessages from './hooks/useCopySelectedMessages'; import useCopySelectedMessages from './hooks/useCopySelectedMessages';
import ReportModal from '../common/ReportModal';
import Button from '../ui/Button'; import Button from '../ui/Button';
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal'; import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
@ -36,6 +36,7 @@ export type OwnProps = {
}; };
type StateProps = { type StateProps = {
chat?: ApiChat;
isSchedule: boolean; isSchedule: boolean;
selectedMessagesCount?: number; selectedMessagesCount?: number;
canDeleteMessages?: boolean; canDeleteMessages?: boolean;
@ -48,6 +49,7 @@ type StateProps = {
}; };
const MessageSelectToolbar: FC<OwnProps & StateProps> = ({ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
chat,
canPost, canPost,
isActive, isActive,
messageListType, messageListType,
@ -67,11 +69,11 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
downloadSelectedMessages, downloadSelectedMessages,
copySelectedMessages, copySelectedMessages,
showNotification, showNotification,
reportMessages,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
useCopySelectedMessages(isActive); useCopySelectedMessages(isActive);
@ -80,7 +82,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
}); });
useEffect(() => { useEffect(() => {
return isActive && !isDeleteModalOpen && !isReportModalOpen && !isAnyModalOpen return isActive && !isDeleteModalOpen && !isAnyModalOpen
? captureKeyboardListeners({ ? captureKeyboardListeners({
onBackspace: canDeleteMessages ? openDeleteModal : undefined, onBackspace: canDeleteMessages ? openDeleteModal : undefined,
onDelete: canDeleteMessages ? openDeleteModal : undefined, onDelete: canDeleteMessages ? openDeleteModal : undefined,
@ -88,7 +90,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
}) })
: undefined; : undefined;
}, [ }, [
isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, handleExitMessageSelectMode, isAnyModalOpen, isActive, isDeleteModalOpen, openDeleteModal, handleExitMessageSelectMode, isAnyModalOpen,
canDeleteMessages, canDeleteMessages,
]); ]);
@ -110,6 +112,15 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
const formattedMessagesCount = lang('VoiceOver.Chat.MessagesSelected', renderingSelectedMessagesCount, 'i'); const formattedMessagesCount = lang('VoiceOver.Chat.MessagesSelected', renderingSelectedMessagesCount, 'i');
const openMessageReport = useLastCallback(() => {
if (!selectedMessageIds || !chat) return;
reportMessages({
chatId: chat.id,
messageIds: selectedMessageIds,
});
exitMessageSelectMode();
});
const className = buildClassName( const className = buildClassName(
'MessageSelectToolbar', 'MessageSelectToolbar',
canPost && 'with-composer', canPost && 'with-composer',
@ -160,7 +171,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
) )
)} )}
{canReportMessages && ( {canReportMessages && (
renderButton('flag', lang('Conversation.ReportMessages'), openReportModal) renderButton('flag', lang('Conversation.ReportMessages'), openMessageReport)
)} )}
{canDownloadMessages && !hasProtectedMessage && ( {canDownloadMessages && !hasProtectedMessage && (
renderButton('download', lang('lng_media_download'), handleDownload) renderButton('download', lang('lng_media_download'), handleDownload)
@ -181,11 +192,6 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
onClose={closeDeleteModal} onClose={closeDeleteModal}
/> />
)} )}
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
messageIds={selectedMessageIds}
/>
</div> </div>
); );
}; };
@ -193,6 +199,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
const tabState = selectTabState(global); const tabState = selectTabState(global);
const chat = selectCurrentChat(global);
const { type: messageListType, chatId } = selectCurrentMessageList(global) || {}; const { type: messageListType, chatId } = selectCurrentMessageList(global) || {};
const isSchedule = messageListType === 'scheduled'; const isSchedule = messageListType === 'scheduled';
const { canDelete } = selectCanDeleteSelectedMessages(global); const { canDelete } = selectCanDeleteSelectedMessages(global);
@ -203,9 +210,10 @@ export default memo(withGlobal<OwnProps>(
const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false; const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false;
const isShareMessageModalOpen = tabState.isShareMessageModalShown; const isShareMessageModalOpen = tabState.isShareMessageModalShown;
const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall); || tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal);
return { return {
chat,
isSchedule, isSchedule,
selectedMessagesCount: selectSelectedMessagesCount(global), selectedMessagesCount: selectSelectedMessagesCount(global),
canDeleteMessages: canDelete, canDeleteMessages: canDelete,

View File

@ -6,6 +6,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { import type {
ApiAvailableReaction, ApiAvailableReaction,
ApiChat,
ApiChatReactions, ApiChatReactions,
ApiMessage, ApiMessage,
ApiPoll, ApiPoll,
@ -72,7 +73,6 @@ import useSchedule from '../../../hooks/useSchedule';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import PinMessageModal from '../../common/PinMessageModal.async'; import PinMessageModal from '../../common/PinMessageModal.async';
import ReportModal from '../../common/ReportModal';
import ConfirmDialog from '../../ui/ConfirmDialog'; import ConfirmDialog from '../../ui/ConfirmDialog';
import MessageContextMenu from './MessageContextMenu'; import MessageContextMenu from './MessageContextMenu';
@ -94,6 +94,7 @@ type StateProps = {
threadId?: ThreadId; threadId?: ThreadId;
poll?: ApiPoll; poll?: ApiPoll;
story?: ApiTypeStory; story?: ApiTypeStory;
chat?: ApiChat;
availableReactions?: ApiAvailableReaction[]; availableReactions?: ApiAvailableReaction[];
topReactions?: ApiReaction[]; topReactions?: ApiReaction[];
defaultTagReactions?: ApiReaction[]; defaultTagReactions?: ApiReaction[];
@ -169,8 +170,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
repliesThreadInfo, repliesThreadInfo,
canUnpin, canUnpin,
canDelete, canDelete,
canReport,
canShowReactionsCount, canShowReactionsCount,
chat,
canReport,
canShowReactionList, canShowReactionList,
canEdit, canEdit,
enabledReactions, enabledReactions,
@ -241,6 +243,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
openDeleteMessageModal, openDeleteMessageModal,
addLocalPaidReaction, addLocalPaidReaction,
openPaidReactionModal, openPaidReactionModal,
reportMessages,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -250,7 +253,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
className: false, className: false,
}); });
const [isMenuOpen, setIsMenuOpen] = useState(true); const [isMenuOpen, setIsMenuOpen] = useState(true);
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const [isPinModalOpen, setIsPinModalOpen] = useState(false); const [isPinModalOpen, setIsPinModalOpen] = useState(false);
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag(); const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
const [canQuoteSelection, setCanQuoteSelection] = useState(false); const [canQuoteSelection, setCanQuoteSelection] = useState(false);
@ -366,16 +368,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
openDeleteMessageModal({ isSchedule: messageListType === 'scheduled', album, message }); openDeleteMessageModal({ isSchedule: messageListType === 'scheduled', album, message });
}); });
const handleReport = useLastCallback(() => {
setIsMenuOpen(false);
setIsReportModalOpen(true);
});
const closeReportModal = useLastCallback(() => {
setIsReportModalOpen(false);
onClose();
});
const closePinModal = useLastCallback(() => { const closePinModal = useLastCallback(() => {
setIsPinModalOpen(false); setIsPinModalOpen(false);
onClose(); onClose();
@ -589,6 +581,15 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]); const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
const handleReport = useLastCallback(() => {
if (!chat) return;
setIsMenuOpen(false);
onClose();
reportMessages({
chatId: chat.id, messageIds: reportMessageIds,
});
});
if (noOptions) { if (noOptions) {
closeMenu(); closeMenu();
@ -622,8 +623,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
canReply={canReply} canReply={canReply}
canQuote={canQuoteSelection} canQuote={canQuoteSelection}
canDelete={canDelete} canDelete={canDelete}
canReport={canReport}
canPin={canPin} canPin={canPin}
canReport={canReport}
repliesThreadInfo={repliesThreadInfo} repliesThreadInfo={repliesThreadInfo}
canUnpin={canUnpin} canUnpin={canUnpin}
canEdit={canEdit} canEdit={canEdit}
@ -683,11 +684,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
onShowOriginal={handleShowOriginal} onShowOriginal={handleShowOriginal}
onSelectLanguage={handleSelectLanguage} onSelectLanguage={handleSelectLanguage}
/> />
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
messageIds={reportMessageIds}
/>
<PinMessageModal <PinMessageModal
isOpen={isPinModalOpen} isOpen={isPinModalOpen}
messageId={message.id} messageId={message.id}
@ -811,17 +807,18 @@ export default memo(withGlobal<OwnProps>(
return { return {
threadId, threadId,
chat,
availableReactions, availableReactions,
topReactions, topReactions,
defaultTagReactions: defaultTags, defaultTagReactions: defaultTags,
noOptions, noOptions,
canReport,
canSendNow: isScheduled, canSendNow: isScheduled,
canReschedule: isScheduled, canReschedule: isScheduled,
canReply: !isPinned && !isScheduled && canReplyGlobally, canReply: !isPinned && !isScheduled && canReplyGlobally,
canPin: !isScheduled && canPin, canPin: !isScheduled && canPin,
canUnpin: !isScheduled && canUnpin, canUnpin: !isScheduled && canUnpin,
canDelete, canDelete,
canReport,
canEdit: !isPinned && canEdit, canEdit: !isPinned && canEdit,
canForward: !isScheduled && canForward, canForward: !isScheduled && canForward,
canFaveSticker: !isScheduled && canFaveSticker, canFaveSticker: !isScheduled && canFaveSticker,

View File

@ -101,8 +101,8 @@ type OwnProps = {
onUnpin?: NoneToVoidFunction; onUnpin?: NoneToVoidFunction;
onForward?: NoneToVoidFunction; onForward?: NoneToVoidFunction;
onDelete?: NoneToVoidFunction; onDelete?: NoneToVoidFunction;
onReport?: NoneToVoidFunction;
onFaveSticker?: NoneToVoidFunction; onFaveSticker?: NoneToVoidFunction;
onReport?: NoneToVoidFunction;
onUnfaveSticker?: NoneToVoidFunction; onUnfaveSticker?: NoneToVoidFunction;
onSelect?: NoneToVoidFunction; onSelect?: NoneToVoidFunction;
onSend?: NoneToVoidFunction; onSend?: NoneToVoidFunction;
@ -161,8 +161,8 @@ const MessageContextMenu: FC<OwnProps> = ({
canPin, canPin,
canUnpin, canUnpin,
canDelete, canDelete,
canReport,
canForward, canForward,
canReport,
canFaveSticker, canFaveSticker,
canUnfaveSticker, canUnfaveSticker,
canCopy, canCopy,
@ -194,8 +194,8 @@ const MessageContextMenu: FC<OwnProps> = ({
onUnpin, onUnpin,
onForward, onForward,
onDelete, onDelete,
onReport,
onFaveSticker, onFaveSticker,
onReport,
onUnfaveSticker, onUnfaveSticker,
onSelect, onSelect,
onSend, onSend,

View File

@ -20,6 +20,7 @@ import MapModal from './map/MapModal.async';
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async'; import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
import PaidReactionModal from './paidReaction/PaidReactionModal.async'; import PaidReactionModal from './paidReaction/PaidReactionModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async'; import ReportAdModal from './reportAd/ReportAdModal.async';
import ReportModal from './reportModal/ReportModal.async';
import StarsGiftModal from './stars/gift/StarsGiftModal.async'; import StarsGiftModal from './stars/gift/StarsGiftModal.async';
import StarsBalanceModal from './stars/StarsBalanceModal.async'; import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async'; import StarsPaymentModal from './stars/StarsPaymentModal.async';
@ -40,6 +41,7 @@ type ModalKey = keyof Pick<TabState,
'requestedAttachBotInstall' | 'requestedAttachBotInstall' |
'collectibleInfoModal' | 'collectibleInfoModal' |
'reportAdModal' | 'reportAdModal' |
'reportModal' |
'starsBalanceModal' | 'starsBalanceModal' |
'starsPayment' | 'starsPayment' |
'starsTransactionModal' | 'starsTransactionModal' |
@ -75,6 +77,7 @@ const MODALS: ModalRegistry = {
inviteViaLinkModal: InviteViaLinkModal, inviteViaLinkModal: InviteViaLinkModal,
requestedAttachBotInstall: AttachBotInstallModal, requestedAttachBotInstall: AttachBotInstallModal,
reportAdModal: ReportAdModal, reportAdModal: ReportAdModal,
reportModal: ReportModal,
webApps: WebAppModal, webApps: WebAppModal,
collectibleInfoModal: CollectibleInfoModal, collectibleInfoModal: CollectibleInfoModal,
mapModal: MapModal, mapModal: MapModal,

View File

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

View File

@ -0,0 +1,92 @@
.slide { // Do not remove .slide, identifier used in JS
overflow-x: hidden;
}
.root {
:global(.modal-dialog) {
max-width: 23rem;
}
}
.modalTitle {
display: flex;
flex-direction: column;
margin-left: 1rem;
}
.header {
display: flex;
align-items: center;
}
.optionText {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option {
margin-bottom: 0 !important;
}
.optionButton {
padding-block: 0 !important;
padding: 0 0 0 1rem !important;
}
.sectionTitle {
color: var(--color-primary);
padding-inline: 1rem;
margin-top: 0.75rem;
font-weight: 500;
font-size: 1rem;
}
.description {
margin-top: 0.75rem;
margin-bottom: 0;
color: var(--color-text-secondary);
padding-inline: 1rem;
}
.title {
margin-bottom: 0;
font-size: 1.1875rem;
}
.hasDepth {
margin-top: 0;
}
.titleMultiline > .title {
font-size: 1rem;
}
.subtitle {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.transition {
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: height 0.25s ease-in-out;
overflow: hidden;
}
.block {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 1rem;
}
.optionInfo {
width: 100%;
margin-bottom: 2rem;
}
.reportIcon {
margin-bottom: 2rem;
}

View File

@ -0,0 +1,232 @@
import type { ChangeEvent } from 'react';
import React, {
memo, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { TabState } from '../../../global/types';
import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom';
import buildClassName from '../../../util/buildClassName';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem';
import Modal from '../../ui/Modal';
import TextArea from '../../ui/TextArea';
import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition';
import styles from './ReportModal.module.scss';
const MAX_DESCRIPTION = 512;
export type OwnProps = {
modal: TabState['reportModal'];
};
const ReportModal = ({
modal,
}: OwnProps) => {
const {
reportMessages, reportStory, closeReportModal, openPreviousReportModal,
} = getActions();
const lang = useOldLang();
const isOpen = Boolean(modal);
// eslint-disable-next-line no-null/no-null
const transitionRef = useRef<HTMLDivElement>(null);
const [text, setText] = useState('');
const handleOptionClick = useLastCallback((e, option: string) => {
const {
messageIds, subject, peerId, chatId,
} = modal!;
if (!messageIds) return;
switch (subject) {
case 'message':
reportMessages({ chatId: chatId!, messageIds, option });
break;
case 'story':
reportStory({
storyId: messageIds[0], peerId: peerId!, option,
});
break;
}
});
const [renderingSection, renderingDepth] = useMemo(() => {
if (!modal) return [undefined, 0];
const sectionDepth = modal.sections.length - 1;
return [modal?.sections[sectionDepth], sectionDepth];
}, [modal]);
const handleBackClick = useLastCallback(() => {
openPreviousReportModal();
});
const handleCloseClick = useLastCallback(() => {
closeReportModal();
});
const header = useMemo(() => {
if (!modal) {
return undefined;
}
const hasSubtitle = Boolean(renderingSection?.subtitle);
return (
<div className="modal-header-condensed">
{renderingDepth ? (
<Button
round
color="translucent"
size="smaller"
ariaLabel={lang('Back')}
onClick={handleBackClick}
>
<Icon name="arrow-left" />
</Button>
) : (
<Button
round
color="translucent"
size="smaller"
ariaLabel={lang('Close')}
onClick={handleCloseClick}
>
<Icon name="close" />
</Button>
)}
<div className={buildClassName('modal-title', styles.modalTitle, hasSubtitle && styles.titleMultiline)}>
<h3 className={buildClassName(styles.title, renderingDepth && styles.hasDepth)}>
{renderingSection?.options
? lang(modal?.subject === 'story' ? 'ReportStory' : 'Report') : renderingSection?.title}
</h3>
{hasSubtitle && (
<span className={styles.subtitle}>{renderingSection.subtitle}</span>
)}
</div>
</div>
);
}, [lang, modal, renderingDepth, renderingSection?.options, renderingSection?.subtitle, renderingSection?.title]);
const handleTextChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setText(e.target.value);
});
useEffect(() => {
if (!modal) return;
const slide = document.querySelector<HTMLElement>(`.${ACTIVE_SLIDE_CLASS_NAME} > .${styles.slide}`);
if (!slide) return;
const height = slide.scrollHeight;
requestMutation(() => {
transitionRef.current!.style.height = `${height}px`;
});
}, [modal]);
const handleAnimationStart = useLastCallback(() => {
const slide = document.querySelector<HTMLElement>(`.${TO_SLIDE_CLASS_NAME} > .${styles.slide}`)!;
requestMeasure(() => {
const height = slide.scrollHeight;
requestMutation(() => {
transitionRef.current!.style.height = `${height}px`;
});
});
});
const closeReportMessageModalHandler = useLastCallback(() => {
setText('');
closeReportModal();
});
const sendMessageReportHandler = useLastCallback(() => {
const {
messageIds, subject, peerId, chatId,
} = modal!;
switch (subject) {
case 'message':
reportMessages({
chatId: chatId!, messageIds, option: renderingSection?.option, description: text,
});
break;
case 'story':
reportStory({
storyId: messageIds?.[0], peerId: peerId!, option: renderingSection?.option, description: text,
});
break;
}
closeReportMessageModalHandler();
});
return (
<Modal
isOpen={isOpen}
header={header}
onClose={closeReportMessageModalHandler}
className={buildClassName(styles.root, modal?.subject === 'story' && 'component-theme-dark')}
>
<Transition
name="slide"
className={styles.transition}
ref={transitionRef}
activeKey={renderingDepth}
onStart={handleAnimationStart}
>
<div className={styles.slide}>
{renderingSection?.options
? <h3 className={styles.sectionTitle}>{renderingSection?.title}</h3> : undefined}
{renderingSection?.options?.map((option) => (
<ListItem
narrow
secondaryIcon="next"
className={styles.option}
buttonClassName={styles.optionButton}
clickArg={option.option}
onClick={handleOptionClick}
>
<div className={styles.optionText}>{option.text}</div>
</ListItem>
))}
{renderingSection?.option ? (
<div className={styles.block}>
<AnimatedIconWithPreview
tgsUrl={LOCAL_TGS_URLS.Report}
size={150}
className={styles.reportIcon}
nonInteractive
forceAlways
/>
<TextArea
id="option"
className={styles.optionInfo}
label={renderingSection.isOptional ? lang('Report2CommentOptional') : lang('Report2Comment')}
onChange={handleTextChange}
value={text}
maxLength={MAX_DESCRIPTION}
maxLengthIndicator={(MAX_DESCRIPTION - text.length).toString()}
noReplaceNewlines
/>
<Button
size="smaller"
onClick={sendMessageReportHandler}
disabled={!renderingSection.isOptional ? !text.length : undefined}
>{lang('ReportSend')}
</Button>
</div>
) : undefined}
</div>
</Transition>
</Modal>
);
};
export default memo(ReportModal);

View File

@ -20,13 +20,13 @@ import { disableDirectTextInput, enableDirectTextInput } from '../../util/direct
import { animateClosing, animateOpening } from './helpers/ghostAnimation'; import { animateClosing, animateOpening } from './helpers/ghostAnimation';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck'; import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck';
import useSlideSizes from './hooks/useSlideSizes'; import useSlideSizes from './hooks/useSlideSizes';
import useStoryProps from './hooks/useStoryProps'; import useStoryProps from './hooks/useStoryProps';
import ReportModal from '../common/ReportModal';
import Button from '../ui/Button'; import Button from '../ui/Button';
import ShowTransition from '../ui/ShowTransition'; import ShowTransition from '../ui/ShowTransition';
import StealthModeModal from './StealthModeModal'; import StealthModeModal from './StealthModeModal';
@ -48,6 +48,7 @@ interface StateProps {
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
withAnimation?: boolean; withAnimation?: boolean;
isPrivacyModalOpen?: boolean; isPrivacyModalOpen?: boolean;
isReportModalOpen?: boolean;
} }
function StoryViewer({ function StoryViewer({
@ -59,13 +60,13 @@ function StoryViewer({
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
withAnimation, withAnimation,
isPrivacyModalOpen, isPrivacyModalOpen,
isReportModalOpen,
}: StateProps) { }: StateProps) {
const { closeStoryViewer, closeStoryPrivacyEditor } = getActions(); const { closeStoryViewer, closeStoryPrivacyEditor, reportStory } = getActions();
const lang = useOldLang(); const lang = useOldLang();
const [storyToDelete, setStoryToDelete] = useState<ApiTypeStory | undefined>(undefined); const [storyToDelete, setStoryToDelete] = useState<ApiTypeStory | undefined>(undefined);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false);
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(false);
const { bestImageData, thumbnail } = useStoryProps(story); const { bestImageData, thumbnail } = useStoryProps(story);
const slideSizes = useSlideSizes(); const slideSizes = useSlideSizes();
@ -78,7 +79,6 @@ function StoryViewer({
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setStoryToDelete(undefined); setStoryToDelete(undefined);
closeReportModal();
closeDeleteModal(); closeDeleteModal();
} }
}, [isOpen]); }, [isOpen]);
@ -101,15 +101,20 @@ function StoryViewer({
closeStoryViewer(); closeStoryViewer();
}, [closeStoryViewer]); }, [closeStoryViewer]);
const handleOpenDeleteModal = useCallback((s: ApiTypeStory) => { const handleOpenDeleteModal = useLastCallback((s: ApiTypeStory) => {
setStoryToDelete(s); setStoryToDelete(s);
openDeleteModal(); openDeleteModal();
}, []); });
const handleCloseDeleteModal = useCallback(() => { const handleCloseDeleteModal = useLastCallback(() => {
closeDeleteModal(); closeDeleteModal();
setStoryToDelete(undefined); setStoryToDelete(undefined);
}, []); });
const openMessageReport = useLastCallback(() => {
if (!storyId) return;
reportStory({ storyId, peerId });
});
useEffect(() => (isOpen ? captureEscKeyListener(() => { useEffect(() => (isOpen ? captureEscKeyListener(() => {
handleClose(); handleClose();
@ -162,7 +167,7 @@ function StoryViewer({
isOpen={isOpen} isOpen={isOpen}
isReportModalOpen={isReportModalOpen} isReportModalOpen={isReportModalOpen}
isDeleteModalOpen={isDeleteModalOpen} isDeleteModalOpen={isDeleteModalOpen}
onReport={openReportModal} onReport={openMessageReport}
onClose={handleClose} onClose={handleClose}
onDelete={handleOpenDeleteModal} onDelete={handleOpenDeleteModal}
/> />
@ -175,13 +180,6 @@ function StoryViewer({
<StoryViewModal /> <StoryViewModal />
<StealthModeModal /> <StealthModeModal />
<StorySettings isOpen={isPrivacyModalOpen} onClose={closeStoryPrivacyEditor} /> <StorySettings isOpen={isPrivacyModalOpen} onClose={closeStoryPrivacyEditor} />
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
subject="story"
peerId={peerId!}
storyId={storyId}
/>
</ShowTransition> </ShowTransition>
); );
} }
@ -190,14 +188,17 @@ export default memo(withGlobal((global): StateProps => {
const { const {
shouldSkipHistoryAnimations, storyViewer: { shouldSkipHistoryAnimations, storyViewer: {
storyId, peerId, isPrivacyModalOpen, origin, storyId, peerId, isPrivacyModalOpen, origin,
}, }, reportModal,
} = selectTabState(global); } = selectTabState(global);
const story = peerId && storyId ? selectPeerStory(global, peerId, storyId) : undefined; const story = peerId && storyId ? selectPeerStory(global, peerId, storyId) : undefined;
const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations'); const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
const isReportModalOpen = Boolean(reportModal);
return { return {
isOpen: selectIsStoryViewerOpen(global), isOpen: selectIsStoryViewerOpen(global),
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
isReportModalOpen,
peerId: peerId!, peerId: peerId!,
storyId, storyId,
story, story,

View File

@ -286,6 +286,7 @@ export const RE_TELEGRAM_LINK = /^(https?:\/\/)?telegram\.org\//i;
export const TME_LINK_PREFIX = 'https://t.me/'; export const TME_LINK_PREFIX = 'https://t.me/';
export const BOT_FATHER_USERNAME = 'botfather'; export const BOT_FATHER_USERNAME = 'botfather';
export const USERNAME_PURCHASE_ERROR = 'USERNAME_PURCHASE_AVAILABLE'; export const USERNAME_PURCHASE_ERROR = 'USERNAME_PURCHASE_AVAILABLE';
export const MESSAGE_ID_REQUIRED_ERROR = 'MESSAGE_ID_REQUIRED';
export const PURCHASE_USERNAME = 'auction'; export const PURCHASE_USERNAME = 'auction';
export const ACCEPTABLE_USERNAME_ERRORS = new Set([USERNAME_PURCHASE_ERROR, 'USERNAME_INVALID']); export const ACCEPTABLE_USERNAME_ERRORS = new Set([USERNAME_PURCHASE_ERROR, 'USERNAME_INVALID']);
export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', 'z.t.me']); export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', 'z.t.me']);

View File

@ -27,6 +27,7 @@ import { LoadMoreDirection, type ThreadId } from '../../../types';
import { import {
GIF_MIME_TYPE, GIF_MIME_TYPE,
MAX_MEDIA_FILES_FOR_ALBUM, MAX_MEDIA_FILES_FOR_ALBUM,
MESSAGE_ID_REQUIRED_ERROR,
MESSAGE_LIST_SLICE, MESSAGE_LIST_SLICE,
RE_TELEGRAM_LINK, RE_TELEGRAM_LINK,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
@ -118,6 +119,7 @@ import {
selectMessageReplyInfo, selectMessageReplyInfo,
selectNoWebPage, selectNoWebPage,
selectOutlyingListByMessageId, selectOutlyingListByMessageId,
selectPeer,
selectPeerStory, selectPeerStory,
selectPinnedIds, selectPinnedIds,
selectPollFromMessage, selectPollFromMessage,
@ -824,33 +826,81 @@ addActionHandler('deleteSavedHistory', async (global, actions, payload): Promise
addActionHandler('reportMessages', async (global, actions, payload): Promise<void> => { addActionHandler('reportMessages', async (global, actions, payload): Promise<void> => {
const { const {
messageIds, reason, description, tabId = getCurrentTabId(), messageIds, description = '', option = '', chatId, tabId = getCurrentTabId(),
} = payload!; } = payload!;
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
// TODO: Remove after implementing the new report system
if (messageIds) {
// eslint-disable-next-line no-console
console.warn('UNSUPPORTED');
return;
}
const { chatId } = currentMessageList;
const chat = selectChat(global, chatId)!; const chat = selectChat(global, chatId)!;
const result = await callApi('reportMessages', { const response = await callApi('reportMessages', {
peer: chat, messageIds, reason, description, peer: chat, messageIds, description, option,
}); });
actions.showNotification({ if (!response) return;
message: result
? oldTranslate('ReportPeer.AlertSuccess') const { result, error } = response;
: 'An error occurred while submitting your report. Please, try again later.',
tabId, if (error === MESSAGE_ID_REQUIRED_ERROR) {
}); actions.showNotification({
message: oldTranslate('lng_report_please_select_messages'),
tabId,
});
actions.closeReportModal({ tabId });
return;
}
if (!result) return;
if (result.type === 'reported') {
actions.showNotification({
message: result
? oldTranslate('ReportPeer.AlertSuccess')
: 'An error occurred while submitting your report. Please, try again later.',
tabId,
});
actions.closeReportModal({ tabId });
return;
}
if (result.type === 'selectOption') {
global = getGlobal();
const oldSections = selectTabState(global, tabId).reportModal?.sections;
const selectedOption = oldSections?.[oldSections.length - 1]?.options?.find((o) => o.option === option);
const newSection = {
title: result.title,
options: result.options,
subtitle: selectedOption?.text,
};
global = updateTabState(global, {
reportModal: {
chatId,
messageIds,
description,
subject: 'message',
sections: oldSections ? [...oldSections, newSection] : [newSection],
},
}, tabId);
setGlobal(global);
}
if (result.type === 'comment') {
global = getGlobal();
const oldSections = selectTabState(global, tabId).reportModal?.sections;
const selectedOption = oldSections?.[oldSections.length - 1]?.options?.find((o) => o.option === option);
const newSection = {
isOptional: result.isOptional,
option: result.option,
title: selectedOption?.text,
};
global = updateTabState(global, {
reportModal: {
chatId,
messageIds,
description,
subject: 'message',
sections: oldSections ? [...oldSections, newSection] : [newSection],
},
}, tabId);
setGlobal(global);
}
}); });
addActionHandler('sendMessageAction', async (global, actions, payload): Promise<void> => { addActionHandler('sendMessageAction', async (global, actions, payload): Promise<void> => {
@ -868,6 +918,17 @@ addActionHandler('sendMessageAction', async (global, actions, payload): Promise<
}); });
}); });
addActionHandler('reportChannelSpam', (global, actions, payload): ActionReturnType => {
const { participantId, chatId, messageIds } = payload;
const peer = selectPeer(global, participantId);
const chat = selectChat(global, chatId);
if (!peer || !chat) {
return;
}
void callApi('reportChannelSpam', { peer, chat, messageIds });
});
addActionHandler('markMessageListRead', (global, actions, payload): ActionReturnType => { addActionHandler('markMessageListRead', (global, actions, payload): ActionReturnType => {
const { maxId, tabId = getCurrentTabId() } = payload!; const { maxId, tabId = getCurrentTabId() } = payload!;

View File

@ -1,6 +1,6 @@
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { DEBUG } from '../../../config'; import { DEBUG, MESSAGE_ID_REQUIRED_ERROR } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { oldTranslate } from '../../../util/oldLangProvider'; import { oldTranslate } from '../../../util/oldLangProvider';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
@ -25,9 +25,10 @@ import {
updateStoryViews, updateStoryViews,
updateStoryViewsLoading, updateStoryViewsLoading,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { import {
selectPeer, selectPeerStories, selectPeerStory, selectPeer, selectPeerStories, selectPeerStory,
selectPinnedStories, selectPinnedStories, selectTabState,
} from '../../selectors'; } from '../../selectors';
const INFINITE_LOOP_MARKER = 100; const INFINITE_LOOP_MARKER = 100;
@ -414,8 +415,8 @@ addActionHandler('reportStory', async (global, actions, payload): Promise<void>
const { const {
peerId, peerId,
storyId, storyId,
reason, description = '',
description, option = '',
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
const peer = selectPeer(global, peerId); const peer = selectPeer(global, peerId);
@ -423,26 +424,80 @@ addActionHandler('reportStory', async (global, actions, payload): Promise<void>
return; return;
} }
// TODO: Remove after implementing the new report system const response = await callApi('reportStory', {
if (storyId) { peer,
// eslint-disable-next-line no-console storyId,
console.warn('UNSUPPORTED'); description,
option,
});
if (!response) return;
const { result, error } = response;
if (error === MESSAGE_ID_REQUIRED_ERROR) {
actions.showNotification({
message: oldTranslate('lng_report_please_select_messages'),
tabId,
});
actions.closeReportModal({ tabId });
return; return;
} }
const result = await callApi('reportStory', { if (!result) return;
peer,
storyId,
reason,
description,
});
actions.showNotification({ if (result.type === 'reported') {
message: result actions.showNotification({
? oldTranslate('ReportPeer.AlertSuccess') message: result
: 'An error occurred while submitting your report. Please, try again later.', ? oldTranslate('ReportPeer.AlertSuccess')
tabId, : 'An error occurred while submitting your report. Please, try again later.',
}); tabId,
});
actions.closeReportModal({ tabId });
return;
}
if (result.type === 'selectOption') {
global = getGlobal();
const oldSections = selectTabState(global, tabId).reportModal?.sections;
const selectedOption = oldSections?.[oldSections.length - 1]?.options?.find((o) => o.option === option);
const newSection = {
title: result.title,
options: result.options,
subtitle: selectedOption?.text,
};
global = updateTabState(global, {
reportModal: {
messageIds: [storyId],
subject: 'story',
peerId,
description,
sections: oldSections ? [...oldSections, newSection] : [newSection],
},
}, tabId);
setGlobal(global);
}
if (result.type === 'comment') {
global = getGlobal();
const oldSections = selectTabState(global, tabId).reportModal?.sections;
const selectedOption = oldSections?.[oldSections.length - 1]?.options?.find((o) => o.option === option);
const newSection = {
isOptional: result.isOptional,
option: result.option,
title: selectedOption?.text,
};
global = updateTabState(global, {
reportModal: {
messageIds: [storyId],
description,
peerId,
subject: 'story',
sections: oldSections ? [...oldSections, newSection] : [newSection],
},
}, tabId);
setGlobal(global);
}
}); });
addActionHandler('editStoryPrivacy', (global, actions, payload): ActionReturnType => { addActionHandler('editStoryPrivacy', (global, actions, payload): ActionReturnType => {

View File

@ -929,6 +929,13 @@ addActionHandler('closeReportAdModal', (global, actions, payload): ActionReturnT
}, tabId); }, tabId);
}); });
addActionHandler('closeReportModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
reportModal: undefined,
}, tabId);
});
addActionHandler('openPreviousReportAdModal', (global, actions, payload): ActionReturnType => { addActionHandler('openPreviousReportAdModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const reportAdModal = selectTabState(global, tabId).reportAdModal; const reportAdModal = selectTabState(global, tabId).reportAdModal;
@ -949,6 +956,26 @@ addActionHandler('openPreviousReportAdModal', (global, actions, payload): Action
}, tabId); }, tabId);
}); });
addActionHandler('openPreviousReportModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const reportModal = selectTabState(global, tabId).reportModal;
if (!reportModal) {
return undefined;
}
if (reportModal.sections.length === 1) {
actions.closeReportModal({ tabId });
return undefined;
}
return updateTabState(global, {
reportModal: {
...reportModal,
sections: reportModal.sections.slice(0, -1),
},
}, tabId);
});
addActionHandler('openPaidReactionModal', (global, actions, payload): ActionReturnType => { addActionHandler('openPaidReactionModal', (global, actions, payload): ActionReturnType => {
const { chatId, messageId, tabId = getCurrentTabId() } = payload; const { chatId, messageId, tabId = getCurrentTabId() } = payload;
return updateTabState(global, { return updateTabState(global, {

View File

@ -638,6 +638,24 @@ export type TabState = {
}[]; }[];
}; };
reportModal?: {
chatId?: string;
messageIds: number[];
description: string;
peerId?: string;
subject: 'story' | 'message';
sections: {
title?: string;
subtitle?: string;
options?: {
text: string;
option: string;
}[];
isOptional?: boolean;
option?: string;
}[];
};
activeDownloads: ActiveDownloads; activeDownloads: ActiveDownloads;
statistics: { statistics: {
@ -1728,7 +1746,9 @@ export interface ActionPayloads {
option?: string; option?: string;
} & WithTabId; } & WithTabId;
openPreviousReportAdModal: WithTabId | undefined; openPreviousReportAdModal: WithTabId | undefined;
openPreviousReportModal: WithTabId | undefined;
closeReportAdModal: WithTabId | undefined; closeReportAdModal: WithTabId | undefined;
closeReportModal: WithTabId | undefined;
hideSponsoredMessages: WithTabId | undefined; hideSponsoredMessages: WithTabId | undefined;
loadSendAs: { loadSendAs: {
chatId: string; chatId: string;
@ -1768,15 +1788,21 @@ export interface ActionPayloads {
isReversed?: boolean; isReversed?: boolean;
} & WithTabId; } & WithTabId;
reportMessages: { reportMessages: {
chatId: string;
messageIds: number[]; messageIds: number[];
reason: ApiReportReason; description?: string;
description: string; option?: string;
} & WithTabId; } & WithTabId;
sendMessageAction: { sendMessageAction: {
action: ApiSendMessageAction; action: ApiSendMessageAction;
chatId: string; chatId: string;
threadId: ThreadId; threadId: ThreadId;
}; };
reportChannelSpam: {
chatId: string;
participantId: string;
messageIds: number[];
};
loadSeenBy: { loadSeenBy: {
chatId: string; chatId: string;
messageId: number; messageId: number;
@ -2698,9 +2724,9 @@ export interface ActionPayloads {
} & WithTabId; } & WithTabId;
reportStory: { reportStory: {
peerId: string; peerId: string;
option?: string;
storyId: number; storyId: number;
reason: ApiReportReason; description?: string;
description: string;
} & WithTabId; } & WithTabId;
openStoryPrivacyEditor: WithTabId | undefined; openStoryPrivacyEditor: WithTabId | undefined;
closeStoryPrivacyEditor: WithTabId | undefined; closeStoryPrivacyEditor: WithTabId | undefined;

View File

@ -159,7 +159,7 @@ const useChatContextActions = ({
? { title: lang('Unarchive'), icon: 'unarchive', handler: () => toggleChatArchived({ id: chat.id }) } ? { title: lang('Unarchive'), icon: 'unarchive', handler: () => toggleChatArchived({ id: chat.id }) }
: { title: lang('Archive'), icon: 'archive', handler: () => toggleChatArchived({ id: chat.id }) }; : { title: lang('Archive'), icon: 'archive', handler: () => toggleChatArchived({ id: chat.id }) };
const canReport = handleReport && (isChatChannel(chat) || isChatGroup(chat) || (user && !user.isSelf)); const canReport = handleReport && !user && (isChatChannel(chat) || isChatGroup(chat));
const actionReport = canReport const actionReport = canReport
? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport } ? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport }
: undefined; : undefined;

View File

@ -1600,6 +1600,7 @@ help.getPeerColors#da80f42f hash:int = help.PeerColors;
help.getTimezonesList#49b30240 hash:int = help.TimezonesList; help.getTimezonesList#49b30240 hash:int = help.TimezonesList;
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
channels.reportSpam#f44a8315 channel:InputChannel participant:InputPeer id:Vector<int> = Bool;
channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages; channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;
channels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants; channels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants;
channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant; channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant;

View File

@ -228,6 +228,7 @@
"channels.getChannelRecommendations", "channels.getChannelRecommendations",
"channels.reportSponsoredMessage", "channels.reportSponsoredMessage",
"channels.searchPosts", "channels.searchPosts",
"channels.reportSpam",
"bots.canSendMessage", "bots.canSendMessage",
"bots.allowSendMessage", "bots.allowSendMessage",
"bots.invokeWebViewCustomMethod", "bots.invokeWebViewCustomMethod",