diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index c091dd87a..fc213a6bf 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -11,7 +11,9 @@ import { ApiGroupCall, ApiMessageEntity, ApiMessageEntityTypes, - ApiNewPoll, ApiPhoneCall, + ApiNewPoll, + ApiPhoto, + ApiPhoneCall, ApiReportReason, ApiSendMessageAction, ApiSticker, @@ -343,6 +345,20 @@ export function buildChatPhotoForLocalDb(photo: GramJs.TypePhoto) { }); } +export function buildInputPhoto(photo: ApiPhoto) { + const localPhoto = localDb.photos[photo?.id]; + + if (!localPhoto) { + return undefined; + } + + return new GramJs.InputPhoto(pick(localPhoto, [ + 'id', + 'accessHash', + 'fileReference', + ])); +} + export function buildInputContact({ phone, firstName, diff --git a/src/api/gramjs/methods/account.ts b/src/api/gramjs/methods/account.ts new file mode 100644 index 000000000..f9bae3b46 --- /dev/null +++ b/src/api/gramjs/methods/account.ts @@ -0,0 +1,43 @@ +import { + ApiChat, ApiPhoto, ApiReportReason, ApiUser, +} from '../../types'; +import { invokeRequest } from './client'; +import { Api as GramJs } from '../../../lib/gramjs'; +import { buildInputPeer, buildInputReportReason, buildInputPhoto } from '../gramjsBuilders'; + +export async function reportPeer({ + peer, + reason, + description, +}: { + peer: ApiChat | ApiUser; reason: ApiReportReason; description?: string; +}) { + const result = await invokeRequest(new GramJs.account.ReportPeer({ + peer: buildInputPeer(peer.id, peer.accessHash), + reason: buildInputReportReason(reason), + message: description, + })); + + return result; +} + +export async function reportProfilePhoto({ + peer, + photo, + reason, + description, +}: { + peer: ApiChat | ApiUser; photo: ApiPhoto; reason: ApiReportReason; description?: string; +}) { + const photoId = buildInputPhoto(photo); + if (!photoId) return undefined; + + const result = await invokeRequest(new GramJs.account.ReportProfilePhoto({ + peer: buildInputPeer(peer.id, peer.accessHash), + photoId, + reason: buildInputReportReason(reason), + message: description, + })); + + return result; +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 692e9455d..b66443665 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -2,6 +2,10 @@ export { destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, } from './client'; +export { + reportPeer, reportProfilePhoto, +} from './account'; + export { provideAuthPhoneNumber, provideAuthCode, provideAuthPassword, provideAuthRegistration, restartAuth, restartAuthWithQr, } from './auth'; diff --git a/src/components/common/ReportMessageModal.tsx b/src/components/common/ReportModal.tsx similarity index 63% rename from src/components/common/ReportMessageModal.tsx rename to src/components/common/ReportModal.tsx index 8e7c9deca..64e9a9a9b 100644 --- a/src/components/common/ReportMessageModal.tsx +++ b/src/components/common/ReportModal.tsx @@ -1,11 +1,11 @@ import { ChangeEvent } from 'react'; import React, { - FC, memo, useCallback, useState, + FC, memo, useCallback, useMemo, useState, } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import { ApiReportReason } from '../../api/types'; +import { ApiPhoto, ApiReportReason } from '../../api/types'; import useLang from '../../hooks/useLang'; @@ -16,17 +16,27 @@ import InputText from '../ui/InputText'; export type OwnProps = { isOpen: boolean; + subject?: 'peer' | 'messages' | 'media'; + chatId?: string; + photo?: ApiPhoto; messageIds?: number[]; onClose: () => void; + onCloseAnimationEnd?: () => void; }; -const ReportMessageModal: FC = ({ +const ReportModal: FC = ({ isOpen, + subject = 'messages', + chatId, + photo, messageIds, onClose, + onCloseAnimationEnd, }) => { const { reportMessages, + reportPeer, + reportProfilePhoto, exitMessageSelectMode, } = getActions(); @@ -34,10 +44,34 @@ const ReportMessageModal: FC = ({ const [description, setDescription] = useState(''); const handleReport = useCallback(() => { - reportMessages({ messageIds, reason: selectedReason, description }); - exitMessageSelectMode(); + switch (subject) { + case 'messages': + reportMessages({ messageIds, reason: selectedReason, description }); + exitMessageSelectMode(); + break; + case 'peer': + reportPeer({ chatId, reason: selectedReason, description }); + break; + case 'media': + reportProfilePhoto({ + chatId, photo, reason: selectedReason, description, + }); + break; + } onClose(); - }, [description, exitMessageSelectMode, messageIds, onClose, reportMessages, selectedReason]); + }, [ + description, + exitMessageSelectMode, + messageIds, + photo, + onClose, + reportMessages, + selectedReason, + chatId, + reportProfilePhoto, + reportPeer, + subject, + ]); const handleSelectReason = useCallback((value: string) => { setSelectedReason(value as ApiReportReason); @@ -49,7 +83,7 @@ const ReportMessageModal: FC = ({ const lang = useLang(); - const REPORT_OPTIONS: { value: ApiReportReason; label: string }[] = [ + const REPORT_OPTIONS: { value: ApiReportReason; label: string }[] = useMemo(() => [ { value: 'spam', label: lang('lng_report_reason_spam') }, { value: 'violence', label: lang('lng_report_reason_violence') }, { value: 'pornography', label: lang('lng_report_reason_pornography') }, @@ -58,19 +92,28 @@ const ReportMessageModal: FC = ({ { value: 'illegalDrugs', label: 'Illegal Drugs' }, { value: 'personalDetails', label: 'Personal Details' }, { value: 'other', label: lang('lng_report_reason_other') }, - ]; + ], [lang]); - if (!messageIds) { + if ( + (subject === 'messages' && !messageIds) + || (subject === 'peer' && !chatId) + || (subject === 'media' && (!chatId || !photo)) + ) { return undefined; } + const title = subject === 'messages' + ? lang('lng_report_message_title') + : lang('ReportPeer.Report'); + return ( = ({ ); }; -export default memo(ReportMessageModal); +export default memo(ReportModal); diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 1df4294bc..f33b1477b 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -50,6 +50,7 @@ import ListItem from '../../ui/ListItem'; import Badge from './Badge'; import ChatFolderModal from '../ChatFolderModal.async'; import ChatCallStatus from './ChatCallStatus'; +import ReportModal from '../../common/ReportModal'; import FakeIcon from '../../common/FakeIcon'; import './Chat.scss'; @@ -116,8 +117,10 @@ const Chat: FC = ({ const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag(); + const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag(); const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag(); + const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag(); const { lastMessage, typingStatus } = chat || {}; const isAction = lastMessage && isActionMessage(lastMessage); @@ -190,21 +193,27 @@ const Chat: FC = ({ focusLastMessage, ]); - function handleDelete() { + const handleDelete = useCallback(() => { markRenderDeleteModal(); openDeleteModal(); - } + }, [markRenderDeleteModal, openDeleteModal]); - function handleChatFolderChange() { + const handleChatFolderChange = useCallback(() => { markRenderChatFolderModal(); openChatFolderModal(); - } + }, [markRenderChatFolderModal, openChatFolderModal]); + + const handleReport = useCallback(() => { + markRenderReportModal(); + openReportModal(); + }, [markRenderReportModal, openReportModal]); const contextActions = useChatContextActions({ chat, user, handleDelete, handleChatFolderChange, + handleReport, folderId, isPinned, isMuted, @@ -330,6 +339,15 @@ const Chat: FC = ({ chatId={chatId} /> )} + {shouldRenderReportModal && ( + + )} ); }; diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 24d6bbaf9..15970052e 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -16,6 +16,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck' import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useMedia from '../../hooks/useMedia'; +import useFlag from '../../hooks/useFlag'; import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress'; import usePrevious from '../../hooks/usePrevious'; import { @@ -40,6 +41,7 @@ import { selectChatMessage, selectChatMessages, selectCurrentMediaSearch, + selectIsChatWithSelf, selectListedIds, selectOutlyingIds, selectScheduledMessage, @@ -63,6 +65,7 @@ import PanZoom from './PanZoom'; import SenderInfo from './SenderInfo'; import SlideTransition from './SlideTransition'; import ZoomControls from './ZoomControls'; +import ReportModal from '../common/ReportModal'; import './MediaViewer.scss'; @@ -71,6 +74,7 @@ type StateProps = { threadId?: number; messageId?: number; senderId?: string; + isChatWithSelf?: boolean; origin?: MediaViewerOrigin; avatarOwner?: ApiChat | ApiUser; profilePhotoIndex?: number; @@ -87,6 +91,7 @@ const MediaViewer: FC = ({ threadId, messageId, senderId, + isChatWithSelf, origin, avatarOwner, profilePhotoIndex, @@ -144,6 +149,7 @@ const MediaViewer: FC = ({ const isGhostAnimation = animationLevel === 2; /* Controls */ + const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); const [canPanZoomWrap, setCanPanZoomWrap] = useState(false); const [isZoomed, setIsZoomed] = useState(false); const [zoomLevel, setZoomLevel] = useState(1); @@ -186,6 +192,8 @@ const MediaViewer: FC = ({ undefined, isGhostAnimation && ANIMATION_DURATION, ); + const avatarPhoto = avatarOwner?.photos?.[profilePhotoIndex!]; + const canReport = !!avatarPhoto && profilePhotoIndex! > 0 && !isChatWithSelf; const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined; let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl; @@ -483,11 +491,20 @@ const MediaViewer: FC = ({ isZoomed={isZoomed} message={message} fileName={fileName} + canReport={canReport} + onReport={openReportModal} onCloseMediaViewer={close} onForward={handleForward} onZoomToggle={handleZoomToggle} isAvatar={isAvatar} /> + = ({ isAvatar, isDownloading, isProtected, + canReport, + onReport, onCloseMediaViewer, onForward, onZoomToggle, @@ -154,6 +158,14 @@ const MediaViewerActions: FC = ({ {lang('AccActionDownload')} )} + {canReport && ( + + {lang('ReportPeer.Report')} + + )} {isDownloading && } @@ -183,6 +195,17 @@ const MediaViewerActions: FC = ({ > + {canReport && ( + + )}