Message: Add ability to report bad messages (#1259)
This commit is contained in:
parent
d4ad38bd07
commit
c3d6285795
@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js}": "eslint"
|
"*.{ts,tsx}": "eslint"
|
||||||
},
|
},
|
||||||
"author": "Alexander Zinchuk (alexander@zinchuk.com)",
|
"author": "Alexander Zinchuk (alexander@zinchuk.com)",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
ApiChatFolder,
|
ApiChatFolder,
|
||||||
ApiChatBannedRights,
|
ApiChatBannedRights,
|
||||||
ApiChatAdminRights,
|
ApiChatAdminRights,
|
||||||
|
ApiReportReason,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
@ -382,3 +383,26 @@ export function buildInputPrivacyKey(privacyKey: ApiPrivacyKey) {
|
|||||||
|
|
||||||
return undefined;
|
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,
|
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
||||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
fetchMessageLink,
|
fetchMessageLink, reportMessages,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
MESSAGE_DELETED,
|
MESSAGE_DELETED,
|
||||||
ApiGlobalMessageSearchType,
|
ApiGlobalMessageSearchType,
|
||||||
|
ApiReportReason,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import { ALL_FOLDER_ID, DEBUG, PINNED_MESSAGES_LIMIT } from '../../../config';
|
import { ALL_FOLDER_ID, DEBUG, PINNED_MESSAGES_LIMIT } from '../../../config';
|
||||||
@ -37,6 +38,7 @@ import {
|
|||||||
isMessageWithMedia,
|
isMessageWithMedia,
|
||||||
isServiceMessageWithMedia,
|
isServiceMessageWithMedia,
|
||||||
calculateResultHash,
|
calculateResultHash,
|
||||||
|
buildInputReportReason,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
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({
|
export async function markMessageListRead({
|
||||||
chat, threadId, maxId, serverTimeOffset,
|
chat, threadId, maxId, serverTimeOffset,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -275,6 +275,9 @@ export type ApiKeyboardButtons = ApiKeyboardButton[][];
|
|||||||
export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'profilePhoto';
|
export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'profilePhoto';
|
||||||
export type ApiGlobalMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice';
|
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;
|
export const MAIN_THREAD_ID = -1;
|
||||||
|
|
||||||
// `Symbol` can not be transferred from worker
|
// `Symbol` can not be transferred from worker
|
||||||
|
|||||||
@ -392,6 +392,7 @@ export type ApiUpdateServerTimeOffset = {
|
|||||||
serverTimeOffset: number;
|
serverTimeOffset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type ApiUpdate = (
|
export type ApiUpdate = (
|
||||||
ApiUpdateReady | ApiUpdateSession |
|
ApiUpdateReady | ApiUpdateSession |
|
||||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
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'>;
|
type DispatchProps = Pick<GlobalActions, 'deleteMessages' | 'exitMessageSelectMode' | 'deleteScheduledMessages'>;
|
||||||
|
|
||||||
const DeleteSelectedMessagesModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
const DeleteSelectedMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
isSchedule,
|
isSchedule,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
@ -127,4 +127,4 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
'deleteScheduledMessages',
|
'deleteScheduledMessages',
|
||||||
'exitMessageSelectMode',
|
'exitMessageSelectMode',
|
||||||
]),
|
]),
|
||||||
)(DeleteSelectedMessagesModal));
|
)(DeleteSelectedMessageModal));
|
||||||
@ -1,8 +1,8 @@
|
|||||||
.MessageSelectToolbar {
|
.MessageSelectToolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
left: .5rem;
|
left: 0.5rem;
|
||||||
right: .5rem;
|
right: 0.5rem;
|
||||||
width: auto;
|
width: auto;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -105,6 +105,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
@ -116,24 +117,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.MenuItem {
|
.MenuItem {
|
||||||
border-radius: var(--border-radius-default);
|
padding: 0.6875rem;
|
||||||
padding: 0.75rem 2rem 0.75rem 1rem;
|
border-radius: 50%;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
margin-right: 1rem;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
.item-text {
|
||||||
padding: 0.6875rem;
|
display: none;
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { GlobalActions, MessageListType } from '../../global/types';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
selectCanDeleteSelectedMessages,
|
selectCanDeleteSelectedMessages,
|
||||||
|
selectCanReportSelectedMessages,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectSelectedMessagesCount,
|
selectSelectedMessagesCount,
|
||||||
} from '../../modules/selectors';
|
} from '../../modules/selectors';
|
||||||
@ -18,7 +19,8 @@ import useLang from '../../hooks/useLang';
|
|||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import MenuItem from '../ui/MenuItem';
|
import MenuItem from '../ui/MenuItem';
|
||||||
|
|
||||||
import DeleteSelectedMessagesModal from './DeleteSelectedMessagesModal';
|
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
|
||||||
|
import ReportMessageModal from '../common/ReportMessageModal';
|
||||||
|
|
||||||
import './MessageSelectToolbar.scss';
|
import './MessageSelectToolbar.scss';
|
||||||
|
|
||||||
@ -32,6 +34,8 @@ type StateProps = {
|
|||||||
isSchedule: boolean;
|
isSchedule: boolean;
|
||||||
selectedMessagesCount?: number;
|
selectedMessagesCount?: number;
|
||||||
canDeleteMessages?: boolean;
|
canDeleteMessages?: boolean;
|
||||||
|
canReportMessages?: boolean;
|
||||||
|
selectedMessageIds?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'exitMessageSelectMode' | 'openForwardMenuForSelectedMessages'>;
|
type DispatchProps = Pick<GlobalActions, 'exitMessageSelectMode' | 'openForwardMenuForSelectedMessages'>;
|
||||||
@ -43,20 +47,23 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isSchedule,
|
isSchedule,
|
||||||
selectedMessagesCount,
|
selectedMessagesCount,
|
||||||
canDeleteMessages,
|
canDeleteMessages,
|
||||||
|
canReportMessages,
|
||||||
|
selectedMessageIds,
|
||||||
exitMessageSelectMode,
|
exitMessageSelectMode,
|
||||||
openForwardMenuForSelectedMessages,
|
openForwardMenuForSelectedMessages,
|
||||||
}) => {
|
}) => {
|
||||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||||
|
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return isActive && !isDeleteModalOpen
|
return isActive && !isDeleteModalOpen && !isReportModalOpen
|
||||||
? captureKeyboardListeners({
|
? captureKeyboardListeners({
|
||||||
onBackspace: openDeleteModal,
|
onBackspace: openDeleteModal,
|
||||||
onDelete: openDeleteModal,
|
onDelete: openDeleteModal,
|
||||||
onEsc: exitMessageSelectMode,
|
onEsc: exitMessageSelectMode,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
}, [isActive, isDeleteModalOpen, openDeleteModal, exitMessageSelectMode]);
|
}, [isActive, isDeleteModalOpen, isReportModalOpen, openDeleteModal, exitMessageSelectMode]);
|
||||||
|
|
||||||
const prevSelectedMessagesCount = usePrevious(selectedMessagesCount || undefined, true);
|
const prevSelectedMessagesCount = usePrevious(selectedMessagesCount || undefined, true);
|
||||||
const renderingSelectedMessagesCount = isActive ? selectedMessagesCount : prevSelectedMessagesCount;
|
const renderingSelectedMessagesCount = isActive ? selectedMessagesCount : prevSelectedMessagesCount;
|
||||||
@ -99,6 +106,18 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{canReportMessages && (
|
||||||
|
<MenuItem
|
||||||
|
icon="flag"
|
||||||
|
onClick={openReportModal}
|
||||||
|
disabled={!canReportMessages}
|
||||||
|
ariaLabel={lang('Conversation.ReportMessages')}
|
||||||
|
>
|
||||||
|
<span className="item-text">
|
||||||
|
{lang('Report')}
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
destructive
|
destructive
|
||||||
icon="delete"
|
icon="delete"
|
||||||
@ -113,11 +132,16 @@ const MessageSelectToolbar: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DeleteSelectedMessagesModal
|
<DeleteSelectedMessageModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
isSchedule={isSchedule}
|
isSchedule={isSchedule}
|
||||||
onClose={closeDeleteModal}
|
onClose={closeDeleteModal}
|
||||||
/>
|
/>
|
||||||
|
<ReportMessageModal
|
||||||
|
isOpen={isReportModalOpen}
|
||||||
|
onClose={closeReportModal}
|
||||||
|
messageIds={selectedMessageIds}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -126,11 +150,15 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const { type: messageListType } = selectCurrentMessageList(global) || {};
|
const { type: messageListType } = selectCurrentMessageList(global) || {};
|
||||||
const { canDelete } = selectCanDeleteSelectedMessages(global);
|
const { canDelete } = selectCanDeleteSelectedMessages(global);
|
||||||
|
const canReport = selectCanReportSelectedMessages(global);
|
||||||
|
const { messageIds: selectedMessageIds } = global.selectedMessages || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSchedule: messageListType === 'scheduled',
|
isSchedule: messageListType === 'scheduled',
|
||||||
selectedMessagesCount: selectSelectedMessagesCount(global),
|
selectedMessagesCount: selectSelectedMessagesCount(global),
|
||||||
canDeleteMessages: canDelete,
|
canDeleteMessages: canDelete,
|
||||||
|
canReportMessages: canReport,
|
||||||
|
selectedMessageIds,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, ['exitMessageSelectMode', 'openForwardMenuForSelectedMessages']),
|
(setGlobal, actions): DispatchProps => pick(actions, ['exitMessageSelectMode', 'openForwardMenuForSelectedMessages']),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useEffect, useState,
|
FC, memo, useCallback, useEffect, useMemo, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../lib/teact/teactn';
|
import { withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ import useShowTransition from '../../../hooks/useShowTransition';
|
|||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
||||||
|
import ReportMessageModal from '../../common/ReportMessageModal';
|
||||||
import PinMessageModal from '../../common/PinMessageModal';
|
import PinMessageModal from '../../common/PinMessageModal';
|
||||||
import MessageContextMenu from './MessageContextMenu';
|
import MessageContextMenu from './MessageContextMenu';
|
||||||
import CalendarModal from '../../common/CalendarModal';
|
import CalendarModal from '../../common/CalendarModal';
|
||||||
@ -36,6 +37,7 @@ type StateProps = {
|
|||||||
canPin?: boolean;
|
canPin?: boolean;
|
||||||
canUnpin?: boolean;
|
canUnpin?: boolean;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
|
canReport?: boolean;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
canForward?: boolean;
|
canForward?: boolean;
|
||||||
canFaveSticker?: boolean;
|
canFaveSticker?: boolean;
|
||||||
@ -66,6 +68,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canPin,
|
canPin,
|
||||||
canUnpin,
|
canUnpin,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canReport,
|
||||||
canEdit,
|
canEdit,
|
||||||
canForward,
|
canForward,
|
||||||
canFaveSticker,
|
canFaveSticker,
|
||||||
@ -87,6 +90,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||||
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
||||||
|
|
||||||
@ -95,6 +99,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleReport = useCallback(() => {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
setIsReportModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const closeMenu = useCallback(() => {
|
const closeMenu = useCallback(() => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
onClose();
|
onClose();
|
||||||
@ -105,6 +114,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
const closeReportModal = useCallback(() => {
|
||||||
|
setIsReportModalOpen(false);
|
||||||
|
onClose();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
const closePinModal = useCallback(() => {
|
const closePinModal = useCallback(() => {
|
||||||
setIsPinModalOpen(false);
|
setIsPinModalOpen(false);
|
||||||
onClose();
|
onClose();
|
||||||
@ -200,6 +214,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
return enableScrolling;
|
return enableScrolling;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
||||||
|
|
||||||
if (noOptions) {
|
if (noOptions) {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|
||||||
@ -219,6 +235,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canReschedule={canReschedule}
|
canReschedule={canReschedule}
|
||||||
canReply={canReply}
|
canReply={canReply}
|
||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
|
canReport={canReport}
|
||||||
canPin={canPin}
|
canPin={canPin}
|
||||||
canUnpin={canUnpin}
|
canUnpin={canUnpin}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
@ -234,6 +251,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
onUnpin={handleUnpin}
|
onUnpin={handleUnpin}
|
||||||
onForward={handleForward}
|
onForward={handleForward}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onReport={handleReport}
|
||||||
onFaveSticker={handleFaveSticker}
|
onFaveSticker={handleFaveSticker}
|
||||||
onUnfaveSticker={handleUnfaveSticker}
|
onUnfaveSticker={handleUnfaveSticker}
|
||||||
onSelect={handleSelectMessage}
|
onSelect={handleSelectMessage}
|
||||||
@ -249,6 +267,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
album={album}
|
album={album}
|
||||||
message={message}
|
message={message}
|
||||||
/>
|
/>
|
||||||
|
<ReportMessageModal
|
||||||
|
isOpen={isReportModalOpen}
|
||||||
|
onClose={closeReportModal}
|
||||||
|
messageIds={reportMessageIds}
|
||||||
|
/>
|
||||||
<PinMessageModal
|
<PinMessageModal
|
||||||
isOpen={isPinModalOpen}
|
isOpen={isPinModalOpen}
|
||||||
messageId={message.id}
|
messageId={message.id}
|
||||||
@ -277,6 +300,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canPin,
|
canPin,
|
||||||
canUnpin,
|
canUnpin,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canReport,
|
||||||
canEdit,
|
canEdit,
|
||||||
canForward,
|
canForward,
|
||||||
canFaveSticker,
|
canFaveSticker,
|
||||||
@ -296,6 +320,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canPin: !isScheduled && canPin,
|
canPin: !isScheduled && canPin,
|
||||||
canUnpin: !isScheduled && canUnpin,
|
canUnpin: !isScheduled && canUnpin,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canReport,
|
||||||
canEdit: !isPinned && canEdit,
|
canEdit: !isPinned && canEdit,
|
||||||
canForward: !isScheduled && canForward,
|
canForward: !isScheduled && canForward,
|
||||||
canFaveSticker: !isScheduled && canFaveSticker,
|
canFaveSticker: !isScheduled && canFaveSticker,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type OwnProps = {
|
|||||||
canPin?: boolean;
|
canPin?: boolean;
|
||||||
canUnpin?: boolean;
|
canUnpin?: boolean;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
|
canReport?: boolean;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
canForward?: boolean;
|
canForward?: boolean;
|
||||||
canFaveSticker?: boolean;
|
canFaveSticker?: boolean;
|
||||||
@ -35,6 +36,7 @@ type OwnProps = {
|
|||||||
onUnpin: () => void;
|
onUnpin: () => void;
|
||||||
onForward: () => void;
|
onForward: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
onReport: () => void;
|
||||||
onFaveSticker: () => void;
|
onFaveSticker: () => void;
|
||||||
onUnfaveSticker: () => void;
|
onUnfaveSticker: () => void;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
@ -58,6 +60,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canPin,
|
canPin,
|
||||||
canUnpin,
|
canUnpin,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canReport,
|
||||||
canForward,
|
canForward,
|
||||||
canFaveSticker,
|
canFaveSticker,
|
||||||
canUnfaveSticker,
|
canUnfaveSticker,
|
||||||
@ -70,6 +73,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onUnpin,
|
onUnpin,
|
||||||
onForward,
|
onForward,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onReport,
|
||||||
onFaveSticker,
|
onFaveSticker,
|
||||||
onUnfaveSticker,
|
onUnfaveSticker,
|
||||||
onSelect,
|
onSelect,
|
||||||
@ -135,6 +139,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
||||||
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
||||||
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</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>}
|
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export function getMessageCopyOptions(
|
|||||||
));
|
));
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
label: getCopyLabel(hasSelection, canImageBeCopied),
|
label: getCopyLabel(hasSelection),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
const clipboardText = hasSelection && selection ? selection.toString() : text;
|
const clipboardText = hasSelection && selection ? selection.toString() : text;
|
||||||
copyTextToClipboard(clipboardText);
|
copyTextToClipboard(clipboardText);
|
||||||
@ -71,14 +71,9 @@ export function getMessageCopyOptions(
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCopyLabel(hasSelection: boolean, canImageBeCopied: boolean): string {
|
function getCopyLabel(hasSelection: boolean): string {
|
||||||
if (hasSelection) {
|
if (hasSelection) {
|
||||||
return 'lng_context_copy_selected';
|
return 'lng_context_copy_selected';
|
||||||
}
|
}
|
||||||
|
return 'lng_context_copy_text';
|
||||||
if (canImageBeCopied) {
|
|
||||||
return 'lng_context_copy_text';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Copy';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.report {
|
||||||
|
.modal-dialog {
|
||||||
|
max-width: 15rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@ -422,6 +422,7 @@ export type ActionTypes = (
|
|||||||
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
||||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
'loadMessageLink' | 'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
'loadMessageLink' | 'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
||||||
|
'reportMessages' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
|
||||||
// poll result
|
// 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.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.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.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.getChats#3c6aa187 id:Vector<int> = messages.Chats;
|
||||||
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
||||||
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
|
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.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.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.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.getChats#3c6aa187 id:Vector<int> = messages.Chats;
|
||||||
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
||||||
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
|
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) => {
|
addReducer('markMessageListRead', (global, actions, payload) => {
|
||||||
const { serverTimeOffset } = global;
|
const { serverTimeOffset } = global;
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
|
|||||||
@ -386,6 +386,8 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
|| chat.isCreator
|
|| chat.isCreator
|
||||||
|| getHasAdminRight(chat, 'deleteMessages');
|
|| getHasAdminRight(chat, 'deleteMessages');
|
||||||
|
|
||||||
|
const canReport = !isPrivate && !isOwn;
|
||||||
|
|
||||||
const canDeleteForAll = canDelete && !isServiceNotification && (
|
const canDeleteForAll = canDelete && !isServiceNotification && (
|
||||||
(isPrivate && !isChatWithSelf)
|
(isPrivate && !isChatWithSelf)
|
||||||
|| (isBasicGroup && (
|
|| (isBasicGroup && (
|
||||||
@ -429,6 +431,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canPin,
|
canPin,
|
||||||
canUnpin,
|
canUnpin,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canReport,
|
||||||
canDeleteForAll,
|
canDeleteForAll,
|
||||||
canForward,
|
canForward,
|
||||||
canFaveSticker,
|
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) {
|
export function selectCanDeleteSelectedMessages(global: GlobalState) {
|
||||||
const { messageIds: selectedMessageIds } = global.selectedMessages || {};
|
const { messageIds: selectedMessageIds } = global.selectedMessages || {};
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global) || {};
|
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) {
|
export function selectUploadProgress(global: GlobalState, message: ApiMessage) {
|
||||||
const fileTransfer = global.fileUploads.byMessageLocalId[message.previousLocalId || message.id];
|
const fileTransfer = global.fileUploads.byMessageLocalId[message.previousLocalId || message.id];
|
||||||
|
|
||||||
|
|||||||
@ -15,12 +15,27 @@ const IGNORED_KEYS: Record<string, boolean> = {
|
|||||||
Tab: true,
|
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) => {
|
const preventDefault = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
function preventDefaultForScrollKeys(e: KeyboardEvent) {
|
function preventDefaultForScrollKeys(e: KeyboardEvent) {
|
||||||
if (IGNORED_KEYS[e.key]) {
|
if (IGNORED_KEYS[e.key] && !isTextBox(e.target)) {
|
||||||
preventDefault(e);
|
preventDefault(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user