Message: Add ability to report bad messages (#1259)

This commit is contained in:
Alexander Zinchuk 2021-07-13 17:31:23 +03:00
parent d4ad38bd07
commit c3d6285795
20 changed files with 290 additions and 34 deletions

View File

@ -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",

View File

@ -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;
}

View File

@ -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 {

View File

@ -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,
}: {

View File

@ -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

View File

@ -392,6 +392,7 @@ export type ApiUpdateServerTimeOffset = {
serverTimeOffset: number;
};
export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |

View File

@ -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<GlobalActions, 'reportMessages' | 'exitMessageSelectMode'>;
const ReportMessageModal: FC<OwnProps & DispatchProps> = ({
isOpen,
messageIds,
reportMessages,
exitMessageSelectMode,
onClose,
}) => {
const [selectedReason, setSelectedReason] = useState<ApiReportReason>('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<HTMLInputElement>) => {
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 (
<Modal
isOpen={isOpen}
onClose={onClose}
onEnter={isOpen ? handleReport : undefined}
className="report"
title={lang('lng_report_message_title')}
>
<RadioGroup
name="report-message"
options={REPORT_OPTIONS}
onChange={handleSelectReason}
selected={selectedReason}
/>
<InputText
label={lang('lng_report_reason_description')}
value={description}
onChange={handleDescriptionChange}
/>
<Button color="danger" className="confirm-dialog-button" isText onClick={handleReport}>
{lang('lng_report_button')}
</Button>
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
undefined, (setGlobal, actions): DispatchProps => pick(actions, [
'reportMessages', 'exitMessageSelectMode',
]),
)(ReportMessageModal));

View File

@ -34,7 +34,7 @@ type StateProps = {
type DispatchProps = Pick<GlobalActions, 'deleteMessages' | 'exitMessageSelectMode' | 'deleteScheduledMessages'>;
const DeleteSelectedMessagesModal: FC<OwnProps & StateProps & DispatchProps> = ({
const DeleteSelectedMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
isOpen,
isSchedule,
selectedMessageIds,
@ -127,4 +127,4 @@ export default memo(withGlobal<OwnProps>(
'deleteScheduledMessages',
'exitMessageSelectMode',
]),
)(DeleteSelectedMessagesModal));
)(DeleteSelectedMessageModal));

View File

@ -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;
}
}
}

View File

@ -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<GlobalActions, 'exitMessageSelectMode' | 'openForwardMenuForSelectedMessages'>;
@ -43,20 +47,23 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
</span>
</MenuItem>
)}
{canReportMessages && (
<MenuItem
icon="flag"
onClick={openReportModal}
disabled={!canReportMessages}
ariaLabel={lang('Conversation.ReportMessages')}
>
<span className="item-text">
{lang('Report')}
</span>
</MenuItem>
)}
<MenuItem
destructive
icon="delete"
@ -113,11 +132,16 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
</div>
)}
</div>
<DeleteSelectedMessagesModal
<DeleteSelectedMessageModal
isOpen={isDeleteModalOpen}
isSchedule={isSchedule}
onClose={closeDeleteModal}
/>
<ReportMessageModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
messageIds={selectedMessageIds}
/>
</div>
);
};
@ -126,11 +150,15 @@ export default memo(withGlobal<OwnProps>(
(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']),

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
canPin,
canUnpin,
canDelete,
canReport,
canEdit,
canForward,
canFaveSticker,
@ -87,6 +90,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
setIsDeleteModalOpen(true);
}, []);
const handleReport = useCallback(() => {
setIsMenuOpen(false);
setIsReportModalOpen(true);
}, []);
const closeMenu = useCallback(() => {
setIsMenuOpen(false);
onClose();
@ -105,6 +114,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
onClose();
}, [onClose]);
const closeReportModal = useCallback(() => {
setIsReportModalOpen(false);
onClose();
}, [onClose]);
const closePinModal = useCallback(() => {
setIsPinModalOpen(false);
onClose();
@ -200,6 +214,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
return enableScrolling;
}, []);
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
if (noOptions) {
closeMenu();
@ -219,6 +235,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
canReschedule={canReschedule}
canReply={canReply}
canDelete={canDelete}
canReport={canReport}
canPin={canPin}
canUnpin={canUnpin}
canEdit={canEdit}
@ -234,6 +251,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
onUnpin={handleUnpin}
onForward={handleForward}
onDelete={handleDelete}
onReport={handleReport}
onFaveSticker={handleFaveSticker}
onUnfaveSticker={handleUnfaveSticker}
onSelect={handleSelectMessage}
@ -249,6 +267,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
album={album}
message={message}
/>
<ReportMessageModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
messageIds={reportMessageIds}
/>
<PinMessageModal
isOpen={isPinModalOpen}
messageId={message.id}
@ -277,6 +300,7 @@ export default memo(withGlobal<OwnProps>(
canPin,
canUnpin,
canDelete,
canReport,
canEdit,
canForward,
canFaveSticker,
@ -296,6 +320,7 @@ export default memo(withGlobal<OwnProps>(
canPin: !isScheduled && canPin,
canUnpin: !isScheduled && canUnpin,
canDelete,
canReport,
canEdit: !isPinned && canEdit,
canForward: !isScheduled && canForward,
canFaveSticker: !isScheduled && canFaveSticker,

View File

@ -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<OwnProps> = ({
canPin,
canUnpin,
canDelete,
canReport,
canForward,
canFaveSticker,
canUnfaveSticker,
@ -70,6 +73,7 @@ const MessageContextMenu: FC<OwnProps> = ({
onUnpin,
onForward,
onDelete,
onReport,
onFaveSticker,
onUnfaveSticker,
onSelect,
@ -135,6 +139,7 @@ const MessageContextMenu: FC<OwnProps> = ({
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
</Menu>
);

View File

@ -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';
}

View File

@ -14,6 +14,12 @@
}
}
&.report {
.modal-dialog {
max-width: 15rem;
}
}
.modal-container {
position: fixed;
top: 0;

View File

@ -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

View File

@ -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<MessageEntity> 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<MessageEntity> 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<int> random_id:Vector<long> to_peer:InputPeer schedule_date:flags.10?int = Updates;
messages.report#8953ab4e peer:InputPeer id:Vector<int> reason:ReportReason message:string = Bool;
messages.getChats#3c6aa187 id:Vector<int> = messages.Chats;
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;

View File

@ -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<MessageEntity> 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<MessageEntity> 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<int> random_id:Vector<long> to_peer:InputPeer schedule_date:flags.10?int = Updates;
messages.report#8953ab4e peer:InputPeer id:Vector<int> reason:ReportReason message:string = Bool;
messages.getChats#3c6aa187 id:Vector<int> = messages.Chats;
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;

View File

@ -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);

View File

@ -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];

View File

@ -15,12 +15,27 @@ const IGNORED_KEYS: Record<string, boolean> = {
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);
}
}