diff --git a/package.json b/package.json index 2fb7605d6..2c77a0d37 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ } }, "lint-staged": { - "*.{ts,tsx,js}": "eslint" + "*.{ts,tsx}": "eslint" }, "author": "Alexander Zinchuk (alexander@zinchuk.com)", "license": "GPL-3.0-or-later", diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 4a378896d..c5d817897 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -13,6 +13,7 @@ import { ApiChatFolder, ApiChatBannedRights, ApiChatAdminRights, + ApiReportReason, } from '../../types'; import localDb from '../localDb'; import { pick } from '../../../util/iteratees'; @@ -382,3 +383,26 @@ export function buildInputPrivacyKey(privacyKey: ApiPrivacyKey) { return undefined; } + +export function buildInputReportReason(reason: ApiReportReason) { + switch (reason) { + case 'spam': + return new GramJs.InputReportReasonSpam(); + case 'violence': + return new GramJs.InputReportReasonViolence(); + case 'childAbuse': + return new GramJs.InputReportReasonChildAbuse(); + case 'pornography': + return new GramJs.InputReportReasonPornography(); + case 'copyright': + return new GramJs.InputReportReasonCopyright(); + case 'fake': + return new GramJs.InputReportReasonFake(); + case 'geoIrrelevant': + return new GramJs.InputReportReasonGeoIrrelevant(); + case 'other': + return new GramJs.InputReportReasonOther(); + } + + return undefined; +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 3d9941646..2cd0236cf 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -22,7 +22,7 @@ export { markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal, fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate, fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, - fetchMessageLink, + fetchMessageLink, reportMessages, } from './messages'; export { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 685b5e74a..809543475 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -15,6 +15,7 @@ import { MAIN_THREAD_ID, MESSAGE_DELETED, ApiGlobalMessageSearchType, + ApiReportReason, } from '../../types'; import { ALL_FOLDER_ID, DEBUG, PINNED_MESSAGES_LIMIT } from '../../../config'; @@ -37,6 +38,7 @@ import { isMessageWithMedia, isServiceMessageWithMedia, calculateResultHash, + buildInputReportReason, } from '../gramjsBuilders'; import localDb from '../localDb'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; @@ -616,6 +618,21 @@ export async function deleteHistory({ }); } +export async function reportMessages({ + peer, messageIds, reason, description, +}: { + peer: ApiChat | ApiUser; messageIds: number[]; reason: ApiReportReason; description?: string; +}) { + const result = await invokeRequest(new GramJs.messages.Report({ + peer: buildInputPeer(peer.id, peer.accessHash), + id: messageIds, + reason: buildInputReportReason(reason), + message: description, + })); + + return result; +} + export async function markMessageListRead({ chat, threadId, maxId, serverTimeOffset, }: { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 15e527f70..b13f9078e 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -275,6 +275,9 @@ export type ApiKeyboardButtons = ApiKeyboardButton[][]; export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'profilePhoto'; export type ApiGlobalMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice'; +export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse' +| 'copyright' | 'geoIrrelevant' | 'fake' | 'other'; + export const MAIN_THREAD_ID = -1; // `Symbol` can not be transferred from worker diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 5e71eeb13..5aaaf1941 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -392,6 +392,7 @@ export type ApiUpdateServerTimeOffset = { serverTimeOffset: number; }; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | diff --git a/src/components/common/ReportMessageModal.tsx b/src/components/common/ReportMessageModal.tsx new file mode 100644 index 000000000..592f0dce1 --- /dev/null +++ b/src/components/common/ReportMessageModal.tsx @@ -0,0 +1,98 @@ +import { ChangeEvent } from 'react'; + +import React, { + FC, memo, useCallback, useState, +} from '../../lib/teact/teact'; +import { withGlobal } from '../../lib/teact/teactn'; + +import { ApiReportReason } from '../../api/types'; + +import { GlobalActions } from '../../global/types'; + +import { pick } from '../../util/iteratees'; +import useLang from '../../hooks/useLang'; + +import Modal from '../ui/Modal'; +import Button from '../ui/Button'; +import RadioGroup from '../ui/RadioGroup'; +import InputText from '../ui/InputText'; + +export type OwnProps = { + isOpen: boolean; + messageIds?: number[]; + onClose: () => void; +}; + +type DispatchProps = Pick; + +const ReportMessageModal: FC = ({ + isOpen, + messageIds, + reportMessages, + exitMessageSelectMode, + onClose, +}) => { + const [selectedReason, setSelectedReason] = useState('spam'); + const [description, setDescription] = useState(''); + + const handleReport = () => { + reportMessages({ messageIds, reason: selectedReason, description }); + exitMessageSelectMode(); + onClose(); + }; + + const handleSelectReason = useCallback((value: string) => { + setSelectedReason(value as ApiReportReason); + }, []); + + const handleDescriptionChange = useCallback((e: ChangeEvent) => { + setDescription(e.target.value); + }, []); + + const lang = useLang(); + + const REPORT_OPTIONS: {value: ApiReportReason; label: string}[] = [ + { value: 'spam', label: lang('lng_report_reason_spam') }, + { value: 'violence', label: lang('lng_report_reason_violence') }, + { value: 'pornography', label: lang('lng_report_reason_pornography') }, + { value: 'childAbuse', label: lang('lng_report_reason_child_abuse') }, + { value: 'copyright', label: lang('ReportPeer.ReasonCopyright') }, + { value: 'other', label: lang('lng_report_reason_other') }, + ]; + + if (!messageIds) { + return undefined; + } + + return ( + + + + + + + ); +}; + +export default memo(withGlobal( + undefined, (setGlobal, actions): DispatchProps => pick(actions, [ + 'reportMessages', 'exitMessageSelectMode', + ]), +)(ReportMessageModal)); diff --git a/src/components/middle/DeleteSelectedMessagesModal.tsx b/src/components/middle/DeleteSelectedMessageModal.tsx similarity index 97% rename from src/components/middle/DeleteSelectedMessagesModal.tsx rename to src/components/middle/DeleteSelectedMessageModal.tsx index 11a755598..7ad3c8ed5 100644 --- a/src/components/middle/DeleteSelectedMessagesModal.tsx +++ b/src/components/middle/DeleteSelectedMessageModal.tsx @@ -34,7 +34,7 @@ type StateProps = { type DispatchProps = Pick; -const DeleteSelectedMessagesModal: FC = ({ +const DeleteSelectedMessageModal: FC = ({ isOpen, isSchedule, selectedMessageIds, @@ -127,4 +127,4 @@ export default memo(withGlobal( 'deleteScheduledMessages', 'exitMessageSelectMode', ]), -)(DeleteSelectedMessagesModal)); +)(DeleteSelectedMessageModal)); diff --git a/src/components/middle/MessageSelectToolbar.scss b/src/components/middle/MessageSelectToolbar.scss index 00fccc111..d26626820 100644 --- a/src/components/middle/MessageSelectToolbar.scss +++ b/src/components/middle/MessageSelectToolbar.scss @@ -1,8 +1,8 @@ .MessageSelectToolbar { position: absolute; bottom: 0.5rem; - left: .5rem; - right: .5rem; + left: 0.5rem; + right: 0.5rem; width: auto; z-index: 20; justify-content: center; @@ -105,6 +105,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + flex-grow: 1; @media (max-width: 600px) { margin-right: 0.5rem; @@ -116,24 +117,15 @@ display: flex; .MenuItem { - border-radius: var(--border-radius-default); - padding: 0.75rem 2rem 0.75rem 1rem; + padding: 0.6875rem; + border-radius: 50%; i { - margin-right: 1rem; + margin-right: 0; } - @media (max-width: 600px) { - padding: 0.6875rem; - border-radius: 50%; - - i { - margin-right: 0; - } - - .item-text { - display: none; - } + .item-text { + display: none; } } } diff --git a/src/components/middle/MessageSelectToolbar.tsx b/src/components/middle/MessageSelectToolbar.tsx index 487fc5b7e..abe9dcb5a 100644 --- a/src/components/middle/MessageSelectToolbar.tsx +++ b/src/components/middle/MessageSelectToolbar.tsx @@ -5,6 +5,7 @@ import { GlobalActions, MessageListType } from '../../global/types'; import { selectCanDeleteSelectedMessages, + selectCanReportSelectedMessages, selectCurrentMessageList, selectSelectedMessagesCount, } from '../../modules/selectors'; @@ -18,7 +19,8 @@ import useLang from '../../hooks/useLang'; import Button from '../ui/Button'; import MenuItem from '../ui/MenuItem'; -import DeleteSelectedMessagesModal from './DeleteSelectedMessagesModal'; +import DeleteSelectedMessageModal from './DeleteSelectedMessageModal'; +import ReportMessageModal from '../common/ReportMessageModal'; import './MessageSelectToolbar.scss'; @@ -32,6 +34,8 @@ type StateProps = { isSchedule: boolean; selectedMessagesCount?: number; canDeleteMessages?: boolean; + canReportMessages?: boolean; + selectedMessageIds?: number[]; }; type DispatchProps = Pick; @@ -43,20 +47,23 @@ const MessageSelectToolbar: FC = ({ isSchedule, selectedMessagesCount, canDeleteMessages, + canReportMessages, + selectedMessageIds, exitMessageSelectMode, openForwardMenuForSelectedMessages, }) => { const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); + const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); useEffect(() => { - return isActive && !isDeleteModalOpen + return isActive && !isDeleteModalOpen && !isReportModalOpen ? captureKeyboardListeners({ onBackspace: openDeleteModal, onDelete: openDeleteModal, onEsc: exitMessageSelectMode, }) : undefined; - }, [isActive, isDeleteModalOpen, openDeleteModal, exitMessageSelectMode]); + }, [isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode]); const prevSelectedMessagesCount = usePrevious(selectedMessagesCount || undefined, true); const renderingSelectedMessagesCount = isActive ? selectedMessagesCount : prevSelectedMessagesCount; @@ -99,6 +106,18 @@ const MessageSelectToolbar: FC = ({ )} + {canReportMessages && ( + + + {lang('Report')} + + + )} = ({ )} - + ); }; @@ -126,11 +150,15 @@ export default memo(withGlobal( (global): StateProps => { const { type: messageListType } = selectCurrentMessageList(global) || {}; const { canDelete } = selectCanDeleteSelectedMessages(global); + const canReport = selectCanReportSelectedMessages(global); + const { messageIds: selectedMessageIds } = global.selectedMessages || {}; return { isSchedule: messageListType === 'scheduled', selectedMessagesCount: selectSelectedMessagesCount(global), canDeleteMessages: canDelete, + canReportMessages: canReport, + selectedMessageIds, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['exitMessageSelectMode', 'openForwardMenuForSelectedMessages']), diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 1ab90c170..7817ac1e4 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -1,5 +1,5 @@ import React, { - FC, memo, useCallback, useEffect, useState, + FC, memo, useCallback, useEffect, useMemo, useState, } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; @@ -13,6 +13,7 @@ import useShowTransition from '../../../hooks/useShowTransition'; import useFlag from '../../../hooks/useFlag'; import DeleteMessageModal from '../../common/DeleteMessageModal'; +import ReportMessageModal from '../../common/ReportMessageModal'; import PinMessageModal from '../../common/PinMessageModal'; import MessageContextMenu from './MessageContextMenu'; import CalendarModal from '../../common/CalendarModal'; @@ -36,6 +37,7 @@ type StateProps = { canPin?: boolean; canUnpin?: boolean; canDelete?: boolean; + canReport?: boolean; canEdit?: boolean; canForward?: boolean; canFaveSticker?: boolean; @@ -66,6 +68,7 @@ const ContextMenuContainer: FC = ({ canPin, canUnpin, canDelete, + canReport, canEdit, canForward, canFaveSticker, @@ -87,6 +90,7 @@ const ContextMenuContainer: FC = ({ const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false); const [isMenuOpen, setIsMenuOpen] = useState(true); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isReportModalOpen, setIsReportModalOpen] = useState(false); const [isPinModalOpen, setIsPinModalOpen] = useState(false); const [isCalendarOpen, openCalendar, closeCalendar] = useFlag(); @@ -95,6 +99,11 @@ const ContextMenuContainer: FC = ({ setIsDeleteModalOpen(true); }, []); + const handleReport = useCallback(() => { + setIsMenuOpen(false); + setIsReportModalOpen(true); + }, []); + const closeMenu = useCallback(() => { setIsMenuOpen(false); onClose(); @@ -105,6 +114,11 @@ const ContextMenuContainer: FC = ({ onClose(); }, [onClose]); + const closeReportModal = useCallback(() => { + setIsReportModalOpen(false); + onClose(); + }, [onClose]); + const closePinModal = useCallback(() => { setIsPinModalOpen(false); onClose(); @@ -200,6 +214,8 @@ const ContextMenuContainer: FC = ({ return enableScrolling; }, []); + const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]); + if (noOptions) { closeMenu(); @@ -219,6 +235,7 @@ const ContextMenuContainer: FC = ({ canReschedule={canReschedule} canReply={canReply} canDelete={canDelete} + canReport={canReport} canPin={canPin} canUnpin={canUnpin} canEdit={canEdit} @@ -234,6 +251,7 @@ const ContextMenuContainer: FC = ({ onUnpin={handleUnpin} onForward={handleForward} onDelete={handleDelete} + onReport={handleReport} onFaveSticker={handleFaveSticker} onUnfaveSticker={handleUnfaveSticker} onSelect={handleSelectMessage} @@ -249,6 +267,11 @@ const ContextMenuContainer: FC = ({ album={album} message={message} /> + ( canPin, canUnpin, canDelete, + canReport, canEdit, canForward, canFaveSticker, @@ -296,6 +320,7 @@ export default memo(withGlobal( canPin: !isScheduled && canPin, canUnpin: !isScheduled && canUnpin, canDelete, + canReport, canEdit: !isPinned && canEdit, canForward: !isScheduled && canForward, canFaveSticker: !isScheduled && canFaveSticker, diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index e10907af2..fc9e4d5c2 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -22,6 +22,7 @@ type OwnProps = { canPin?: boolean; canUnpin?: boolean; canDelete?: boolean; + canReport?: boolean; canEdit?: boolean; canForward?: boolean; canFaveSticker?: boolean; @@ -35,6 +36,7 @@ type OwnProps = { onUnpin: () => void; onForward: () => void; onDelete: () => void; + onReport: () => void; onFaveSticker: () => void; onUnfaveSticker: () => void; onSelect: () => void; @@ -58,6 +60,7 @@ const MessageContextMenu: FC = ({ canPin, canUnpin, canDelete, + canReport, canForward, canFaveSticker, canUnfaveSticker, @@ -70,6 +73,7 @@ const MessageContextMenu: FC = ({ onUnpin, onForward, onDelete, + onReport, onFaveSticker, onUnfaveSticker, onSelect, @@ -135,6 +139,7 @@ const MessageContextMenu: FC = ({ {canUnpin && {lang('DialogUnpin')}} {canForward && {lang('Forward')}} {canSelect && {lang('Common.Select')}} + {canReport && {lang('lng_context_report_msg')}} {canDelete && {lang('Delete')}} ); diff --git a/src/components/middle/message/helpers/copyOptions.ts b/src/components/middle/message/helpers/copyOptions.ts index 0314e176e..79d922737 100644 --- a/src/components/middle/message/helpers/copyOptions.ts +++ b/src/components/middle/message/helpers/copyOptions.ts @@ -43,7 +43,7 @@ export function getMessageCopyOptions( )); options.push({ - label: getCopyLabel(hasSelection, canImageBeCopied), + label: getCopyLabel(hasSelection), handler: () => { const clipboardText = hasSelection && selection ? selection.toString() : text; copyTextToClipboard(clipboardText); @@ -71,14 +71,9 @@ export function getMessageCopyOptions( return options; } -function getCopyLabel(hasSelection: boolean, canImageBeCopied: boolean): string { +function getCopyLabel(hasSelection: boolean): string { if (hasSelection) { return 'lng_context_copy_selected'; } - - if (canImageBeCopied) { - return 'lng_context_copy_text'; - } - - return 'Copy'; + return 'lng_context_copy_text'; } diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index f2ccf224e..66f5c4b32 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -14,6 +14,12 @@ } } + &.report { + .modal-dialog { + max-width: 15rem; + } + } + .modal-container { position: fixed; top: 0; diff --git a/src/global/types.ts b/src/global/types.ts index e43d5f40d..3d3a89821 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -422,6 +422,7 @@ export type ActionTypes = ( 'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' | 'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' | 'loadMessageLink' | 'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' | + 'reportMessages' | // scheduled messages 'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' | // poll result diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index d1147c86e..09ccefc72 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -967,6 +967,7 @@ messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action messages.sendMessage#520c3870 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; messages.sendMedia#3491eba9 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; messages.forwardMessages#d9fee60e flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int = Updates; +messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; messages.getChats#3c6aa187 id:Vector = messages.Chats; messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull; messages.editChatTitle#dc452855 chat_id:int title:string = Updates; diff --git a/src/lib/gramjs/tl/static/api.reduced.tl b/src/lib/gramjs/tl/static/api.reduced.tl index 9c0f229aa..2fb5a3ecb 100644 --- a/src/lib/gramjs/tl/static/api.reduced.tl +++ b/src/lib/gramjs/tl/static/api.reduced.tl @@ -967,6 +967,7 @@ messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action messages.sendMessage#520c3870 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; messages.sendMedia#3491eba9 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; messages.forwardMessages#d9fee60e flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int = Updates; +messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; messages.getChats#3c6aa187 id:Vector = messages.Chats; messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull; messages.editChatTitle#dc452855 chat_id:int title:string = Updates; diff --git a/src/modules/actions/api/messages.ts b/src/modules/actions/api/messages.ts index f77b5d3fe..d45de56ac 100644 --- a/src/modules/actions/api/messages.ts +++ b/src/modules/actions/api/messages.ts @@ -415,6 +415,31 @@ addReducer('deleteHistory', (global, actions, payload) => { })(); }); +addReducer('reportMessages', (global, actions, payload) => { + (async () => { + const { + messageIds, reason, description, + } = payload!; + const currentMessageList = selectCurrentMessageList(global); + if (!currentMessageList) { + return; + } + + const { chatId } = currentMessageList; + const chat = selectChat(global, chatId)!; + + const result = await callApi('reportMessages', { + peer: chat, messageIds, reason, description, + }); + + actions.showNotification({ + message: result + ? 'Thank you! Your report will be reviewed by our team.' + : 'Error occured while submiting report. Please, try again later.', + }); + })(); +}); + addReducer('markMessageListRead', (global, actions, payload) => { const { serverTimeOffset } = global; const currentMessageList = selectCurrentMessageList(global); diff --git a/src/modules/selectors/messages.ts b/src/modules/selectors/messages.ts index 7f4dbaf9b..241428319 100644 --- a/src/modules/selectors/messages.ts +++ b/src/modules/selectors/messages.ts @@ -386,6 +386,8 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes || chat.isCreator || getHasAdminRight(chat, 'deleteMessages'); + const canReport = !isPrivate && !isOwn; + const canDeleteForAll = canDelete && !isServiceNotification && ( (isPrivate && !isChatWithSelf) || (isBasicGroup && ( @@ -429,6 +431,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes canPin, canUnpin, canDelete, + canReport, canDeleteForAll, canForward, canFaveSticker, @@ -439,6 +442,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes }; } +// This selector always returns a new object which can not be safely used in shallow-equal checks export function selectCanDeleteSelectedMessages(global: GlobalState) { const { messageIds: selectedMessageIds } = global.selectedMessages || {}; const { chatId, threadId } = selectCurrentMessageList(global) || {}; @@ -457,6 +461,21 @@ export function selectCanDeleteSelectedMessages(global: GlobalState) { }; } +export function selectCanReportSelectedMessages(global: GlobalState) { + const { messageIds: selectedMessageIds } = global.selectedMessages || {}; + const { chatId, threadId } = selectCurrentMessageList(global) || {}; + const chatMessages = chatId && selectChatMessages(global, chatId); + if (!chatMessages || !selectedMessageIds || !threadId) { + return false; + } + + const messageActions = selectedMessageIds + .map((id) => chatMessages[id] && selectAllowedMessageActions(global, chatMessages[id], threadId)) + .filter(Boolean); + + return messageActions.every((actions) => actions.canReport); +} + export function selectUploadProgress(global: GlobalState, message: ApiMessage) { const fileTransfer = global.fileUploads.byMessageLocalId[message.previousLocalId || message.id]; diff --git a/src/util/scrollLock.ts b/src/util/scrollLock.ts index aa2b716e8..5cfd78844 100644 --- a/src/util/scrollLock.ts +++ b/src/util/scrollLock.ts @@ -15,12 +15,27 @@ const IGNORED_KEYS: Record = { Tab: true, }; +function isTextBox(target: EventTarget | null) { + if (!target || !(target instanceof HTMLElement)) return false; + const element = target; + const tagName = element.tagName.toLowerCase(); + if (tagName === 'textarea') return true; + if (tagName !== 'input') return false; + const type = element.getAttribute('type'); + if (!type) return false; + const inputTypes = [ + 'text', 'password', 'number', 'email', 'tel', 'url', + 'search', 'date', 'datetime', 'datetime-local', 'time', 'month', 'week', + ]; + return inputTypes.indexOf(type.toLowerCase()) > -1; +} + const preventDefault = (e: Event) => { e.preventDefault(); }; function preventDefaultForScrollKeys(e: KeyboardEvent) { - if (IGNORED_KEYS[e.key]) { + if (IGNORED_KEYS[e.key] && !isTextBox(e.target)) { preventDefault(e); } }