Message Reports: Implement reporting for messages and stories (#5052)
This commit is contained in:
parent
f7e553734b
commit
01dc204fa7
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
BIN
src/assets/tgs/Report.tgs
Normal file
Binary file not shown.
@ -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';
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
18
src/components/modals/reportModal/ReportModal.async.tsx
Normal file
18
src/components/modals/reportModal/ReportModal.async.tsx
Normal 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;
|
||||
92
src/components/modals/reportModal/ReportModal.module.scss
Normal file
92
src/components/modals/reportModal/ReportModal.module.scss
Normal 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;
|
||||
}
|
||||
232
src/components/modals/reportModal/ReportModal.tsx
Normal file
232
src/components/modals/reportModal/ReportModal.tsx
Normal 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);
|
||||
@ -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,
|
||||
|
||||
@ -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']);
|
||||
|
||||
@ -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!;
|
||||
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -228,6 +228,7 @@
|
||||
"channels.getChannelRecommendations",
|
||||
"channels.reportSponsoredMessage",
|
||||
"channels.searchPosts",
|
||||
"channels.reportSpam",
|
||||
"bots.canSendMessage",
|
||||
"bots.allowSendMessage",
|
||||
"bots.invokeWebViewCustomMethod",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user