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,
ApiMessageEntity,
ApiMessageForwardInfo,
ApiMessageReportResult,
ApiNewPoll,
ApiPeer,
ApiPhoto,
@ -1246,3 +1247,33 @@ export function buildApiQuickReply(reply: GramJs.TypeQuickReply): ApiQuickReply
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,
ApiClickSponsoredMessage,
ApiContact,
ApiError,
ApiFormattedText,
ApiGlobalMessageSearchType,
ApiInputReplyInfo,
@ -19,7 +20,6 @@ import type {
ApiPeer,
ApiPoll,
ApiReaction,
ApiReportReason,
ApiSendMessageAction,
ApiSticker,
ApiStory,
@ -36,6 +36,7 @@ import {
GIF_MIME_TYPE,
MAX_INT_32,
MENTION_UNREAD_SLICE,
MESSAGE_ID_REQUIRED_ERROR,
PINNED_MESSAGES_LIMIT,
REACTION_UNREAD_SLICE,
SUPPORTED_PHOTO_CONTENT_TYPES,
@ -47,13 +48,17 @@ import { compact, split } from '../../../util/iteratees';
import { getMessageKey } from '../../../util/keys/messageKey';
import { getServerTimeOffset } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform';
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
import {
buildApiChatFromPreview,
buildApiSendAsPeerId,
} from '../apiBuilders/chats';
import { buildApiFormattedText } from '../apiBuilders/common';
import { buildMessageMediaContent, buildMessageTextContent, buildWebPage } from '../apiBuilders/messageContent';
import {
buildApiFactCheck,
buildApiMessage,
buildApiQuickReply,
buildApiReportResult,
buildApiSponsoredMessage,
buildApiThreadInfo,
buildLocalForwardedMessage,
@ -901,18 +906,45 @@ export async function deleteSavedHistory({
}
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({
peer: buildInputPeer(peer.id, peer.accessHash),
id: messageIds,
option: Buffer.alloc(0),
message: description,
}));
try {
const result = await invokeRequest(new GramJs.messages.Report({
peer: buildInputPeer(peer.id, peer.accessHash),
id: messageIds,
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({

View File

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

View File

@ -871,6 +871,21 @@ export interface ApiMessageThreadInfo extends ApiBaseThreadInfo {
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 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 VerificationMonetizationModal } from '../components/common/VerificationMonetizationModal';
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 DeleteMessageModal } from '../components/common/DeleteMessageModal';
export { default as PinMessageModal } from '../components/common/PinMessageModal';

View File

@ -96,7 +96,7 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
const {
deleteMessages,
deleteScheduledMessages,
reportMessages,
reportChannelSpam,
deleteChatMember,
updateChatMemberBannedRights,
closeDeleteMessageModal,
@ -231,10 +231,10 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
if (isSchedule) {
deleteScheduledMessages({ messageIds });
} else if (!isOwn && (chosenSpanOption || chosenDeleteOption || chosenBanOption) && (isGroup || isSuperGroup)) {
if (chosenSpanOption) {
if (chosenSpanOption && sender) {
const filteredMessageIdList = filterMessageIdByUserId(chosenSpanOption, messageIdList!);
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 buildClassName from '../../util/buildClassName';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
@ -17,55 +15,28 @@ import RadioGroup from '../ui/RadioGroup';
export type OwnProps = {
isOpen: boolean;
subject?: 'peer' | 'messages' | 'media' | 'story';
peerId?: string;
photo?: ApiPhoto;
messageIds?: number[];
storyId?: number;
onClose: () => void;
onCloseAnimationEnd?: () => void;
};
const ReportModal: FC<OwnProps> = ({
const ReportAvatarModal: FC<OwnProps> = ({
isOpen,
subject = 'messages',
peerId,
photo,
messageIds,
storyId,
onClose,
onCloseAnimationEnd,
}) => {
const {
reportMessages,
reportPeer,
reportProfilePhoto,
reportStory,
exitMessageSelectMode,
} = getActions();
const { reportProfilePhoto } = getActions();
const [selectedReason, setSelectedReason] = useState<ApiReportReason>('spam');
const [description, setDescription] = useState('');
const handleReport = useLastCallback(() => {
switch (subject) {
case 'messages':
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,
});
}
reportProfilePhoto({
chatId: peerId, photo, reason: selectedReason, description,
});
onClose();
});
@ -90,18 +61,11 @@ const ReportModal: FC<OwnProps> = ({
{ value: 'other', label: lang('lng_report_reason_other') },
], [lang]);
if (
(subject === 'messages' && !messageIds)
|| (subject === 'peer' && !peerId)
|| (subject === 'media' && (!peerId || !photo))
|| (subject === 'story' && (!storyId || !peerId))
) {
if (!peerId || !photo) {
return undefined;
}
const title = subject === 'messages'
? lang('lng_report_message_title')
: lang('ReportPeer.Report');
const title = lang('ReportPeer.Report');
return (
<Modal
@ -109,7 +73,7 @@ const ReportModal: FC<OwnProps> = ({
onClose={onClose}
onEnter={isOpen ? handleReport : undefined}
onCloseAnimationEnd={onCloseAnimationEnd}
className={buildClassName('narrow', subject === 'story' && 'component-theme-dark')}
className="narrow"
title={title}
>
<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 MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs';
import ReadTime from '../../../assets/tgs/ReadTime.tgs';
import Report from '../../../assets/tgs/Report.tgs';
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
import Experimental from '../../../assets/tgs/settings/Experimental.tgs';
@ -62,4 +63,5 @@ export const LOCAL_TGS_URLS = {
Fragment,
StarReactionEffect,
StarReaction,
Report,
};

View File

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

View File

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

View File

@ -94,7 +94,7 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
}) => {
const {
deleteMessages,
reportMessages,
reportChannelSpam,
deleteChatMember,
deleteScheduledMessages,
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[]) => {
if (filteredMessageIdList && filteredMessageIdList.length) {
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
@ -257,10 +269,18 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
} else if (!isSenderOwner && shouldShowOptions) {
if (chosenSpanOption) {
const userIdList = chosenSpanOption.filter((option) => !Number.isNaN(Number(option)));
const filteredMessageIdList = filterMessageIdByUserId(userIdList, selectedMessageIds!);
if (filteredMessageIdList?.length) {
reportMessages({ messageIds: filteredMessageIdList, reason: 'spam', description: '' });
}
const userMessagesMap = selectedMessageIds!.reduce<Record<string, number[]>>((acc, msgId) => {
const sender = selectSenderFromMessage(getGlobal(), chat.id, msgId);
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) {

View File

@ -49,7 +49,6 @@ import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
import DeleteChatModal from '../common/DeleteChatModal';
import ReportModal from '../common/ReportModal';
import MuteChatModal from '../left/MuteChatModal.async';
import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem';
@ -109,8 +108,8 @@ type StateProps = {
isForum?: boolean;
isForumAsMessages?: true;
canAddContact?: boolean;
canReportChat?: boolean;
canDeleteChat?: boolean;
canReportChat?: boolean;
canGift?: boolean;
canCreateTopic?: boolean;
canEditTopic?: boolean;
@ -143,6 +142,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
isChatInfoShown,
canStartBot,
canSubscribe,
canReportChat,
canSearch,
canCall,
canMute,
@ -156,7 +156,6 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
chat,
isPrivate,
isMuted,
canReportChat,
canDeleteChat,
canGift,
hasLinkedChat,
@ -203,13 +202,13 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
unblockUser,
setViewForumAsMessages,
openBoostModal,
reportMessages,
} = getActions();
const { isMobile } = useAppLayout();
const [isMenuOpen, setIsMenuOpen] = useState(true);
const [shouldCloseFast, setShouldCloseFast] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag();
const { x, y } = anchor;
@ -219,18 +218,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
(!isChatInfoShown && isForum) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION,
);
const handleReport = useLastCallback(() => {
setIsMenuOpen(false);
setIsReportModalOpen(true);
});
const closeReportModal = useLastCallback(() => {
setIsReportModalOpen(false);
const closeMuteModal = useLastCallback(() => {
setIsMuteModalOpen(false);
onClose();
});
const closeMuteModal = useLastCallback(() => {
setIsMuteModalOpen(false);
const handleReport = useLastCallback(() => {
setIsMenuOpen(false);
reportMessages({ chatId, messageIds: [] });
onClose();
});
@ -725,14 +720,6 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
chatId={chat.id}
/>
)}
{canReportChat && chat?.id && (
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
subject="peer"
peerId={chat.id}
/>
)}
</div>
</Portal>
);
@ -749,8 +736,8 @@ export default memo(withGlobal<OwnProps>(
const canAddContact = user && getCanAddContact(user);
const isMainThread = threadId === MAIN_THREAD_ID;
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const canReportChat = isMainThread && (isChatChannel(chat) || isChatGroup(chat) || (user && !user.isSelf));
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
const canReportChat = isMainThread && !user && (isChatChannel(chat) || isChatGroup(chat));
const chatBot = !isSystemBot(chatId) ? selectBot(global, chatId) : undefined;
const userFullInfo = isPrivate ? selectUserFullInfo(global, chatId) : undefined;
@ -778,8 +765,8 @@ export default memo(withGlobal<OwnProps>(
isForum: chat?.isForum,
isForumAsMessages: chat?.isForumAsMessages,
canAddContact,
canReportChat,
canDeleteChat: getCanDeleteChat(chat),
canReportChat,
canGift,
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
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 { getActions, withGlobal } from '../../global';
import type { ApiChat } from '../../api/types';
import type { MessageListType } from '../../global/types';
import type { IconName } from '../../types/icons';
@ -9,7 +10,7 @@ import {
selectCanDeleteSelectedMessages,
selectCanDownloadSelectedMessages,
selectCanForwardMessages,
selectCanReportSelectedMessages,
selectCanReportSelectedMessages, selectCurrentChat,
selectCurrentMessageList, selectHasProtectedMessage,
selectSelectedMessagesCount,
selectTabState,
@ -23,7 +24,6 @@ import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useCopySelectedMessages from './hooks/useCopySelectedMessages';
import ReportModal from '../common/ReportModal';
import Button from '../ui/Button';
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
@ -36,6 +36,7 @@ export type OwnProps = {
};
type StateProps = {
chat?: ApiChat;
isSchedule: boolean;
selectedMessagesCount?: number;
canDeleteMessages?: boolean;
@ -48,6 +49,7 @@ type StateProps = {
};
const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
chat,
canPost,
isActive,
messageListType,
@ -67,11 +69,11 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
downloadSelectedMessages,
copySelectedMessages,
showNotification,
reportMessages,
} = getActions();
const lang = useOldLang();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
useCopySelectedMessages(isActive);
@ -80,7 +82,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
});
useEffect(() => {
return isActive && !isDeleteModalOpen && !isReportModalOpen && !isAnyModalOpen
return isActive && !isDeleteModalOpen && !isAnyModalOpen
? captureKeyboardListeners({
onBackspace: canDeleteMessages ? openDeleteModal : undefined,
onDelete: canDeleteMessages ? openDeleteModal : undefined,
@ -88,7 +90,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
})
: undefined;
}, [
isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, handleExitMessageSelectMode, isAnyModalOpen,
isActive, isDeleteModalOpen, openDeleteModal, handleExitMessageSelectMode, isAnyModalOpen,
canDeleteMessages,
]);
@ -110,6 +112,15 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
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(
'MessageSelectToolbar',
canPost && 'with-composer',
@ -160,7 +171,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
)
)}
{canReportMessages && (
renderButton('flag', lang('Conversation.ReportMessages'), openReportModal)
renderButton('flag', lang('Conversation.ReportMessages'), openMessageReport)
)}
{canDownloadMessages && !hasProtectedMessage && (
renderButton('download', lang('lng_media_download'), handleDownload)
@ -181,11 +192,6 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
onClose={closeDeleteModal}
/>
)}
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
messageIds={selectedMessageIds}
/>
</div>
);
};
@ -193,6 +199,7 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const tabState = selectTabState(global);
const chat = selectCurrentChat(global);
const { type: messageListType, chatId } = selectCurrentMessageList(global) || {};
const isSchedule = messageListType === 'scheduled';
const { canDelete } = selectCanDeleteSelectedMessages(global);
@ -203,9 +210,10 @@ export default memo(withGlobal<OwnProps>(
const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false;
const isShareMessageModalOpen = tabState.isShareMessageModalShown;
const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall);
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal);
return {
chat,
isSchedule,
selectedMessagesCount: selectSelectedMessagesCount(global),
canDeleteMessages: canDelete,

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import MapModal from './map/MapModal.async';
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async';
import ReportModal from './reportModal/ReportModal.async';
import StarsGiftModal from './stars/gift/StarsGiftModal.async';
import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async';
@ -40,6 +41,7 @@ type ModalKey = keyof Pick<TabState,
'requestedAttachBotInstall' |
'collectibleInfoModal' |
'reportAdModal' |
'reportModal' |
'starsBalanceModal' |
'starsPayment' |
'starsTransactionModal' |
@ -75,6 +77,7 @@ const MODALS: ModalRegistry = {
inviteViaLinkModal: InviteViaLinkModal,
requestedAttachBotInstall: AttachBotInstallModal,
reportAdModal: ReportAdModal,
reportModal: ReportModal,
webApps: WebAppModal,
collectibleInfoModal: CollectibleInfoModal,
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 useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import { dispatchPriorityPlaybackEvent } from '../../hooks/usePriorityPlaybackCheck';
import useSlideSizes from './hooks/useSlideSizes';
import useStoryProps from './hooks/useStoryProps';
import ReportModal from '../common/ReportModal';
import Button from '../ui/Button';
import ShowTransition from '../ui/ShowTransition';
import StealthModeModal from './StealthModeModal';
@ -48,6 +48,7 @@ interface StateProps {
shouldSkipHistoryAnimations?: boolean;
withAnimation?: boolean;
isPrivacyModalOpen?: boolean;
isReportModalOpen?: boolean;
}
function StoryViewer({
@ -59,13 +60,13 @@ function StoryViewer({
shouldSkipHistoryAnimations,
withAnimation,
isPrivacyModalOpen,
isReportModalOpen,
}: StateProps) {
const { closeStoryViewer, closeStoryPrivacyEditor } = getActions();
const { closeStoryViewer, closeStoryPrivacyEditor, reportStory } = getActions();
const lang = useOldLang();
const [storyToDelete, setStoryToDelete] = useState<ApiTypeStory | undefined>(undefined);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false);
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(false);
const { bestImageData, thumbnail } = useStoryProps(story);
const slideSizes = useSlideSizes();
@ -78,7 +79,6 @@ function StoryViewer({
useEffect(() => {
if (!isOpen) {
setStoryToDelete(undefined);
closeReportModal();
closeDeleteModal();
}
}, [isOpen]);
@ -101,15 +101,20 @@ function StoryViewer({
closeStoryViewer();
}, [closeStoryViewer]);
const handleOpenDeleteModal = useCallback((s: ApiTypeStory) => {
const handleOpenDeleteModal = useLastCallback((s: ApiTypeStory) => {
setStoryToDelete(s);
openDeleteModal();
}, []);
});
const handleCloseDeleteModal = useCallback(() => {
const handleCloseDeleteModal = useLastCallback(() => {
closeDeleteModal();
setStoryToDelete(undefined);
}, []);
});
const openMessageReport = useLastCallback(() => {
if (!storyId) return;
reportStory({ storyId, peerId });
});
useEffect(() => (isOpen ? captureEscKeyListener(() => {
handleClose();
@ -162,7 +167,7 @@ function StoryViewer({
isOpen={isOpen}
isReportModalOpen={isReportModalOpen}
isDeleteModalOpen={isDeleteModalOpen}
onReport={openReportModal}
onReport={openMessageReport}
onClose={handleClose}
onDelete={handleOpenDeleteModal}
/>
@ -175,13 +180,6 @@ function StoryViewer({
<StoryViewModal />
<StealthModeModal />
<StorySettings isOpen={isPrivacyModalOpen} onClose={closeStoryPrivacyEditor} />
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
subject="story"
peerId={peerId!}
storyId={storyId}
/>
</ShowTransition>
);
}
@ -190,14 +188,17 @@ export default memo(withGlobal((global): StateProps => {
const {
shouldSkipHistoryAnimations, storyViewer: {
storyId, peerId, isPrivacyModalOpen, origin,
},
}, reportModal,
} = selectTabState(global);
const story = peerId && storyId ? selectPeerStory(global, peerId, storyId) : undefined;
const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
const isReportModalOpen = Boolean(reportModal);
return {
isOpen: selectIsStoryViewerOpen(global),
shouldSkipHistoryAnimations,
isReportModalOpen,
peerId: peerId!,
storyId,
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 BOT_FATHER_USERNAME = 'botfather';
export const USERNAME_PURCHASE_ERROR = 'USERNAME_PURCHASE_AVAILABLE';
export const MESSAGE_ID_REQUIRED_ERROR = 'MESSAGE_ID_REQUIRED';
export const PURCHASE_USERNAME = 'auction';
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']);

View File

@ -27,6 +27,7 @@ import { LoadMoreDirection, type ThreadId } from '../../../types';
import {
GIF_MIME_TYPE,
MAX_MEDIA_FILES_FOR_ALBUM,
MESSAGE_ID_REQUIRED_ERROR,
MESSAGE_LIST_SLICE,
RE_TELEGRAM_LINK,
SERVICE_NOTIFICATIONS_USER_ID,
@ -118,6 +119,7 @@ import {
selectMessageReplyInfo,
selectNoWebPage,
selectOutlyingListByMessageId,
selectPeer,
selectPeerStory,
selectPinnedIds,
selectPollFromMessage,
@ -824,33 +826,81 @@ addActionHandler('deleteSavedHistory', async (global, actions, payload): Promise
addActionHandler('reportMessages', async (global, actions, payload): Promise<void> => {
const {
messageIds, reason, description, tabId = getCurrentTabId(),
messageIds, description = '', option = '', chatId, tabId = getCurrentTabId(),
} = 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 result = await callApi('reportMessages', {
peer: chat, messageIds, reason, description,
const response = await callApi('reportMessages', {
peer: chat, messageIds, description, option,
});
actions.showNotification({
message: result
? oldTranslate('ReportPeer.AlertSuccess')
: 'An error occurred while submitting your report. Please, try again later.',
tabId,
});
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;
}
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> => {
@ -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 => {
const { maxId, tabId = getCurrentTabId() } = payload!;

View File

@ -1,6 +1,6 @@
import type { ActionReturnType } from '../../types';
import { DEBUG } from '../../../config';
import { DEBUG, MESSAGE_ID_REQUIRED_ERROR } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { oldTranslate } from '../../../util/oldLangProvider';
import { getServerTime } from '../../../util/serverTime';
@ -25,9 +25,10 @@ import {
updateStoryViews,
updateStoryViewsLoading,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
selectPeer, selectPeerStories, selectPeerStory,
selectPinnedStories,
selectPinnedStories, selectTabState,
} from '../../selectors';
const INFINITE_LOOP_MARKER = 100;
@ -414,8 +415,8 @@ addActionHandler('reportStory', async (global, actions, payload): Promise<void>
const {
peerId,
storyId,
reason,
description,
description = '',
option = '',
tabId = getCurrentTabId(),
} = payload;
const peer = selectPeer(global, peerId);
@ -423,26 +424,80 @@ addActionHandler('reportStory', async (global, actions, payload): Promise<void>
return;
}
// TODO: Remove after implementing the new report system
if (storyId) {
// eslint-disable-next-line no-console
console.warn('UNSUPPORTED');
const response = await callApi('reportStory', {
peer,
storyId,
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;
}
const result = await callApi('reportStory', {
peer,
storyId,
reason,
description,
});
if (!result) return;
actions.showNotification({
message: result
? oldTranslate('ReportPeer.AlertSuccess')
: 'An error occurred while submitting your report. Please, try again later.',
tabId,
});
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: {
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 => {

View File

@ -929,6 +929,13 @@ addActionHandler('closeReportAdModal', (global, actions, payload): ActionReturnT
}, tabId);
});
addActionHandler('closeReportModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
reportModal: undefined,
}, tabId);
});
addActionHandler('openPreviousReportAdModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const reportAdModal = selectTabState(global, tabId).reportAdModal;
@ -949,6 +956,26 @@ addActionHandler('openPreviousReportAdModal', (global, actions, payload): Action
}, 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 => {
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
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;
statistics: {
@ -1728,7 +1746,9 @@ export interface ActionPayloads {
option?: string;
} & WithTabId;
openPreviousReportAdModal: WithTabId | undefined;
openPreviousReportModal: WithTabId | undefined;
closeReportAdModal: WithTabId | undefined;
closeReportModal: WithTabId | undefined;
hideSponsoredMessages: WithTabId | undefined;
loadSendAs: {
chatId: string;
@ -1768,15 +1788,21 @@ export interface ActionPayloads {
isReversed?: boolean;
} & WithTabId;
reportMessages: {
chatId: string;
messageIds: number[];
reason: ApiReportReason;
description: string;
description?: string;
option?: string;
} & WithTabId;
sendMessageAction: {
action: ApiSendMessageAction;
chatId: string;
threadId: ThreadId;
};
reportChannelSpam: {
chatId: string;
participantId: string;
messageIds: number[];
};
loadSeenBy: {
chatId: string;
messageId: number;
@ -2698,9 +2724,9 @@ export interface ActionPayloads {
} & WithTabId;
reportStory: {
peerId: string;
option?: string;
storyId: number;
reason: ApiReportReason;
description: string;
description?: string;
} & WithTabId;
openStoryPrivacyEditor: 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('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
? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport }
: undefined;

View File

@ -1600,6 +1600,7 @@ help.getPeerColors#da80f42f hash:int = help.PeerColors;
help.getTimezonesList#49b30240 hash:int = help.TimezonesList;
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
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.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants;
channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant;

View File

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