Message: Add ability to report bad messages (#1259)
This commit is contained in:
parent
d4ad38bd07
commit
c3d6285795
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -392,6 +392,7 @@ export type ApiUpdateServerTimeOffset = {
|
||||
serverTimeOffset: number;
|
||||
};
|
||||
|
||||
|
||||
export type ApiUpdate = (
|
||||
ApiUpdateReady | ApiUpdateSession |
|
||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||
|
||||
98
src/components/common/ReportMessageModal.tsx
Normal file
98
src/components/common/ReportMessageModal.tsx
Normal 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));
|
||||
@ -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));
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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']),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -14,6 +14,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.report {
|
||||
.modal-dialog {
|
||||
max-width: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user