Feature: Implement mass moderation for groups and channels (#4730)
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com> Co-authored-by: Anton <anton@korenskoy.ru>
This commit is contained in:
parent
ff9a0e3fdf
commit
7bfb334d63
@ -164,7 +164,6 @@ import Button from '../ui/Button';
|
|||||||
import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
|
import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
import DeleteMessageModal from './DeleteMessageModal.async';
|
|
||||||
import Icon from './icons/Icon';
|
import Icon from './icons/Icon';
|
||||||
import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji';
|
import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji';
|
||||||
|
|
||||||
@ -586,7 +585,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag();
|
const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag();
|
||||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||||
const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag();
|
const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag();
|
||||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
|
||||||
const [isHoverDisabled, disableHover, enableHover] = useFlag();
|
const [isHoverDisabled, disableHover, enableHover] = useFlag();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -756,7 +754,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
setHtml,
|
setHtml,
|
||||||
editingMessage,
|
editingMessage,
|
||||||
resetComposer,
|
resetComposer,
|
||||||
openDeleteModal,
|
|
||||||
chatId,
|
chatId,
|
||||||
threadId,
|
threadId,
|
||||||
messageListType,
|
messageListType,
|
||||||
@ -1405,7 +1402,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}, [isInStoryViewer, slowMode?.nextSendDate, stealthMode?.activeUntil]);
|
}, [isInStoryViewer, slowMode?.nextSendDate, stealthMode?.activeUntil]);
|
||||||
|
|
||||||
const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen
|
const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen
|
||||||
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isDeleteModalOpen || isBotCommandMenuOpen || isAttachMenuOpen
|
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isBotCommandMenuOpen || isAttachMenuOpen
|
||||||
|| isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen
|
|| isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen
|
||||||
|| isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus;
|
|| isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus;
|
||||||
const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen
|
const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen
|
||||||
@ -1473,9 +1470,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const prevEditedMessage = usePrevious(editingMessage, true);
|
|
||||||
const renderedEditedMessage = editingMessage || prevEditedMessage;
|
|
||||||
|
|
||||||
const scheduledDefaultDate = new Date();
|
const scheduledDefaultDate = new Date();
|
||||||
scheduledDefaultDate.setSeconds(0);
|
scheduledDefaultDate.setSeconds(0);
|
||||||
scheduledDefaultDate.setMilliseconds(0);
|
scheduledDefaultDate.setMilliseconds(0);
|
||||||
@ -1663,14 +1657,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
onClear={closePollModal}
|
onClear={closePollModal}
|
||||||
onSend={handlePollSend}
|
onSend={handlePollSend}
|
||||||
/>
|
/>
|
||||||
{renderedEditedMessage && (
|
|
||||||
<DeleteMessageModal
|
|
||||||
isOpen={isDeleteModalOpen}
|
|
||||||
isSchedule={messageListType === 'scheduled'}
|
|
||||||
onClose={closeDeleteModal}
|
|
||||||
message={renderedEditedMessage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<SendAsMenu
|
<SendAsMenu
|
||||||
isOpen={isSendAsMenuOpen}
|
isOpen={isSendAsMenuOpen}
|
||||||
onClose={closeSendAsMenu}
|
onClose={closeSendAsMenu}
|
||||||
|
|||||||
67
src/components/common/DeleteMessageModal.module.scss
Normal file
67
src/components/common/DeleteMessageModal.module.scss
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
.mainContainer {
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionTitle {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
color: var(--color-links);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictionTitle {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItemButton {
|
||||||
|
margin-top: 0.1875rem;
|
||||||
|
margin-left: 0.4375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: var(--color-links) !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
&:hover {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownList {
|
||||||
|
padding-left: 3.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogButtons {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictionContainer,
|
||||||
|
.dropdownList {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictionContainerOpen,
|
||||||
|
.dropdownListOpen {
|
||||||
|
max-height: 100vh;
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: max-height 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.Checkbox) {
|
||||||
|
padding-left: 3.875rem;
|
||||||
|
}
|
||||||
@ -1,49 +1,86 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, { memo, useCallback } from '../../lib/teact/teact';
|
import React, {
|
||||||
import { getActions, withGlobal } from '../../global';
|
memo, useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiMessage } from '../../api/types';
|
import type {
|
||||||
|
ApiChat, ApiChatMember, ApiMessage, ApiPeer,
|
||||||
|
} from '../../api/types';
|
||||||
import type { IAlbum } from '../../types';
|
import type { IAlbum } from '../../types';
|
||||||
|
import type { IRadioOption } from '../ui/CheckboxGroup';
|
||||||
|
|
||||||
|
import { REPLIES_USER_ID } from '../../config';
|
||||||
import {
|
import {
|
||||||
|
getHasAdminRight,
|
||||||
getPrivateChatUserId,
|
getPrivateChatUserId,
|
||||||
getUserFirstOrLastName,
|
getUserFirstOrLastName, getUserFullName,
|
||||||
isChatBasicGroup,
|
isChatBasicGroup, isChatChannel,
|
||||||
isChatSuperGroup,
|
isChatSuperGroup, isOwnMessage,
|
||||||
isUserId,
|
isUserId,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectAllowedMessageActions,
|
selectAllowedMessageActions,
|
||||||
selectBot,
|
selectBot,
|
||||||
selectChat,
|
selectChat, selectChatFullInfo, selectCurrentMessageIds,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList, selectSenderFromMessage, selectTabState,
|
||||||
selectUser,
|
selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
|
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
import useManagePermissions from '../right/hooks/useManagePermissions';
|
||||||
|
|
||||||
|
import PermissionCheckboxList from '../main/PermissionCheckboxList';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
|
import CheckboxGroup from '../ui/CheckboxGroup';
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
|
import Avatar from './Avatar';
|
||||||
|
import Icon from './icons/Icon';
|
||||||
|
|
||||||
|
import styles from './DeleteMessageModal.module.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen?: boolean;
|
||||||
isSchedule: boolean;
|
|
||||||
message: ApiMessage;
|
|
||||||
album?: IAlbum;
|
|
||||||
onClose: NoneToVoidFunction;
|
|
||||||
onConfirm?: NoneToVoidFunction;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
isChannel?: boolean;
|
||||||
|
isGroup?: boolean;
|
||||||
|
isSuperGroup?: boolean;
|
||||||
|
sender: ApiPeer | undefined;
|
||||||
|
currentUserId?: string;
|
||||||
canDeleteForAll?: boolean;
|
canDeleteForAll?: boolean;
|
||||||
contactName?: string;
|
contactName?: string;
|
||||||
willDeleteForCurrentUserOnly?: boolean;
|
willDeleteForCurrentUserOnly?: boolean;
|
||||||
willDeleteForAll?: boolean;
|
willDeleteForAll?: boolean;
|
||||||
|
messageIdList: number[] | undefined;
|
||||||
|
adminMembersById?: Record<string, ApiChatMember>;
|
||||||
|
chatBot?: boolean;
|
||||||
|
isSchedule?: boolean;
|
||||||
|
message?: ApiMessage;
|
||||||
|
album?: IAlbum;
|
||||||
|
onConfirm?: NoneToVoidFunction;
|
||||||
|
isOwn?: boolean;
|
||||||
|
canBanUsers?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
chat,
|
||||||
|
isChannel,
|
||||||
|
isGroup,
|
||||||
|
isSuperGroup,
|
||||||
|
sender,
|
||||||
|
currentUserId,
|
||||||
|
messageIdList,
|
||||||
isSchedule,
|
isSchedule,
|
||||||
message,
|
message,
|
||||||
album,
|
album,
|
||||||
@ -52,89 +89,392 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
|||||||
willDeleteForCurrentUserOnly,
|
willDeleteForCurrentUserOnly,
|
||||||
willDeleteForAll,
|
willDeleteForAll,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onClose,
|
adminMembersById,
|
||||||
|
chatBot,
|
||||||
|
isOwn,
|
||||||
|
canBanUsers,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
deleteMessages,
|
deleteMessages,
|
||||||
deleteScheduledMessages,
|
deleteScheduledMessages,
|
||||||
|
reportMessages,
|
||||||
|
deleteChatMember,
|
||||||
|
updateChatMemberBannedRights,
|
||||||
|
closeDeleteMessageModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const handleDeleteMessageForAll = useCallback(() => {
|
const prevIsOpen = usePrevious(isOpen);
|
||||||
onConfirm?.();
|
|
||||||
const messageIds = album?.messages
|
|
||||||
? album.messages.map(({ id }) => id)
|
|
||||||
: [message.id];
|
|
||||||
deleteMessages({ messageIds, shouldDeleteForAll: true });
|
|
||||||
onClose();
|
|
||||||
}, [onConfirm, album, message.id, deleteMessages, onClose]);
|
|
||||||
|
|
||||||
const handleDeleteMessageForSelf = useCallback(() => {
|
const lang = useOldLang();
|
||||||
|
|
||||||
|
const {
|
||||||
|
permissions, havePermissionChanged, handlePermissionChange, resetPermissions,
|
||||||
|
} = useManagePermissions(chat?.defaultBannedRights);
|
||||||
|
|
||||||
|
const [chosenDeleteOption, setChosenDeleteOption] = useState<string[] | undefined>(undefined);
|
||||||
|
const [chosenBanOption, setChosenBanOptions] = useState<string[] | undefined>(undefined);
|
||||||
|
const [chosenSpanOption, setChosenSpanOptions] = useState<string[] | undefined>(undefined);
|
||||||
|
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
||||||
|
const [isAdditionalOptionsVisible, setIsAdditionalOptionsVisible] = useState(false);
|
||||||
|
const isSenderOwner = useMemo(() => {
|
||||||
|
return sender && adminMembersById && adminMembersById[sender.id] && adminMembersById[sender.id].isOwner;
|
||||||
|
}, [sender, adminMembersById]);
|
||||||
|
|
||||||
|
const user = useMemo(() => {
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
if (!sender || isSchedule) return undefined;
|
||||||
|
|
||||||
|
return usersById[sender.id];
|
||||||
|
}, [isSchedule, sender]);
|
||||||
|
|
||||||
|
const shouldShowAdditionalOptions = useMemo(() => {
|
||||||
|
return user && user.id !== currentUserId;
|
||||||
|
}, [user, currentUserId]);
|
||||||
|
|
||||||
|
const userName = useMemo(() => {
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
if (!sender || isSchedule) return '';
|
||||||
|
|
||||||
|
return getUserFullName(usersById[sender.id]);
|
||||||
|
}, [isSchedule, sender]);
|
||||||
|
|
||||||
|
const ACTION_SPAM_OPTION: IRadioOption[] = useMemo(() => {
|
||||||
|
if (!user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: user.id,
|
||||||
|
label: lang('ReportSpamTitle'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [lang, user]);
|
||||||
|
|
||||||
|
const ACTION_DELETE_OPTION: IRadioOption[] = useMemo(() => {
|
||||||
|
if (!user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: user.id,
|
||||||
|
label: lang('DeleteAllFrom', userName),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [lang, user, userName]);
|
||||||
|
|
||||||
|
const ACTION_BAN_OPTION: IRadioOption[] = useMemo(() => {
|
||||||
|
if (!user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: user.id,
|
||||||
|
label: (message && isAdditionalOptionsVisible ? lang('KickFromSupergroup') : lang('DeleteBan', userName)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [isAdditionalOptionsVisible, lang, message, user, userName]);
|
||||||
|
|
||||||
|
const toggleAdditionalOptions = useLastCallback(() => {
|
||||||
|
setIsAdditionalOptionsVisible(!isAdditionalOptionsVisible);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterMessageIdByUserId = useLastCallback((userIds: string[], selectedMessageIdList: number[]) => {
|
||||||
|
return selectedMessageIdList.filter((msgId) => {
|
||||||
|
const senderPeer = selectSenderFromMessage(getGlobal(), chat, msgId);
|
||||||
|
return senderPeer && userIds.includes(senderPeer.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteMessages = useLastCallback((filteredMessageIdList: number[]) => {
|
||||||
|
if (filteredMessageIdList?.length) {
|
||||||
|
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteMember = useLastCallback((filteredUserIdList: string[]) => {
|
||||||
|
filteredUserIdList.forEach((userId) => {
|
||||||
|
deleteChatMember({ chatId: chat!.id, userId });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateChatMemberBannedRights = useLastCallback((filteredUserIdList: string[]) => {
|
||||||
|
filteredUserIdList.forEach((userId) => {
|
||||||
|
updateChatMemberBannedRights({
|
||||||
|
chatId: chat!.id,
|
||||||
|
userId,
|
||||||
|
bannedRights: permissions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteMessageForAll = useLastCallback(() => {
|
||||||
onConfirm?.();
|
onConfirm?.();
|
||||||
const messageIds = album?.messages
|
const messageIds = album?.messages
|
||||||
? album.messages.map(({ id }) => id)
|
? album.messages.map(({ id }) => id)
|
||||||
: [message.id];
|
: [message!.id];
|
||||||
|
deleteMessages({ messageIds, shouldDeleteForAll: true });
|
||||||
|
closeDeleteMessageModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteMessageForSelf = useLastCallback(() => {
|
||||||
|
onConfirm?.();
|
||||||
|
const messageIds = album?.messages
|
||||||
|
? album.messages.map(({ id }) => id)
|
||||||
|
: [message!.id];
|
||||||
if (isSchedule) {
|
if (isSchedule) {
|
||||||
deleteScheduledMessages({ messageIds });
|
deleteScheduledMessages({ messageIds });
|
||||||
|
} else if (!isOwn && (isChannel || isGroup || isSuperGroup)) {
|
||||||
|
if (chosenSpanOption) {
|
||||||
|
const filteredMessageIdList = filterMessageIdByUserId(chosenSpanOption, messageIdList!);
|
||||||
|
if (filteredMessageIdList && filteredMessageIdList.length) {
|
||||||
|
reportMessages({ messageIds: filteredMessageIdList, reason: 'spam', description: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenDeleteOption) {
|
||||||
|
const filteredMessageIdList = filterMessageIdByUserId(chosenDeleteOption, messageIdList!);
|
||||||
|
handleDeleteMessages(filteredMessageIdList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenBanOption && !havePermissionChanged && message) {
|
||||||
|
const filteredUserIdList = chosenBanOption.filter((userId) => messageIds?.some((msgId) => {
|
||||||
|
const senderPeer = selectSenderFromMessage(getGlobal(), chat, msgId);
|
||||||
|
return senderPeer && senderPeer.id === userId;
|
||||||
|
}));
|
||||||
|
handleDeleteMember(filteredUserIdList);
|
||||||
|
deleteMessages({
|
||||||
|
messageIds: [message.id],
|
||||||
|
shouldDeleteForAll: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenBanOption && havePermissionChanged) {
|
||||||
|
const filteredUserIdList = chosenBanOption.filter((userId) => messageIds?.some((msgId) => {
|
||||||
|
const senderPeer = selectSenderFromMessage(getGlobal(), chat, msgId);
|
||||||
|
return senderPeer && senderPeer.id === userId;
|
||||||
|
}));
|
||||||
|
handleUpdateChatMemberBannedRights(filteredUserIdList);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
deleteMessages({
|
deleteMessages({
|
||||||
messageIds,
|
messageIds,
|
||||||
shouldDeleteForAll: false,
|
shouldDeleteForAll: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
onClose();
|
closeDeleteMessageModal();
|
||||||
}, [onConfirm, album, message.id, isSchedule, onClose, deleteScheduledMessages, deleteMessages]);
|
});
|
||||||
|
|
||||||
const lang = useOldLang();
|
const handleDeleteOptionChange = useLastCallback((options: string[]) => {
|
||||||
|
setChosenDeleteOption(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBanOptionChange = useLastCallback((options: string[]) => {
|
||||||
|
setChosenBanOptions(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSpanOptionChange = useLastCallback((options: string[]) => {
|
||||||
|
setChosenSpanOptions(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
closeDeleteMessageModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen && prevIsOpen) {
|
||||||
|
setChosenSpanOptions(undefined);
|
||||||
|
setChosenDeleteOption(undefined);
|
||||||
|
setChosenBanOptions(undefined);
|
||||||
|
setIsMediaDropdownOpen(false);
|
||||||
|
setIsAdditionalOptionsVisible(false);
|
||||||
|
resetPermissions();
|
||||||
|
}
|
||||||
|
}, [isOpen, prevIsOpen, resetPermissions]);
|
||||||
|
|
||||||
|
function renderHeader() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
{(shouldShowAdditionalOptions && !canDeleteForAll && !isSchedule) && (
|
||||||
|
<Avatar
|
||||||
|
size="small"
|
||||||
|
peer={user!}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h3 className={styles.title}>{lang('DeleteSingleMessagesTitle')}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAdditionalActionOptions() {
|
||||||
|
return (
|
||||||
|
<div className={styles.options}>
|
||||||
|
<CheckboxGroup
|
||||||
|
options={ACTION_SPAM_OPTION}
|
||||||
|
onChange={handleSpanOptionChange}
|
||||||
|
selected={chosenSpanOption}
|
||||||
|
/>
|
||||||
|
<CheckboxGroup
|
||||||
|
options={ACTION_DELETE_OPTION}
|
||||||
|
onChange={handleDeleteOptionChange}
|
||||||
|
selected={chosenDeleteOption}
|
||||||
|
/>
|
||||||
|
{!isSenderOwner && canBanUsers && (
|
||||||
|
<CheckboxGroup
|
||||||
|
options={ACTION_BAN_OPTION}
|
||||||
|
onChange={handleBanOptionChange}
|
||||||
|
selected={chosenBanOption}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPartiallyRestrictedUser() {
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.restrictionContainer,
|
||||||
|
isAdditionalOptionsVisible && styles.restrictionContainerOpen)}
|
||||||
|
>
|
||||||
|
<h3 className={buildClassName(styles.actionTitle, styles.restrictionTitle)}>
|
||||||
|
{lang('UserRestrictionsCanDoUsers', 1)}
|
||||||
|
</h3>
|
||||||
|
<PermissionCheckboxList
|
||||||
|
withCheckbox
|
||||||
|
permissionGroup
|
||||||
|
chatId={chat?.id}
|
||||||
|
isMediaDropdownOpen={isMediaDropdownOpen}
|
||||||
|
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
|
||||||
|
handlePermissionChange={handlePermissionChange}
|
||||||
|
permissions={permissions}
|
||||||
|
className={buildClassName(
|
||||||
|
styles.dropdownList,
|
||||||
|
isMediaDropdownOpen && styles.dropdownListOpen,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={handleClose}
|
||||||
onEnter={isOpen && !canDeleteForAll ? handleDeleteMessageForSelf : undefined}
|
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
|
||||||
className="delete"
|
className="delete"
|
||||||
title={lang('DeleteSingleMessagesTitle')}
|
|
||||||
>
|
>
|
||||||
<p>{lang('AreYouSureDeleteSingleMessage')}</p>
|
<div className={buildClassName(styles.mainContainer, 'custom-scroll')}>
|
||||||
{willDeleteForCurrentUserOnly && (
|
{renderHeader()}
|
||||||
<p>{lang('lng_delete_for_me_chat_hint', 1, 'i')}</p>
|
{(shouldShowAdditionalOptions && !canDeleteForAll && !isSchedule && (isChannel || isGroup || isSuperGroup)) && (
|
||||||
)}
|
<>
|
||||||
{willDeleteForAll && (
|
<p className={styles.actionTitle}>{lang('DeleteAdditionalActions')}</p>
|
||||||
<p>{lang('lng_delete_for_everyone_hint', 1, 'i')}</p>
|
{renderAdditionalActionOptions()}
|
||||||
)}
|
{renderPartiallyRestrictedUser()}
|
||||||
<div className={canDeleteForAll ? 'dialog-buttons-column' : 'dialog-buttons'}>
|
{
|
||||||
{canDeleteForAll && (
|
chosenBanOption && canBanUsers && chosenBanOption?.length ? (
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
<ListItem
|
||||||
{contactName && renderText(lang('Conversation.DeleteMessagesFor', contactName))}
|
narrow
|
||||||
{!contactName && lang('Conversation.DeleteMessagesForEveryone')}
|
className={styles.listItemButton}
|
||||||
</Button>
|
buttonClassName={styles.button}
|
||||||
|
onClick={toggleAdditionalOptions}
|
||||||
|
>
|
||||||
|
{lang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
|
||||||
|
<Icon
|
||||||
|
name={isAdditionalOptionsVisible ? 'up' : 'down'}
|
||||||
|
className={buildClassName(styles.button, 'ml-2')}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
) : setIsAdditionalOptionsVisible(false)
|
||||||
|
}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
|
{(chatBot || !shouldShowAdditionalOptions) && (
|
||||||
{lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
|
<>
|
||||||
</Button>
|
<p>{lang('AreYouSureDeleteSingleMessage')}</p>
|
||||||
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
|
{willDeleteForCurrentUserOnly && (
|
||||||
|
<p>{lang('lng_delete_for_me_chat_hint', 1, 'i')}</p>
|
||||||
|
)}
|
||||||
|
{willDeleteForAll && (
|
||||||
|
<p>{lang('lng_delete_for_everyone_hint', 1, 'i')}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className={canDeleteForAll ? 'dialog-buttons-column'
|
||||||
|
: buildClassName('dialog-buttons', isAdditionalOptionsVisible && styles.dialogButtons)}
|
||||||
|
>
|
||||||
|
{canDeleteForAll && (
|
||||||
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||||
|
{contactName && renderText(lang('Conversation.DeleteMessagesFor', contactName))}
|
||||||
|
{!contactName && lang('Conversation.DeleteMessagesForEveryone')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
|
||||||
|
{lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="confirm-dialog-button"
|
||||||
|
isText
|
||||||
|
onClick={handleClose}
|
||||||
|
>{lang('Cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { message, isSchedule }): StateProps => {
|
(global): StateProps => {
|
||||||
const { threadId } = selectCurrentMessageList(global) || {};
|
const {
|
||||||
const { canDeleteForAll } = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
deleteMessageModal,
|
||||||
const chat = selectChat(global, message.chatId);
|
} = selectTabState(global);
|
||||||
|
const chatId = deleteMessageModal && deleteMessageModal.message?.chatId;
|
||||||
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
|
const chatFullInfo = chat && selectChatFullInfo(global, chat.id);
|
||||||
|
const { threadId, type } = selectCurrentMessageList(global) || {};
|
||||||
|
const { canDeleteForAll } = (deleteMessageModal && deleteMessageModal.message && threadId
|
||||||
|
&& selectAllowedMessageActions(global, deleteMessageModal.message, threadId)) || {};
|
||||||
|
const adminMembersById = chatFullInfo && chatFullInfo?.adminMembersById;
|
||||||
|
const messageIdList = chat && selectCurrentMessageIds(global, chat.id, threadId!, type!);
|
||||||
|
const isChannel = Boolean(chat) && isChatChannel(chat);
|
||||||
|
const isGroup = Boolean(chat) && isChatBasicGroup(chat);
|
||||||
|
const isSuperGroup = Boolean(chat) && isChatSuperGroup(chat);
|
||||||
|
const sender = deleteMessageModal && chat && deleteMessageModal.message
|
||||||
|
&& selectSenderFromMessage(global, chat, deleteMessageModal.message.id);
|
||||||
const contactName = chat && isUserId(chat.id)
|
const contactName = chat && isUserId(chat.id)
|
||||||
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
||||||
: undefined;
|
: undefined;
|
||||||
const isChatWithBot = Boolean(selectBot(global, message.chatId));
|
const isChatWithBot = Boolean(deleteMessageModal && deleteMessageModal.message
|
||||||
|
&& selectBot(global, deleteMessageModal.message.chatId));
|
||||||
|
const chatBot = Boolean(chat && chat.id !== REPLIES_USER_ID && selectBot(global, chat.id));
|
||||||
|
const canBanUsers = chat && (chat.isCreator || getHasAdminRight(chat, 'banUsers'));
|
||||||
|
const isOwn = deleteMessageModal && deleteMessageModal.message && isOwnMessage(deleteMessageModal.message);
|
||||||
|
|
||||||
const willDeleteForCurrentUserOnly = (chat && isChatBasicGroup(chat) && !canDeleteForAll) || isChatWithBot;
|
const willDeleteForCurrentUserOnly = (chat && isChatBasicGroup(chat) && !canDeleteForAll) || isChatWithBot;
|
||||||
const willDeleteForAll = chat && isChatSuperGroup(chat);
|
const willDeleteForAll = chat && isChatSuperGroup(chat);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canDeleteForAll: !isSchedule && canDeleteForAll,
|
chat,
|
||||||
|
isChannel,
|
||||||
|
isGroup,
|
||||||
|
isSuperGroup,
|
||||||
|
currentUserId: global.currentUserId,
|
||||||
|
sender,
|
||||||
|
messageIdList,
|
||||||
|
canDeleteForAll: deleteMessageModal && !deleteMessageModal.isSchedule && canDeleteForAll,
|
||||||
contactName,
|
contactName,
|
||||||
willDeleteForCurrentUserOnly,
|
willDeleteForCurrentUserOnly,
|
||||||
willDeleteForAll,
|
willDeleteForAll,
|
||||||
|
adminMembersById,
|
||||||
|
chatBot,
|
||||||
|
isSchedule: deleteMessageModal && deleteMessageModal.isSchedule,
|
||||||
|
message: deleteMessageModal && deleteMessageModal.message,
|
||||||
|
album: deleteMessageModal && deleteMessageModal.album,
|
||||||
|
onConfirm: deleteMessageModal && deleteMessageModal.onConfirm,
|
||||||
|
isOwn,
|
||||||
|
canBanUsers,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(DeleteMessageModal));
|
)(DeleteMessageModal));
|
||||||
|
|||||||
@ -64,6 +64,7 @@ import GroupCall from '../calls/group/GroupCall.async';
|
|||||||
import PhoneCall from '../calls/phone/PhoneCall.async';
|
import PhoneCall from '../calls/phone/PhoneCall.async';
|
||||||
import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async';
|
import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async';
|
||||||
import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
||||||
|
import DeleteMessageModal from '../common/DeleteMessageModal.async';
|
||||||
import StickerSetModal from '../common/StickerSetModal.async';
|
import StickerSetModal from '../common/StickerSetModal.async';
|
||||||
import UnreadCount from '../common/UnreadCounter';
|
import UnreadCount from '../common/UnreadCounter';
|
||||||
import LeftColumn from '../left/LeftColumn';
|
import LeftColumn from '../left/LeftColumn';
|
||||||
@ -143,6 +144,7 @@ type StateProps = {
|
|||||||
isReactionPickerOpen: boolean;
|
isReactionPickerOpen: boolean;
|
||||||
isAppendModalOpen?: boolean;
|
isAppendModalOpen?: boolean;
|
||||||
isGiveawayModalOpen?: boolean;
|
isGiveawayModalOpen?: boolean;
|
||||||
|
isDeleteMessageModalOpen?: boolean;
|
||||||
isPremiumGiftingModalOpen?: boolean;
|
isPremiumGiftingModalOpen?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
noRightColumnAnimation?: boolean;
|
noRightColumnAnimation?: boolean;
|
||||||
@ -192,6 +194,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
requestedDraft,
|
requestedDraft,
|
||||||
isPremiumModalOpen,
|
isPremiumModalOpen,
|
||||||
isGiveawayModalOpen,
|
isGiveawayModalOpen,
|
||||||
|
isDeleteMessageModalOpen,
|
||||||
isPremiumGiftingModalOpen,
|
isPremiumGiftingModalOpen,
|
||||||
isPaymentModalOpen,
|
isPaymentModalOpen,
|
||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
@ -577,6 +580,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||||
<DeleteFolderDialog folder={deleteFolderDialog} />
|
<DeleteFolderDialog folder={deleteFolderDialog} />
|
||||||
<ReactionPicker isOpen={isReactionPickerOpen} />
|
<ReactionPicker isOpen={isReactionPickerOpen} />
|
||||||
|
<DeleteMessageModal isOpen={isDeleteMessageModalOpen} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -610,6 +614,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
ratingPhoneCall,
|
ratingPhoneCall,
|
||||||
premiumModal,
|
premiumModal,
|
||||||
giveawayModal,
|
giveawayModal,
|
||||||
|
deleteMessageModal,
|
||||||
giftingModal,
|
giftingModal,
|
||||||
isMasterTab,
|
isMasterTab,
|
||||||
payment,
|
payment,
|
||||||
@ -665,6 +670,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
isPremiumModalOpen: premiumModal?.isOpen,
|
isPremiumModalOpen: premiumModal?.isOpen,
|
||||||
isGiveawayModalOpen: giveawayModal?.isOpen,
|
isGiveawayModalOpen: giveawayModal?.isOpen,
|
||||||
|
isDeleteMessageModalOpen: Boolean(deleteMessageModal),
|
||||||
isPremiumGiftingModalOpen: giftingModal?.isOpen,
|
isPremiumGiftingModalOpen: giftingModal?.isOpen,
|
||||||
limitReached: limitReachedModal?.limit,
|
limitReached: limitReachedModal?.limit,
|
||||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||||
|
|||||||
297
src/components/main/PermissionCheckboxList.tsx
Normal file
297
src/components/main/PermissionCheckboxList.tsx
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import React, {
|
||||||
|
memo, useMemo,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiChatBannedRights } from '../../api/types';
|
||||||
|
|
||||||
|
import { isChatPublic } from '../../global/helpers';
|
||||||
|
import { selectChat, selectChatFullInfo } from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import stopEvent from '../../util/stopEvent';
|
||||||
|
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import Checkbox from '../ui/Checkbox';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
chatId?: string;
|
||||||
|
handlePermissionChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
permissions: ApiChatBannedRights;
|
||||||
|
isMediaDropdownOpen: boolean;
|
||||||
|
setIsMediaDropdownOpen: (open: boolean) => void;
|
||||||
|
className?: string;
|
||||||
|
shiftedClassName?: string;
|
||||||
|
dropdownClassName?: string;
|
||||||
|
withCheckbox?: boolean;
|
||||||
|
permissionGroup?: boolean;
|
||||||
|
getControlIsDisabled?: (key: Exclude<keyof ApiChatBannedRights, 'untilDate'>) => boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
hasLinkedChat?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissionKeyList: (keyof ApiChatBannedRights)[] = [
|
||||||
|
'sendPhotos', 'sendVideos', 'sendStickers',
|
||||||
|
'sendAudios', 'sendDocs', 'sendVoices', 'sendRoundvideos', 'embedLinks', 'sendPolls',
|
||||||
|
];
|
||||||
|
|
||||||
|
const PermissionCheckboxList: FC<OwnProps & StateProps> = ({
|
||||||
|
chat,
|
||||||
|
isMediaDropdownOpen,
|
||||||
|
setIsMediaDropdownOpen,
|
||||||
|
hasLinkedChat,
|
||||||
|
permissions,
|
||||||
|
handlePermissionChange,
|
||||||
|
className,
|
||||||
|
shiftedClassName,
|
||||||
|
dropdownClassName,
|
||||||
|
withCheckbox,
|
||||||
|
getControlIsDisabled,
|
||||||
|
permissionGroup,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
showNotification,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const { isForum } = chat || {};
|
||||||
|
|
||||||
|
const lang = useOldLang();
|
||||||
|
|
||||||
|
const isPublic = useMemo(() => chat && isChatPublic(chat), [chat]);
|
||||||
|
const shouldDisablePermissionForPublicGroup = hasLinkedChat || isPublic;
|
||||||
|
|
||||||
|
const countCheckedPermissions = useMemo(() => {
|
||||||
|
return permissionKeyList.reduce((count, key) => {
|
||||||
|
if (!permissions[key]) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}, 0);
|
||||||
|
}, [permissions]);
|
||||||
|
|
||||||
|
const handleOpenMediaDropdown = useLastCallback((e: React.MouseEvent) => {
|
||||||
|
stopEvent(e);
|
||||||
|
setIsMediaDropdownOpen(!isMediaDropdownOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDisabledClick = useLastCallback(() => {
|
||||||
|
showNotification({ message: lang('lng_rights_permission_unavailable') });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendPlain"
|
||||||
|
checked={!permissions.sendPlain}
|
||||||
|
label={lang('UserRestrictionsSend')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendPlain')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendMedia"
|
||||||
|
checked={!permissions.sendMedia}
|
||||||
|
label={lang('UserRestrictionsSendMedia')}
|
||||||
|
labelText={`${countCheckedPermissions}/${permissionKeyList.length}`}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
rightIcon={isMediaDropdownOpen ? 'up' : 'down'}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
onClickLabel={handleOpenMediaDropdown}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendMedia')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={dropdownClassName}>
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendPhotos"
|
||||||
|
checked={!permissions.sendPhotos}
|
||||||
|
label={lang('UserRestrictionsSendPhotos')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendPhotos')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendVideos"
|
||||||
|
checked={!permissions.sendVideos}
|
||||||
|
label={lang('UserRestrictionsSendVideos')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendVideos')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendStickers"
|
||||||
|
checked={!permissions.sendStickers && !permissions.sendGifs}
|
||||||
|
label={lang('UserRestrictionsSendStickers')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendStickers')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendAudios"
|
||||||
|
checked={!permissions.sendAudios}
|
||||||
|
label={lang('UserRestrictionsSendMusic')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendAudios')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendDocs"
|
||||||
|
checked={!permissions.sendDocs}
|
||||||
|
label={lang('UserRestrictionsSendFiles')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendDocs')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendVoices"
|
||||||
|
checked={!permissions.sendVoices}
|
||||||
|
label={lang('UserRestrictionsSendVoices')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendVoices')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendRoundvideos"
|
||||||
|
checked={!permissions.sendRoundvideos}
|
||||||
|
label={lang('UserRestrictionsSendRound')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendRoundvideos')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="embedLinks"
|
||||||
|
checked={!permissions.embedLinks}
|
||||||
|
label={lang('UserRestrictionsEmbedLinks')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('embedLinks')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="sendPolls"
|
||||||
|
checked={!permissions.sendPolls}
|
||||||
|
label={lang('UserRestrictionsSendPolls')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('sendPolls')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={shiftedClassName}>
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="inviteUsers"
|
||||||
|
checked={!permissions.inviteUsers}
|
||||||
|
label={lang('UserRestrictionsInviteUsers')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('inviteUsers')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}
|
||||||
|
onClick={shouldDisablePermissionForPublicGroup ? handleDisabledClick : undefined}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
name="pinMessages"
|
||||||
|
checked={!permissions.pinMessages}
|
||||||
|
label={lang('UserRestrictionsPinMessages')}
|
||||||
|
disabled={getControlIsDisabled ? getControlIsDisabled('pinMessages')
|
||||||
|
: shouldDisablePermissionForPublicGroup}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}
|
||||||
|
onClick={shouldDisablePermissionForPublicGroup ? handleDisabledClick : undefined}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
name="changeInfo"
|
||||||
|
checked={!permissions.changeInfo}
|
||||||
|
label={lang('UserRestrictionsChangeInfo')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
disabled={getControlIsDisabled ? getControlIsDisabled('changeInfo')
|
||||||
|
: shouldDisablePermissionForPublicGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isForum && (
|
||||||
|
<div className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}>
|
||||||
|
<Checkbox
|
||||||
|
name="manageTopics"
|
||||||
|
checked={!permissions.manageTopics}
|
||||||
|
label={lang('CreateTopicsPermission')}
|
||||||
|
blocking
|
||||||
|
permissionGroup={permissionGroup}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
disabled={getControlIsDisabled && getControlIsDisabled('manageTopics')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
|
const fullInfo = chat && selectChatFullInfo(global, chat.id);
|
||||||
|
const hasLinkedChat = Boolean(fullInfo?.linkedChatId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
hasLinkedChat,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(PermissionCheckboxList));
|
||||||
@ -31,7 +31,6 @@ import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
|||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
import useZoomChange from './hooks/useZoomChangeSignal';
|
import useZoomChange from './hooks/useZoomChangeSignal';
|
||||||
|
|
||||||
import DeleteMessageModal from '../common/DeleteMessageModal';
|
|
||||||
import DeleteProfilePhotoModal from '../common/DeleteProfilePhotoModal';
|
import DeleteProfilePhotoModal from '../common/DeleteProfilePhotoModal';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import DropdownMenu from '../ui/DropdownMenu';
|
import DropdownMenu from '../ui/DropdownMenu';
|
||||||
@ -90,6 +89,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
updateProfilePhoto,
|
updateProfilePhoto,
|
||||||
updateChatPhoto,
|
updateChatPhoto,
|
||||||
openMediaViewer,
|
openMediaViewer,
|
||||||
|
openDeleteMessageModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const isMessage = item?.type === 'message';
|
const isMessage = item?.type === 'message';
|
||||||
@ -163,31 +163,16 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function renderDeleteModals() {
|
function renderDeleteModal() {
|
||||||
if (item?.type === 'message') {
|
return (item?.type === 'avatar') ? (
|
||||||
return (
|
<DeleteProfilePhotoModal
|
||||||
<DeleteMessageModal
|
isOpen={isDeleteModalOpen}
|
||||||
isOpen={isDeleteModalOpen}
|
onClose={closeDeleteModal}
|
||||||
isSchedule={messageListType === 'scheduled'}
|
onConfirm={onBeforeDelete}
|
||||||
onClose={closeDeleteModal}
|
profileId={item.avatarOwner.id}
|
||||||
onConfirm={onBeforeDelete}
|
photo={item.avatarOwner.photos![item.mediaIndex!]}
|
||||||
message={item.message}
|
/>
|
||||||
/>
|
) : undefined;
|
||||||
);
|
|
||||||
}
|
|
||||||
if (item?.type === 'avatar') {
|
|
||||||
return (
|
|
||||||
<DeleteProfilePhotoModal
|
|
||||||
isOpen={isDeleteModalOpen}
|
|
||||||
onClose={closeDeleteModal}
|
|
||||||
onConfirm={onBeforeDelete}
|
|
||||||
profileId={item.avatarOwner.id}
|
|
||||||
photo={item.avatarOwner.photos![item.mediaIndex!]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDownloadButton() {
|
function renderDownloadButton() {
|
||||||
@ -223,6 +208,17 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openDeleteModalHandler = useLastCallback(() => {
|
||||||
|
if (item?.type === 'message') {
|
||||||
|
openDeleteMessageModal({
|
||||||
|
isSchedule: messageListType === 'scheduled',
|
||||||
|
message: item.message, onConfirm: onBeforeDelete,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
openDeleteModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const menuItems: MenuItemProps[] = [];
|
const menuItems: MenuItemProps[] = [];
|
||||||
if (isMessage && item.message.isForwardingAllowed && !item.message.content.action && !isChatProtected) {
|
if (isMessage && item.message.isForwardingAllowed && !item.message.content.action && !isChatProtected) {
|
||||||
@ -268,7 +264,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
if (canDelete) {
|
if (canDelete) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
icon: 'delete',
|
icon: 'delete',
|
||||||
onClick: openDeleteModal,
|
onClick: openDeleteModalHandler,
|
||||||
children: lang('Delete'),
|
children: lang('Delete'),
|
||||||
destructive: true,
|
destructive: true,
|
||||||
});
|
});
|
||||||
@ -300,7 +296,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
))}
|
))}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{isDownloading && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
|
{isDownloading && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
|
||||||
{canDelete && renderDeleteModals()}
|
{canDelete && renderDeleteModal()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -365,7 +361,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
size="smaller"
|
size="smaller"
|
||||||
color="translucent-white"
|
color="translucent-white"
|
||||||
ariaLabel={lang('Delete')}
|
ariaLabel={lang('Delete')}
|
||||||
onClick={openDeleteModal}
|
onClick={openDeleteModalHandler}
|
||||||
>
|
>
|
||||||
<i className="icon icon-delete" />
|
<i className="icon icon-delete" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -379,7 +375,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<i className="icon icon-close" />
|
<i className="icon icon-close" />
|
||||||
</Button>
|
</Button>
|
||||||
{canDelete && renderDeleteModals()}
|
{canDelete && renderDeleteModal()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
63
src/components/middle/DeleteSelectedMessageModal.module.scss
Normal file
63
src/components/middle/DeleteSelectedMessageModal.module.scss
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
.main {
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionTitle {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
color: var(--color-links);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictionTitle {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItemButton {
|
||||||
|
margin-top: 0.1875rem;
|
||||||
|
margin-left: 0.4375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: var(--color-links) !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
&:hover {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownList {
|
||||||
|
padding-left: 3.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogButtons {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictionContainer,
|
||||||
|
.dropdownList {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: max-height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictionContainerOpen,
|
||||||
|
.dropdownListOpen {
|
||||||
|
max-height: 100vh;
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: max-height 0.6s ease-in-out;
|
||||||
|
}
|
||||||
@ -1,26 +1,57 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, { memo, useEffect } from '../../lib/teact/teact';
|
import React, {
|
||||||
import { getActions, withGlobal } from '../../global';
|
memo,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiChatMember } from '../../api/types';
|
||||||
|
import type { IRadioOption } from '../ui/CheckboxGroup';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getHasAdminRight,
|
||||||
getPrivateChatUserId,
|
getPrivateChatUserId,
|
||||||
getUserFirstOrLastName,
|
getUserFirstOrLastName,
|
||||||
|
getUserFullName,
|
||||||
isChatBasicGroup,
|
isChatBasicGroup,
|
||||||
|
isChatChannel,
|
||||||
isChatSuperGroup,
|
isChatSuperGroup,
|
||||||
isUserId,
|
isUserId,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectCanDeleteSelectedMessages, selectCurrentChat, selectTabState, selectUser,
|
selectCanDeleteSelectedMessages,
|
||||||
|
selectChatFullInfo,
|
||||||
|
selectCurrentChat,
|
||||||
|
selectCurrentMessageIds,
|
||||||
|
selectCurrentMessageList,
|
||||||
|
selectSenderFromMessage,
|
||||||
|
selectSendersFromSelectedMessages,
|
||||||
|
selectTabState,
|
||||||
|
selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { buildCollectionByCallback } from '../../util/iteratees';
|
||||||
import renderText from '../common/helpers/renderText';
|
import renderText from '../common/helpers/renderText';
|
||||||
|
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
import useManagePermissions from '../right/hooks/useManagePermissions';
|
||||||
|
|
||||||
|
import Avatar from '../common/Avatar';
|
||||||
|
import AvatarList from '../common/AvatarList';
|
||||||
|
import Icon from '../common/icons/Icon';
|
||||||
|
import PermissionCheckboxList from '../main/PermissionCheckboxList';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
|
import CheckboxGroup from '../ui/CheckboxGroup';
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
|
|
||||||
|
import styles from './DeleteSelectedMessageModal.module.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isSchedule: boolean;
|
isSchedule: boolean;
|
||||||
@ -28,39 +59,226 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
isChannel?: boolean;
|
||||||
|
isGroup?: boolean;
|
||||||
|
isSuperGroup?: boolean;
|
||||||
selectedMessageIds?: number[];
|
selectedMessageIds?: number[];
|
||||||
canDeleteForAll?: boolean;
|
canDeleteForAll?: boolean;
|
||||||
contactName?: string;
|
contactName?: string;
|
||||||
|
currentUserId?: string;
|
||||||
willDeleteForCurrentUserOnly?: boolean;
|
willDeleteForCurrentUserOnly?: boolean;
|
||||||
willDeleteForAll?: boolean;
|
willDeleteForAll?: boolean;
|
||||||
|
messageIds: number[] | undefined;
|
||||||
|
adminMembersById?: Record<string, ApiChatMember>;
|
||||||
|
canBanUsers?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
|
const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
|
||||||
|
chat,
|
||||||
|
isChannel,
|
||||||
|
isGroup,
|
||||||
|
isSuperGroup,
|
||||||
isOpen,
|
isOpen,
|
||||||
isSchedule,
|
isSchedule,
|
||||||
|
currentUserId,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
canDeleteForAll,
|
canDeleteForAll,
|
||||||
contactName,
|
contactName,
|
||||||
willDeleteForCurrentUserOnly,
|
willDeleteForCurrentUserOnly,
|
||||||
willDeleteForAll,
|
willDeleteForAll,
|
||||||
|
messageIds,
|
||||||
onClose,
|
onClose,
|
||||||
|
adminMembersById,
|
||||||
|
canBanUsers,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
deleteMessages,
|
deleteMessages,
|
||||||
|
reportMessages,
|
||||||
|
deleteChatMember,
|
||||||
deleteScheduledMessages,
|
deleteScheduledMessages,
|
||||||
exitMessageSelectMode,
|
exitMessageSelectMode,
|
||||||
|
updateChatMemberBannedRights,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const prevIsOpen = usePrevious(isOpen);
|
const prevIsOpen = usePrevious(isOpen);
|
||||||
|
|
||||||
|
const oldLang = useOldLang();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const {
|
||||||
|
permissions, havePermissionChanged, handlePermissionChange, resetPermissions,
|
||||||
|
} = useManagePermissions(chat?.defaultBannedRights);
|
||||||
|
|
||||||
|
const [chosenDeleteOption, setChosenDeleteOption] = useState<string[] | undefined>(undefined);
|
||||||
|
const [chosenBanOption, setChosenBanOptions] = useState<string[] | undefined>(undefined);
|
||||||
|
const [chosenSpanOption, setChosenSpanOptions] = useState<string[] | undefined>(undefined);
|
||||||
|
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
||||||
|
const [isAdditionalOptionsVisible, setIsAdditionalOptionsVisible] = useState(false);
|
||||||
|
|
||||||
|
const senderList = useMemo(() => {
|
||||||
|
return selectSendersFromSelectedMessages(getGlobal(), chat);
|
||||||
|
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
||||||
|
}, [chat, isOpen]);
|
||||||
|
|
||||||
|
const isSenderOwner = useMemo(() => {
|
||||||
|
if (!senderList) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderList.some((sender) => sender
|
||||||
|
&& adminMembersById
|
||||||
|
&& adminMembersById[sender.id] && adminMembersById[sender.id].isOwner);
|
||||||
|
}, [senderList, adminMembersById]);
|
||||||
|
|
||||||
|
const userList = useMemo(() => {
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
if (!senderList || isSchedule) return [];
|
||||||
|
const uniqueUserIds = new Set(senderList.map((user) => user!.id));
|
||||||
|
|
||||||
|
return Array.from(uniqueUserIds)
|
||||||
|
.map((id) => usersById[id])
|
||||||
|
.filter(Boolean);
|
||||||
|
}, [isSchedule, senderList]);
|
||||||
|
|
||||||
|
const nestedOptionsWithAvatarList = useLastCallback(() => {
|
||||||
|
return userList.map((user) => ({
|
||||||
|
value: `${user.id}`,
|
||||||
|
label: getUserFullName(user) || '',
|
||||||
|
leftElement: <Avatar size="small" peer={user} />,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const showAdditionalOptions = useMemo(() => {
|
||||||
|
return !userList.some((user) => user.id === currentUserId);
|
||||||
|
}, [userList, currentUserId]);
|
||||||
|
|
||||||
|
const userNames = useMemo(() => {
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
if (!senderList || isSchedule) return {};
|
||||||
|
|
||||||
|
const uniqueUserIds = new Set(senderList.map((user) => user!.id));
|
||||||
|
const userIds = Array.from(uniqueUserIds).map((userId) => usersById[userId]);
|
||||||
|
|
||||||
|
return buildCollectionByCallback(userIds, (user) => [user.id, getUserFullName(user)]);
|
||||||
|
}, [isSchedule, senderList]);
|
||||||
|
|
||||||
|
const ACTION_SPAM_OPTION: IRadioOption[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: selectedMessageIds && userList.length >= 2 ? 'spam' : userList?.[0]?.id,
|
||||||
|
label: oldLang('ReportSpamTitle'),
|
||||||
|
nestedOptions: selectedMessageIds && userList.length >= 2 ? [
|
||||||
|
...nestedOptionsWithAvatarList(),
|
||||||
|
] : undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [oldLang, selectedMessageIds, userList]);
|
||||||
|
|
||||||
|
const ACTION_DELETE_OPTION: IRadioOption[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: selectedMessageIds && userList.length >= 2 ? 'delete_all' : userList?.[0]?.id,
|
||||||
|
label: selectedMessageIds && userList.length >= 2
|
||||||
|
? oldLang('DeleteAllFromUsers')
|
||||||
|
: oldLang('DeleteAllFrom', Object.values(userNames)[0]),
|
||||||
|
nestedOptions: selectedMessageIds && userList.length >= 2 ? [
|
||||||
|
...nestedOptionsWithAvatarList(),
|
||||||
|
] : undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [oldLang, selectedMessageIds, userList, userNames]);
|
||||||
|
|
||||||
|
const ACTION_BAN_OPTION: IRadioOption[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: selectedMessageIds && userList.length >= 2 ? 'ban' : userList?.[0]?.id,
|
||||||
|
label: selectedMessageIds && userList.length >= 2
|
||||||
|
? (isAdditionalOptionsVisible ? oldLang('DeleteRestrictUsers') : oldLang('DeleteBanUsers'))
|
||||||
|
: (isAdditionalOptionsVisible ? oldLang('KickFromSupergroup')
|
||||||
|
: oldLang('DeleteBan', Object.values(userNames)[0])),
|
||||||
|
nestedOptions: selectedMessageIds && userList.length >= 2 ? [
|
||||||
|
...nestedOptionsWithAvatarList(),
|
||||||
|
] : undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [isAdditionalOptionsVisible, oldLang, selectedMessageIds, userList, userNames]);
|
||||||
|
|
||||||
|
const toggleAdditionalOptions = useLastCallback(() => {
|
||||||
|
setIsAdditionalOptionsVisible((prev) => !prev);
|
||||||
|
});
|
||||||
|
|
||||||
const handleDeleteMessageForAll = useLastCallback(() => {
|
const handleDeleteMessageForAll = useLastCallback(() => {
|
||||||
onClose();
|
onClose();
|
||||||
deleteMessages({ messageIds: selectedMessageIds!, shouldDeleteForAll: true });
|
deleteMessages({ messageIds: selectedMessageIds!, shouldDeleteForAll: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filterMessageIdByUserId = useLastCallback((userIds: string[], selectedMessageIdList: number[]) => {
|
||||||
|
return selectedMessageIdList.filter((msgId) => {
|
||||||
|
const sender = selectSenderFromMessage(getGlobal(), chat, msgId);
|
||||||
|
return sender && userIds.includes(sender.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteMessages = useLastCallback((filteredMessageIdList: number[]) => {
|
||||||
|
if (filteredMessageIdList && filteredMessageIdList.length) {
|
||||||
|
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteMember = useLastCallback((filteredUserIdList: string[]) => {
|
||||||
|
filteredUserIdList.forEach((userId) => {
|
||||||
|
deleteChatMember({ chatId: chat!.id, userId });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdateChatMemberBannedRights = useLastCallback((filteredUserIdList: string[]) => {
|
||||||
|
filteredUserIdList.forEach((userId) => {
|
||||||
|
updateChatMemberBannedRights({
|
||||||
|
chatId: chat!.id,
|
||||||
|
userId,
|
||||||
|
bannedRights: permissions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handleDeleteMessageForSelf = useLastCallback(() => {
|
const handleDeleteMessageForSelf = useLastCallback(() => {
|
||||||
if (isSchedule) {
|
if (isSchedule) {
|
||||||
deleteScheduledMessages({ messageIds: selectedMessageIds! });
|
deleteScheduledMessages({ messageIds: selectedMessageIds! });
|
||||||
|
} else if (!isSenderOwner && (isChannel || isSuperGroup)) {
|
||||||
|
if (chosenSpanOption) {
|
||||||
|
const userIdList = chosenSpanOption.filter((option) => !Number.isNaN(Number(option)));
|
||||||
|
const filteredMessageIdList = filterMessageIdByUserId(userIdList, selectedMessageIds!);
|
||||||
|
if (filteredMessageIdList && filteredMessageIdList.length) {
|
||||||
|
reportMessages({ messageIds: filteredMessageIdList, reason: 'spam', description: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenDeleteOption) {
|
||||||
|
const userIdList = chosenDeleteOption.filter((option) => !Number.isNaN(Number(option)));
|
||||||
|
const filteredMessageIdList = filterMessageIdByUserId(userIdList, messageIds!);
|
||||||
|
handleDeleteMessages(filteredMessageIdList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenBanOption && !havePermissionChanged) {
|
||||||
|
const userIdList = chosenBanOption.filter((option) => !Number.isNaN(Number(option)));
|
||||||
|
const filteredUserIdList = userIdList.filter((userId) => selectedMessageIds?.some((msgId) => {
|
||||||
|
const sender = selectSenderFromMessage(getGlobal(), chat, msgId);
|
||||||
|
return sender && sender.id === userId;
|
||||||
|
}));
|
||||||
|
handleDeleteMember(filteredUserIdList);
|
||||||
|
const filteredMessageIdList = filterMessageIdByUserId(userIdList, selectedMessageIds!);
|
||||||
|
handleDeleteMessages(filteredMessageIdList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenBanOption && havePermissionChanged) {
|
||||||
|
const userIdList = chosenBanOption.filter((option) => !Number.isNaN(Number(option)));
|
||||||
|
const filteredUserIdList = userIdList.filter((userId) => selectedMessageIds?.some((msgId) => {
|
||||||
|
const sender = selectSenderFromMessage(getGlobal(), chat, msgId);
|
||||||
|
return sender && sender.id === userId;
|
||||||
|
}));
|
||||||
|
handleUpdateChatMemberBannedRights(filteredUserIdList);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
deleteMessages({ messageIds: selectedMessageIds!, shouldDeleteForAll: false });
|
deleteMessages({ messageIds: selectedMessageIds!, shouldDeleteForAll: false });
|
||||||
}
|
}
|
||||||
@ -68,14 +286,97 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
const lang = useOldLang();
|
const onCloseHandler = useLastCallback(() => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteOptionChange = useLastCallback((options: string[]) => {
|
||||||
|
setChosenDeleteOption(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBanOptionChange = useLastCallback((options: string[]) => {
|
||||||
|
setChosenBanOptions(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSpanOptionChange = useLastCallback((options: string[]) => {
|
||||||
|
setChosenSpanOptions(options);
|
||||||
|
});
|
||||||
|
|
||||||
// Returning `undefined` from FC instead of `<Modal>` doesn't trigger useHistoryBack
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen && prevIsOpen) {
|
if (!isOpen && prevIsOpen) {
|
||||||
exitMessageSelectMode();
|
exitMessageSelectMode();
|
||||||
|
setChosenSpanOptions(undefined);
|
||||||
|
setChosenDeleteOption(undefined);
|
||||||
|
setChosenBanOptions(undefined);
|
||||||
|
setIsMediaDropdownOpen(false);
|
||||||
|
setIsAdditionalOptionsVisible(false);
|
||||||
|
resetPermissions();
|
||||||
}
|
}
|
||||||
}, [exitMessageSelectMode, isOpen, prevIsOpen]);
|
}, [exitMessageSelectMode, isOpen, prevIsOpen, resetPermissions]);
|
||||||
|
|
||||||
|
function renderHeader() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container} dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||||
|
{(showAdditionalOptions && !canDeleteForAll && !isSchedule) && (
|
||||||
|
<AvatarList
|
||||||
|
size="small"
|
||||||
|
peers={userList}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h3 className={styles.title}>{oldLang('Chat.DeleteMessagesConfirmation', selectedMessageIds?.length)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAdditionalActionOptions() {
|
||||||
|
return (
|
||||||
|
<div className={styles.options}>
|
||||||
|
<CheckboxGroup
|
||||||
|
options={ACTION_SPAM_OPTION}
|
||||||
|
onChange={handleSpanOptionChange}
|
||||||
|
selected={chosenSpanOption}
|
||||||
|
nestedCheckbox={selectedMessageIds && userList.length >= 2}
|
||||||
|
/>
|
||||||
|
<CheckboxGroup
|
||||||
|
options={ACTION_DELETE_OPTION}
|
||||||
|
onChange={handleDeleteOptionChange}
|
||||||
|
selected={chosenDeleteOption}
|
||||||
|
nestedCheckbox={selectedMessageIds && userList.length >= 2}
|
||||||
|
/>
|
||||||
|
{!isSenderOwner && canBanUsers && (
|
||||||
|
<CheckboxGroup
|
||||||
|
options={ACTION_BAN_OPTION}
|
||||||
|
onChange={handleBanOptionChange}
|
||||||
|
selected={chosenBanOption}
|
||||||
|
nestedCheckbox={selectedMessageIds && userList.length >= 2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPartiallyRestrictedUser() {
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.restrictionContainer,
|
||||||
|
isAdditionalOptionsVisible && styles.restrictionContainerOpen)}
|
||||||
|
>
|
||||||
|
<h3 className={buildClassName(styles.actionTitle, styles.restrictionTitle)}>
|
||||||
|
{oldLang('UserRestrictionsCanDoUsers', userList.length)}
|
||||||
|
</h3>
|
||||||
|
<PermissionCheckboxList
|
||||||
|
withCheckbox
|
||||||
|
permissionGroup
|
||||||
|
chatId={chat?.id}
|
||||||
|
isMediaDropdownOpen={isMediaDropdownOpen}
|
||||||
|
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
|
||||||
|
handlePermissionChange={handlePermissionChange}
|
||||||
|
permissions={permissions}
|
||||||
|
className={buildClassName(styles.dropdownList, isMediaDropdownOpen && styles.dropdownListOpen)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedMessageIds) {
|
if (!selectedMessageIds) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -87,27 +388,50 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
|
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
|
||||||
className="delete"
|
className="delete"
|
||||||
title={lang('Conversation.DeleteManyMessages')}
|
|
||||||
>
|
>
|
||||||
<p>{lang('AreYouSureDeleteFewMessages')}</p>
|
<div className={styles.main}>
|
||||||
{willDeleteForCurrentUserOnly && (
|
{renderHeader()}
|
||||||
<p>This will delete them just for you, not for other participants in the chat.</p>
|
{!showAdditionalOptions && <p>{lang('AreYouSureDeleteFewMessages')}</p>}
|
||||||
)}
|
{(showAdditionalOptions && !canDeleteForAll && !isSchedule && (isChannel || isGroup || isSuperGroup)) && (
|
||||||
{willDeleteForAll && (
|
<>
|
||||||
<p>This will delete them for everyone in this chat.</p>
|
<p className={styles.actionTitle}>{oldLang('DeleteAdditionalActions')}</p>
|
||||||
)}
|
{renderAdditionalActionOptions()}
|
||||||
<div className={canDeleteForAll ? 'dialog-buttons-column' : 'dialog-buttons'}>
|
{renderPartiallyRestrictedUser()}
|
||||||
{canDeleteForAll && (
|
{
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
chosenBanOption && canBanUsers && chosenBanOption?.length ? (
|
||||||
{contactName
|
<ListItem
|
||||||
? renderText(lang('ChatList.DeleteForEveryone', contactName))
|
narrow
|
||||||
: lang('Conversation.DeleteMessagesForEveryone')}
|
className={styles.listItemButton}
|
||||||
</Button>
|
buttonClassName={styles.button}
|
||||||
|
onClick={toggleAdditionalOptions}
|
||||||
|
>
|
||||||
|
{oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
|
||||||
|
<Icon
|
||||||
|
name={isAdditionalOptionsVisible ? 'up' : 'down'}
|
||||||
|
className={buildClassName(styles.button, 'ml-2')}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
) : setIsAdditionalOptionsVisible(false)
|
||||||
|
}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
|
{willDeleteForCurrentUserOnly && lang('DeleteForMeDescription')}
|
||||||
{lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
|
{(willDeleteForAll && !showAdditionalOptions) && lang('DeleteForEveryoneDescription')}
|
||||||
</Button>
|
<div className={canDeleteForAll ? 'dialog-buttons-column'
|
||||||
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
|
: buildClassName('dialog-buttons', isAdditionalOptionsVisible && styles.dialogButtons)}
|
||||||
|
>
|
||||||
|
{canDeleteForAll && (
|
||||||
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||||
|
{contactName
|
||||||
|
? renderText(oldLang('ChatList.DeleteForEveryone', contactName))
|
||||||
|
: oldLang('Conversation.DeleteMessagesForEveryone')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
|
||||||
|
{oldLang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
|
||||||
|
</Button>
|
||||||
|
<Button className="confirm-dialog-button" isText onClick={onCloseHandler}>{oldLang('Cancel')}</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@ -118,19 +442,34 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const { messageIds: selectedMessageIds } = selectTabState(global).selectedMessages || {};
|
const { messageIds: selectedMessageIds } = selectTabState(global).selectedMessages || {};
|
||||||
const { canDeleteForAll } = selectCanDeleteSelectedMessages(global);
|
const { canDeleteForAll } = selectCanDeleteSelectedMessages(global);
|
||||||
const chat = selectCurrentChat(global);
|
const chat = selectCurrentChat(global);
|
||||||
|
const chatFullInfo = chat && selectChatFullInfo(global, chat.id);
|
||||||
|
const { threadId, type } = selectCurrentMessageList(global) || {};
|
||||||
|
const messageIds = chat && selectCurrentMessageIds(global, chat.id, threadId!, type!);
|
||||||
|
const isChannel = Boolean(chat) && isChatChannel(chat);
|
||||||
|
const isGroup = Boolean(chat) && isChatBasicGroup(chat);
|
||||||
|
const isSuperGroup = Boolean(chat) && isChatSuperGroup(chat);
|
||||||
const contactName = chat && isUserId(chat.id)
|
const contactName = chat && isUserId(chat.id)
|
||||||
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const adminMembersById = chatFullInfo?.adminMembersById;
|
||||||
|
const canBanUsers = chat && (chat.isCreator || getHasAdminRight(chat, 'banUsers'));
|
||||||
const willDeleteForCurrentUserOnly = chat && isChatBasicGroup(chat) && !canDeleteForAll;
|
const willDeleteForCurrentUserOnly = chat && isChatBasicGroup(chat) && !canDeleteForAll;
|
||||||
const willDeleteForAll = chat && isChatSuperGroup(chat);
|
const willDeleteForAll = chat && isChatSuperGroup(chat);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
chat,
|
||||||
|
isChannel,
|
||||||
|
isGroup,
|
||||||
|
isSuperGroup,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
|
currentUserId: global.currentUserId,
|
||||||
canDeleteForAll: !isSchedule && canDeleteForAll,
|
canDeleteForAll: !isSchedule && canDeleteForAll,
|
||||||
contactName,
|
contactName,
|
||||||
willDeleteForCurrentUserOnly,
|
willDeleteForCurrentUserOnly,
|
||||||
willDeleteForAll,
|
willDeleteForAll,
|
||||||
|
messageIds,
|
||||||
|
adminMembersById,
|
||||||
|
canBanUsers,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(DeleteSelectedMessageModal));
|
)(DeleteSelectedMessageModal));
|
||||||
|
|||||||
@ -29,14 +29,15 @@ const useEditing = (
|
|||||||
setHtml: (html: string) => void,
|
setHtml: (html: string) => void,
|
||||||
editedMessage: ApiMessage | undefined,
|
editedMessage: ApiMessage | undefined,
|
||||||
resetComposer: (shouldPreserveInput?: boolean) => void,
|
resetComposer: (shouldPreserveInput?: boolean) => void,
|
||||||
openDeleteModal: () => void,
|
|
||||||
chatId: string,
|
chatId: string,
|
||||||
threadId: ThreadId,
|
threadId: ThreadId,
|
||||||
type: MessageListType,
|
type: MessageListType,
|
||||||
draft?: ApiDraft,
|
draft?: ApiDraft,
|
||||||
editingDraft?: ApiFormattedText,
|
editingDraft?: ApiFormattedText,
|
||||||
): [VoidFunction, VoidFunction, boolean] => {
|
): [VoidFunction, VoidFunction, boolean] => {
|
||||||
const { editMessage, setEditingDraft, toggleMessageWebPage } = getActions();
|
const {
|
||||||
|
editMessage, setEditingDraft, toggleMessageWebPage, openDeleteMessageModal,
|
||||||
|
} = getActions();
|
||||||
const [shouldForceShowEditing, setShouldForceShowEditing] = useState(false);
|
const [shouldForceShowEditing, setShouldForceShowEditing] = useState(false);
|
||||||
|
|
||||||
const replyingToId = draft?.replyInfo?.replyToMsgId;
|
const replyingToId = draft?.replyInfo?.replyToMsgId;
|
||||||
@ -152,7 +153,7 @@ const useEditing = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!text && !hasMessageMedia(editedMessage)) {
|
if (!text && !hasMessageMedia(editedMessage)) {
|
||||||
openDeleteModal();
|
openDeleteMessageModal({ isSchedule: type === 'scheduled', message: editedMessage });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,6 @@ import useOldLang from '../../../hooks/useOldLang';
|
|||||||
import useSchedule from '../../../hooks/useSchedule';
|
import useSchedule from '../../../hooks/useSchedule';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
|
||||||
import PinMessageModal from '../../common/PinMessageModal.async';
|
import PinMessageModal from '../../common/PinMessageModal.async';
|
||||||
import ReportModal from '../../common/ReportModal';
|
import ReportModal from '../../common/ReportModal';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
@ -222,12 +221,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
openPremiumModal,
|
openPremiumModal,
|
||||||
loadOutboxReadDate,
|
loadOutboxReadDate,
|
||||||
copyMessageLink,
|
copyMessageLink,
|
||||||
|
openDeleteMessageModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
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 [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||||
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
||||||
@ -333,24 +332,20 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
isMessageTranslated, message.content.text,
|
isMessageTranslated, message.content.text,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDelete = useLastCallback(() => {
|
|
||||||
setIsMenuOpen(false);
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleReport = useLastCallback(() => {
|
|
||||||
setIsMenuOpen(false);
|
|
||||||
setIsReportModalOpen(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const closeMenu = useLastCallback(() => {
|
const closeMenu = useLastCallback(() => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeDeleteModal = useLastCallback(() => {
|
const handleDelete = useLastCallback(() => {
|
||||||
setIsDeleteModalOpen(false);
|
setIsMenuOpen(false);
|
||||||
onClose();
|
closeMenu();
|
||||||
|
openDeleteMessageModal({ isSchedule: messageListType === 'scheduled', album, message });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleReport = useLastCallback(() => {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
setIsReportModalOpen(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeReportModal = useLastCallback(() => {
|
const closeReportModal = useLastCallback(() => {
|
||||||
@ -639,13 +634,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onShowOriginal={handleShowOriginal}
|
onShowOriginal={handleShowOriginal}
|
||||||
onSelectLanguage={handleSelectLanguage}
|
onSelectLanguage={handleSelectLanguage}
|
||||||
/>
|
/>
|
||||||
<DeleteMessageModal
|
|
||||||
isOpen={isDeleteModalOpen}
|
|
||||||
isSchedule={messageListType === 'scheduled'}
|
|
||||||
onClose={closeDeleteModal}
|
|
||||||
album={album}
|
|
||||||
message={message}
|
|
||||||
/>
|
|
||||||
<ReportModal
|
<ReportModal
|
||||||
isOpen={isReportModalOpen}
|
isOpen={isReportModalOpen}
|
||||||
onClose={closeReportModal}
|
onClose={closeReportModal}
|
||||||
|
|||||||
@ -109,11 +109,17 @@ export default function useManagePermissions(defaultPermissions: ApiChatBannedRi
|
|||||||
}));
|
}));
|
||||||
}, [defaultPermissions, permissions]);
|
}, [defaultPermissions, permissions]);
|
||||||
|
|
||||||
|
const resetPermissions = useCallback(() => {
|
||||||
|
setPermissions(defaultPermissions || {});
|
||||||
|
setHavePermissionChanged(false);
|
||||||
|
}, [defaultPermissions]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permissions,
|
permissions,
|
||||||
isLoading,
|
isLoading,
|
||||||
havePermissionChanged,
|
havePermissionChanged,
|
||||||
handlePermissionChange,
|
handlePermissionChange,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
|
resetPermissions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,17 +7,15 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
import type { ApiChat, ApiChatBannedRights, ApiChatMember } from '../../../api/types';
|
import type { ApiChat, ApiChatBannedRights, ApiChatMember } from '../../../api/types';
|
||||||
import { ManagementScreens } from '../../../types';
|
import { ManagementScreens } from '../../../types';
|
||||||
|
|
||||||
import { isChatPublic } from '../../../global/helpers';
|
|
||||||
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import stopEvent from '../../../util/stopEvent';
|
|
||||||
|
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
import useManagePermissions from '../hooks/useManagePermissions';
|
import useManagePermissions from '../hooks/useManagePermissions';
|
||||||
|
|
||||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import PermissionCheckboxList from '../../main/PermissionCheckboxList';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
@ -33,7 +31,6 @@ type OwnProps = {
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
hasLinkedChat?: boolean;
|
|
||||||
removedUsersCount: number;
|
removedUsersCount: number;
|
||||||
members?: ApiChatMember[];
|
members?: ApiChatMember[];
|
||||||
};
|
};
|
||||||
@ -86,21 +83,17 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
onChatMemberSelect,
|
onChatMemberSelect,
|
||||||
chat,
|
chat,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
hasLinkedChat,
|
|
||||||
removedUsersCount,
|
removedUsersCount,
|
||||||
members,
|
members,
|
||||||
onClose,
|
onClose,
|
||||||
isActive,
|
isActive,
|
||||||
}) => {
|
}) => {
|
||||||
const { updateChatDefaultBannedRights, showNotification } = getActions();
|
const { updateChatDefaultBannedRights } = getActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
|
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
|
||||||
} = useManagePermissions(chat?.defaultBannedRights);
|
} = useManagePermissions(chat?.defaultBannedRights);
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
const { isForum } = chat || {};
|
|
||||||
const isPublic = useMemo(() => chat && isChatPublic(chat), [chat]);
|
|
||||||
const shouldDisablePermissionForPublicGroup = hasLinkedChat || isPublic;
|
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive,
|
isActive,
|
||||||
@ -121,14 +114,6 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
}, [currentUserId, onChatMemberSelect, onScreenSelect]);
|
}, [currentUserId, onChatMemberSelect, onScreenSelect]);
|
||||||
|
|
||||||
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
||||||
const handleOpenMediaDropdown = useCallback((e: React.MouseEvent) => {
|
|
||||||
stopEvent(e);
|
|
||||||
setIsMediaDropdownOpen(!isMediaDropdownOpen);
|
|
||||||
}, [isMediaDropdownOpen]);
|
|
||||||
|
|
||||||
const handleDisabledClick = useCallback(() => {
|
|
||||||
showNotification({ message: lang('lng_rights_permission_unavailable') });
|
|
||||||
}, [lang, showNotification]);
|
|
||||||
|
|
||||||
const handleSavePermissions = useCallback(() => {
|
const handleSavePermissions = useCallback(() => {
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
@ -186,174 +171,19 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="custom-scroll">
|
<div className="custom-scroll">
|
||||||
<div className="section without-bottom-shadow">
|
<div className="section without-bottom-shadow">
|
||||||
<h3 className="section-heading" dir="auto">{lang('ChannelPermissionsHeader')}</h3>
|
<h3 className="section-heading" dir="auto">{lang('ChannelPermissionsHeader')}</h3>
|
||||||
|
<PermissionCheckboxList
|
||||||
<div className="ListItem with-checkbox">
|
chatId={chat?.id}
|
||||||
<Checkbox
|
isMediaDropdownOpen={isMediaDropdownOpen}
|
||||||
name="sendPlain"
|
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
|
||||||
checked={!permissions.sendPlain}
|
handlePermissionChange={handlePermissionChange}
|
||||||
label={lang('UserRestrictionsSend')}
|
permissions={permissions}
|
||||||
blocking
|
dropdownClassName="DropdownListTrap"
|
||||||
onChange={handlePermissionChange}
|
className={buildClassName(
|
||||||
/>
|
'DropdownList',
|
||||||
</div>
|
isMediaDropdownOpen && 'DropdownList--open',
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendMedia"
|
|
||||||
checked={!permissions.sendMedia}
|
|
||||||
label={lang('UserRestrictionsSendMedia')}
|
|
||||||
blocking
|
|
||||||
rightIcon={isMediaDropdownOpen ? 'up' : 'down'}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
onClickLabel={handleOpenMediaDropdown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="DropdownListTrap">
|
|
||||||
<div
|
|
||||||
className={buildClassName(
|
|
||||||
'DropdownList',
|
|
||||||
isMediaDropdownOpen && 'DropdownList--open',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendPhotos"
|
|
||||||
checked={!permissions.sendPhotos}
|
|
||||||
label={lang('UserRestrictionsSendPhotos')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendVideos"
|
|
||||||
checked={!permissions.sendVideos}
|
|
||||||
label={lang('UserRestrictionsSendVideos')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendStickers"
|
|
||||||
checked={!permissions.sendStickers && !permissions.sendGifs}
|
|
||||||
label={lang('UserRestrictionsSendStickers')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendAudios"
|
|
||||||
checked={!permissions.sendAudios}
|
|
||||||
label={lang('UserRestrictionsSendMusic')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendDocs"
|
|
||||||
checked={!permissions.sendDocs}
|
|
||||||
label={lang('UserRestrictionsSendFiles')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendVoices"
|
|
||||||
checked={!permissions.sendVoices}
|
|
||||||
label={lang('UserRestrictionsSendVoices')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendRoundvideos"
|
|
||||||
checked={!permissions.sendRoundvideos}
|
|
||||||
label={lang('UserRestrictionsSendRound')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="embedLinks"
|
|
||||||
checked={!permissions.embedLinks}
|
|
||||||
label={lang('UserRestrictionsEmbedLinks')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="sendPolls"
|
|
||||||
checked={!permissions.sendPolls}
|
|
||||||
label={lang('UserRestrictionsSendPolls')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={buildClassName('part', isMediaDropdownOpen && 'shifted')}>
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="inviteUsers"
|
|
||||||
checked={!permissions.inviteUsers}
|
|
||||||
label={lang('UserRestrictionsInviteUsers')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="ListItem with-checkbox"
|
|
||||||
onClick={shouldDisablePermissionForPublicGroup ? handleDisabledClick : undefined}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
name="pinMessages"
|
|
||||||
checked={!permissions.pinMessages}
|
|
||||||
label={lang('UserRestrictionsPinMessages')}
|
|
||||||
disabled={shouldDisablePermissionForPublicGroup}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="ListItem with-checkbox"
|
|
||||||
onClick={shouldDisablePermissionForPublicGroup ? handleDisabledClick : undefined}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
name="changeInfo"
|
|
||||||
checked={!permissions.changeInfo}
|
|
||||||
label={lang('UserRestrictionsChangeInfo')}
|
|
||||||
blocking
|
|
||||||
disabled={shouldDisablePermissionForPublicGroup}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isForum && (
|
|
||||||
<div className="ListItem with-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
name="manageTopics"
|
|
||||||
checked={!permissions.manageTopics}
|
|
||||||
label={lang('CreateTopicsPermission')}
|
|
||||||
blocking
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -425,12 +255,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global, { chatId }): StateProps => {
|
(global, { chatId }): StateProps => {
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const fullInfo = selectChatFullInfo(global, chatId);
|
const fullInfo = selectChatFullInfo(global, chatId);
|
||||||
const hasLinkedChat = Boolean(fullInfo?.linkedChatId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat,
|
chat,
|
||||||
currentUserId: global.currentUserId,
|
currentUserId: global.currentUserId,
|
||||||
hasLinkedChat,
|
|
||||||
removedUsersCount: fullInfo?.kickedMembers?.length || 0,
|
removedUsersCount: fullInfo?.kickedMembers?.length || 0,
|
||||||
members: fullInfo?.members,
|
members: fullInfo?.members,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,15 +9,15 @@ import { ManagementScreens } from '../../../types';
|
|||||||
|
|
||||||
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import stopEvent from '../../../util/stopEvent';
|
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
import useManagePermissions from '../hooks/useManagePermissions';
|
import useManagePermissions from '../hooks/useManagePermissions';
|
||||||
|
|
||||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import PermissionCheckboxList from '../../main/PermissionCheckboxList';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
@ -69,9 +69,8 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
|
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
|
||||||
} = useManagePermissions(selectedChatMember?.bannedRights || chat?.defaultBannedRights);
|
} = useManagePermissions(selectedChatMember?.bannedRights || chat?.defaultBannedRights);
|
||||||
const [isBanConfirmationDialogOpen, openBanConfirmationDialog, closeBanConfirmationDialog] = useFlag();
|
const [isBanConfirmationDialogOpen, openBanConfirmationDialog, closeBanConfirmationDialog] = useFlag();
|
||||||
const lang = useOldLang();
|
const lang = useLang();
|
||||||
|
const oldLang = useOldLang();
|
||||||
const { isForum } = chat || {};
|
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive,
|
isActive,
|
||||||
@ -124,10 +123,6 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
}, [chat, isFormFullyDisabled]);
|
}, [chat, isFormFullyDisabled]);
|
||||||
|
|
||||||
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
||||||
const handleOpenMediaDropdown = useCallback((e: React.MouseEvent) => {
|
|
||||||
stopEvent(e);
|
|
||||||
setIsMediaDropdownOpen(!isMediaDropdownOpen);
|
|
||||||
}, [isMediaDropdownOpen]);
|
|
||||||
|
|
||||||
if (!selectedChatMember) {
|
if (!selectedChatMember) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -145,185 +140,21 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
<PrivateChatInfo userId={selectedChatMember.userId} forceShowSelf />
|
<PrivateChatInfo userId={selectedChatMember.userId} forceShowSelf />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<h3 className="section-heading mt-4" dir="auto">{lang('UserRestrictionsCanDo')}</h3>
|
<h3 className="section-heading mt-4" dir="auto">{oldLang('UserRestrictionsCanDo')}</h3>
|
||||||
|
<PermissionCheckboxList
|
||||||
<div className="ListItem">
|
chatId={chat?.id}
|
||||||
<Checkbox
|
isMediaDropdownOpen={isMediaDropdownOpen}
|
||||||
name="sendPlain"
|
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
|
||||||
checked={!permissions.sendPlain}
|
handlePermissionChange={handlePermissionChange}
|
||||||
label={lang('UserRestrictionsSend')}
|
permissions={permissions}
|
||||||
blocking
|
className={buildClassName(
|
||||||
disabled={getControlIsDisabled('sendPlain')}
|
'DropdownList',
|
||||||
onChange={handlePermissionChange}
|
isMediaDropdownOpen && 'DropdownList--open',
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendMedia"
|
|
||||||
checked={!permissions.sendMedia}
|
|
||||||
label={lang('UserRestrictionsSendMedia')}
|
|
||||||
blocking
|
|
||||||
rightIcon={isMediaDropdownOpen ? 'up' : 'down'}
|
|
||||||
disabled={getControlIsDisabled('sendMedia')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
onClickLabel={handleOpenMediaDropdown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="DropdownListTrap">
|
|
||||||
<div
|
|
||||||
className={buildClassName(
|
|
||||||
'DropdownList',
|
|
||||||
isMediaDropdownOpen && 'DropdownList--open',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendPhotos"
|
|
||||||
checked={!permissions.sendPhotos}
|
|
||||||
label={lang('UserRestrictionsSendPhotos')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendPhotos')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendVideos"
|
|
||||||
checked={!permissions.sendVideos}
|
|
||||||
label={lang('UserRestrictionsSendVideos')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendVideos')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendStickers"
|
|
||||||
checked={!permissions.sendStickers && !permissions.sendGifs}
|
|
||||||
label={lang('UserRestrictionsSendStickers')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendStickers')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendAudios"
|
|
||||||
checked={!permissions.sendAudios}
|
|
||||||
label={lang('UserRestrictionsSendMusic')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendAudios')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendDocs"
|
|
||||||
checked={!permissions.sendDocs}
|
|
||||||
label={lang('UserRestrictionsSendFiles')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendDocs')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendVoices"
|
|
||||||
checked={!permissions.sendVoices}
|
|
||||||
label={lang('UserRestrictionsSendVoices')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendVoices')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendRoundvideos"
|
|
||||||
checked={!permissions.sendRoundvideos}
|
|
||||||
label={lang('UserRestrictionsSendRound')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendRoundvideos')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="embedLinks"
|
|
||||||
checked={!permissions.embedLinks}
|
|
||||||
label={lang('UserRestrictionsEmbedLinks')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('embedLinks')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="sendPolls"
|
|
||||||
checked={!permissions.sendPolls}
|
|
||||||
label={lang('UserRestrictionsSendPolls')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('sendPolls')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={buildClassName('part', isMediaDropdownOpen && 'shifted')}>
|
|
||||||
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="inviteUsers"
|
|
||||||
checked={!permissions.inviteUsers}
|
|
||||||
label={lang('UserRestrictionsInviteUsers')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('inviteUsers')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="pinMessages"
|
|
||||||
checked={!permissions.pinMessages}
|
|
||||||
label={lang('UserRestrictionsPinMessages')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('pinMessages')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="changeInfo"
|
|
||||||
checked={!permissions.changeInfo}
|
|
||||||
label={lang('UserRestrictionsChangeInfo')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('changeInfo')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isForum && (
|
|
||||||
<div className="ListItem">
|
|
||||||
<Checkbox
|
|
||||||
name="manageTopics"
|
|
||||||
checked={!permissions.manageTopics}
|
|
||||||
label={lang('CreateTopicsPermission')}
|
|
||||||
blocking
|
|
||||||
disabled={getControlIsDisabled('manageTopics')}
|
|
||||||
onChange={handlePermissionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
dropdownClassName="DropdownListTrap"
|
||||||
|
shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')}
|
||||||
|
getControlIsDisabled={getControlIsDisabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isFormFullyDisabled && (
|
{!isFormFullyDisabled && (
|
||||||
@ -334,7 +165,7 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ListItem icon="delete-user" ripple destructive onClick={openBanConfirmationDialog}>
|
<ListItem icon="delete-user" ripple destructive onClick={openBanConfirmationDialog}>
|
||||||
{lang('UserRestrictionsBlock')}
|
{oldLang('UserRestrictionsBlock')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -343,7 +174,7 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
isShown={havePermissionChanged}
|
isShown={havePermissionChanged}
|
||||||
onClick={handleSavePermissions}
|
onClick={handleSavePermissions}
|
||||||
ariaLabel={lang('Save')}
|
ariaLabel={oldLang('Save')}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -356,7 +187,7 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={isBanConfirmationDialogOpen}
|
isOpen={isBanConfirmationDialogOpen}
|
||||||
onClose={closeBanConfirmationDialog}
|
onClose={closeBanConfirmationDialog}
|
||||||
text="Are you sure you want to ban and remove this user from the group?"
|
text={lang('GroupManagementBanUserConfirm')}
|
||||||
confirmLabel="Remove"
|
confirmLabel="Remove"
|
||||||
confirmHandler={handleBanFromGroup}
|
confirmHandler={handleBanFromGroup}
|
||||||
confirmIsDestructive
|
confirmIsDestructive
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
var(--color-primary)
|
var(--color-primary)
|
||||||
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLjkuOEw1LjggOC45IDIuMSA1LjJjLS40LS40LTEuMS0uNC0xLjYgMC0uNC40LS40IDEuMSAwIDEuNkw1IDExLjJjLjQuNCAxLjEuNCAxLjYgMGw4LjktOC45Yy40LS40LjQtMS4xIDAtMS42LS41LS40LTEuMi0uNC0xLjYuMXoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=)
|
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLjkuOEw1LjggOC45IDIuMSA1LjJjLS40LS40LTEuMS0uNC0xLjYgMC0uNC40LS40IDEuMSAwIDEuNkw1IDExLjJjLjQuNCAxLjEuNCAxLjYgMGw4LjktOC45Yy40LS40LjQtMS4xIDAtMS42LS41LS40LTEuMi0uNC0xLjYuMXoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=)
|
||||||
no-repeat 50% 50%;
|
no-repeat 50% 50%;
|
||||||
background-size: 12px;
|
background-size: 0.75rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,14 +75,14 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 1.1875rem;
|
left: 0.6875rem;
|
||||||
top: 0.1875rem;
|
top: 0.1875rem;
|
||||||
width: 1.125rem;
|
width: 1.125rem;
|
||||||
height: 1.125rem;
|
height: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
border: 2px solid var(--color-borders-input);
|
border: 0.125rem solid var(--color-borders-input);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
transition: border-color 0.1s ease, background-color 0.1s ease;
|
transition: border-color 0.1s ease, background-color 0.1s ease;
|
||||||
@ -120,6 +120,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Nested-avatar-list {
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 4.3125rem;
|
||||||
|
top: 0.625rem;
|
||||||
|
width: 1.125rem;
|
||||||
|
height: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
column-gap: 0.6875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input:checked ~ .Checkbox-main {
|
input:checked ~ .Checkbox-main {
|
||||||
&::before {
|
&::before {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
@ -155,4 +173,64 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.nested {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3125rem;
|
||||||
|
margin-bottom: 0.5625rem;
|
||||||
|
padding-left: 4.1875rem;
|
||||||
|
.Checkbox-main {
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
top: 0.875rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.permission-group {
|
||||||
|
padding-left: 4rem;
|
||||||
|
.Checkbox-main {
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
left: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.avatar {
|
||||||
|
padding-left: 6.625rem;
|
||||||
|
margin-bottom: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: var(--color-text);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-icon {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nested-checkbox-group &.Checkbox-main::before,
|
||||||
|
&.nested-checkbox-group &.Checkbox-main::after {
|
||||||
|
top: 1.875rem;
|
||||||
|
left: 2.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-checkbox-group {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: max-height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-checkbox-group-open {
|
||||||
|
max-height: 100vh;
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: max-height 0.6s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||||
import React, { memo, useCallback, useRef } from '../../lib/teact/teact';
|
import React, {
|
||||||
|
memo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { IconName } from '../../types/icons';
|
import type { IconName } from '../../types/icons';
|
||||||
|
import type { IRadioOption } from './CheckboxGroup';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import renderText from '../common/helpers/renderText';
|
import renderText from '../common/helpers/renderText';
|
||||||
|
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import Icon from '../common/icons/Icon';
|
||||||
|
import Button from './Button';
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
|
|
||||||
import './Checkbox.scss';
|
import './Checkbox.scss';
|
||||||
@ -18,6 +26,7 @@ type OwnProps = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
label: TeactNode;
|
label: TeactNode;
|
||||||
|
labelText?: TeactNode;
|
||||||
subLabel?: string;
|
subLabel?: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
rightIcon?: IconName;
|
rightIcon?: IconName;
|
||||||
@ -25,12 +34,18 @@ type OwnProps = {
|
|||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
round?: boolean;
|
round?: boolean;
|
||||||
blocking?: boolean;
|
blocking?: boolean;
|
||||||
|
permissionGroup?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
withCheckedCallback?: boolean;
|
withCheckedCallback?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (e: ChangeEvent<HTMLInputElement>, nestedOptionList?: IRadioOption) => void;
|
||||||
onCheck?: (isChecked: boolean) => void;
|
onCheck?: (isChecked: boolean) => void;
|
||||||
onClickLabel?: (e: React.MouseEvent, value?: string) => void;
|
onClickLabel?: (e: React.MouseEvent, value?: string) => void;
|
||||||
|
nestedCheckbox?: boolean;
|
||||||
|
nestedCheckboxCount?: number | undefined;
|
||||||
|
nestedOptionList?: IRadioOption;
|
||||||
|
leftElement?: TeactNode;
|
||||||
|
values?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const Checkbox: FC<OwnProps> = ({
|
const Checkbox: FC<OwnProps> = ({
|
||||||
@ -38,37 +53,48 @@ const Checkbox: FC<OwnProps> = ({
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
|
labelText,
|
||||||
subLabel,
|
subLabel,
|
||||||
checked,
|
checked,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
disabled,
|
disabled,
|
||||||
round,
|
round,
|
||||||
blocking,
|
blocking,
|
||||||
|
permissionGroup,
|
||||||
isLoading,
|
isLoading,
|
||||||
className,
|
className,
|
||||||
rightIcon,
|
rightIcon,
|
||||||
onChange,
|
onChange,
|
||||||
onCheck,
|
onCheck,
|
||||||
onClickLabel,
|
onClickLabel,
|
||||||
|
nestedCheckbox,
|
||||||
|
nestedCheckboxCount,
|
||||||
|
nestedOptionList,
|
||||||
|
leftElement,
|
||||||
|
values = [],
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const labelRef = useRef<HTMLLabelElement>(null);
|
const labelRef = useRef<HTMLLabelElement>(null);
|
||||||
|
const [showNested, setShowNested] = useState(false);
|
||||||
|
|
||||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useLastCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(event);
|
onChange(event, nestedOptionList);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onCheck) {
|
if (onCheck) {
|
||||||
onCheck(event.currentTarget.checked);
|
onCheck(event.currentTarget.checked);
|
||||||
}
|
}
|
||||||
}, [disabled, onChange, onCheck]);
|
});
|
||||||
|
|
||||||
|
const toggleNested = useLastCallback(() => {
|
||||||
|
setShowNested(!showNested);
|
||||||
|
});
|
||||||
|
|
||||||
function handleClick(event: React.MouseEvent) {
|
function handleClick(event: React.MouseEvent) {
|
||||||
if (event.target !== labelRef.current) {
|
if (event.target !== labelRef.current) {
|
||||||
@ -86,37 +112,70 @@ const Checkbox: FC<OwnProps> = ({
|
|||||||
round && 'round',
|
round && 'round',
|
||||||
isLoading && 'loading',
|
isLoading && 'loading',
|
||||||
blocking && 'blocking',
|
blocking && 'blocking',
|
||||||
|
nestedCheckbox && 'nested',
|
||||||
|
permissionGroup && 'permission-group',
|
||||||
|
Boolean(leftElement) && 'avatar',
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
<>
|
||||||
<label
|
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
||||||
className={labelClassName}
|
<label
|
||||||
dir={lang.isRtl ? 'rtl' : undefined}
|
className={labelClassName}
|
||||||
onClick={onClickLabel ? handleClick : undefined}
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
ref={labelRef}
|
onClick={onClickLabel ? handleClick : undefined}
|
||||||
>
|
ref={labelRef}
|
||||||
<input
|
>
|
||||||
type="checkbox"
|
<input
|
||||||
id={id}
|
type="checkbox"
|
||||||
name={name}
|
id={id}
|
||||||
value={value}
|
name={name}
|
||||||
checked={checked}
|
value={value}
|
||||||
disabled={disabled}
|
checked={checked}
|
||||||
tabIndex={tabIndex}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
tabIndex={tabIndex}
|
||||||
onClick={onClickLabel ? handleInputClick : undefined}
|
onChange={handleChange}
|
||||||
/>
|
onClick={onClickLabel ? handleInputClick : undefined}
|
||||||
<div className="Checkbox-main">
|
/>
|
||||||
<span className="label" dir="auto">
|
<div className={buildClassName('Checkbox-main', Boolean(leftElement) && 'Nested-avatar-list')}>
|
||||||
{typeof label === 'string' ? renderText(label) : label}
|
<span className="label" dir="auto">
|
||||||
{rightIcon && <i className={`icon icon-${rightIcon} right-icon`} />}
|
{leftElement}
|
||||||
</span>
|
{typeof label === 'string' ? renderText(label) : label}
|
||||||
{subLabel && <span className="subLabel" dir="auto">{renderText(subLabel)}</span>}
|
{labelText && <span className="ml-1">{renderText(labelText)}</span>}
|
||||||
</div>
|
{rightIcon && <i className={`icon icon-${rightIcon} right-icon`} />}
|
||||||
{isLoading && <Spinner />}
|
</span>
|
||||||
</label>
|
{subLabel && <span className="subLabel" dir="auto">{renderText(subLabel)}</span>}
|
||||||
|
</div>
|
||||||
|
{nestedCheckbox && (
|
||||||
|
<span className="nestedButton" dir="auto">
|
||||||
|
<Button className="button" color="translucent" size="smaller" onClick={toggleNested}>
|
||||||
|
<Icon name="group-filled" className="group-icon" />
|
||||||
|
{nestedCheckboxCount}
|
||||||
|
<Icon name={showNested ? 'up' : 'down'} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isLoading && <Spinner />}
|
||||||
|
</label>
|
||||||
|
{nestedCheckbox && (
|
||||||
|
<div
|
||||||
|
className={buildClassName('nested-checkbox-group', showNested && 'nested-checkbox-group-open')}
|
||||||
|
>
|
||||||
|
{nestedOptionList?.nestedOptions?.map((nestedOption) => (
|
||||||
|
<Checkbox
|
||||||
|
key={nestedOption.value}
|
||||||
|
leftElement={leftElement}
|
||||||
|
onChange={handleChange}
|
||||||
|
checked={values.indexOf(nestedOption.value) !== -1}
|
||||||
|
values={values}
|
||||||
|
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||||
|
{...nestedOption}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||||
import React, { memo, useCallback, useState } from '../../lib/teact/teact';
|
import React, { memo, useState } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiUser } from '../../api/types';
|
||||||
|
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
|
||||||
import Checkbox from './Checkbox';
|
import Checkbox from './Checkbox';
|
||||||
|
|
||||||
@ -9,6 +13,8 @@ export type IRadioOption = {
|
|||||||
subLabel?: string;
|
subLabel?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
|
nestedOptions?: IRadioOption[];
|
||||||
|
user?: ApiUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -17,6 +23,7 @@ type OwnProps = {
|
|||||||
selected?: string[];
|
selected?: string[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
round?: boolean;
|
round?: boolean;
|
||||||
|
nestedCheckbox?: boolean;
|
||||||
loadingOptions?: string[];
|
loadingOptions?: string[];
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
};
|
};
|
||||||
@ -27,38 +34,71 @@ const CheckboxGroup: FC<OwnProps> = ({
|
|||||||
selected = [],
|
selected = [],
|
||||||
disabled,
|
disabled,
|
||||||
round,
|
round,
|
||||||
|
nestedCheckbox,
|
||||||
loadingOptions,
|
loadingOptions,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [values, setValues] = useState<string[]>(selected || []);
|
const [values, setValues] = useState<string[]>(selected || []);
|
||||||
|
|
||||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useLastCallback((event: ChangeEvent<HTMLInputElement>, nestedOptionList?: IRadioOption) => {
|
||||||
const { value, checked } = event.currentTarget;
|
const { value, checked } = event.currentTarget;
|
||||||
let newValues: string[];
|
let newValues: string[];
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
newValues = [...values, value];
|
newValues = [...values, value];
|
||||||
|
if (nestedOptionList && value) {
|
||||||
|
newValues.push(nestedOptionList.value);
|
||||||
|
}
|
||||||
|
if (nestedOptionList && value === nestedOptionList.value) {
|
||||||
|
nestedOptionList.nestedOptions?.forEach((nestedOption) => {
|
||||||
|
if (!newValues.includes(nestedOption.value)) {
|
||||||
|
newValues.push(nestedOption.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newValues = values.filter((v) => v !== value);
|
newValues = values.filter((v) => v !== value);
|
||||||
|
if (nestedOptionList && value === nestedOptionList.value) {
|
||||||
|
nestedOptionList.nestedOptions?.forEach((nestedOption) => {
|
||||||
|
newValues = newValues.filter((v) => v !== nestedOption.value);
|
||||||
|
});
|
||||||
|
} else if (nestedOptionList) {
|
||||||
|
const nestedOptionValues = nestedOptionList.nestedOptions?.map((nestedOption) => nestedOption.value) || [];
|
||||||
|
const hasOtherNestedValuesChecked = nestedOptionValues.some((nestedValue) => newValues.includes(nestedValue));
|
||||||
|
if (!hasOtherNestedValuesChecked) {
|
||||||
|
newValues = newValues.filter((v) => v !== nestedOptionList.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setValues(newValues);
|
setValues(newValues);
|
||||||
onChange(newValues);
|
onChange(newValues);
|
||||||
}, [onChange, values]);
|
});
|
||||||
|
const getCheckedNestedCount = useLastCallback((nestedOptions: IRadioOption[]) => {
|
||||||
|
const checkedCount = nestedOptions?.filter((nestedOption) => values.includes(nestedOption.value)).length;
|
||||||
|
return checkedCount > 0 ? checkedCount : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={id} className="radio-group">
|
<div id={id} className="radio-group">
|
||||||
{options.map((option) => (
|
{options.map((option) => {
|
||||||
<Checkbox
|
return (
|
||||||
label={option.label}
|
<Checkbox
|
||||||
subLabel={option.subLabel}
|
label={option.label}
|
||||||
value={option.value}
|
subLabel={option.subLabel}
|
||||||
checked={selected.indexOf(option.value) !== -1}
|
value={option.value}
|
||||||
disabled={option.disabled || disabled}
|
checked={selected.indexOf(option.value) !== -1}
|
||||||
round={round}
|
disabled={option.disabled || disabled}
|
||||||
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
round={round}
|
||||||
onChange={handleChange}
|
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
||||||
/>
|
onChange={handleChange}
|
||||||
))}
|
nestedCheckbox={nestedCheckbox}
|
||||||
|
nestedCheckboxCount={getCheckedNestedCount(option.nestedOptions ?? [])}
|
||||||
|
nestedOptionList={option}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -958,3 +958,29 @@ function copyTextForMessages(global: GlobalState, chatId: string, messageIds: nu
|
|||||||
|
|
||||||
copyHtmlToClipboard(resultHtml.join('\n'), resultText.join('\n'));
|
copyHtmlToClipboard(resultHtml.join('\n'), resultText.join('\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addActionHandler('openDeleteMessageModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
message, isSchedule, album,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
|
||||||
|
global = updateTabState(global, {
|
||||||
|
deleteMessageModal: {
|
||||||
|
isSchedule,
|
||||||
|
album,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeDeleteMessageModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
deleteMessageModal: undefined,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|||||||
@ -423,6 +423,42 @@ export function selectSender<T extends GlobalState>(global: T, message: ApiMessa
|
|||||||
return selectPeer(global, senderId);
|
return selectPeer(global, senderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectSendersFromSelectedMessages<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
chat: ApiChat | undefined,
|
||||||
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
|
) {
|
||||||
|
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {};
|
||||||
|
if (!chat?.id || !selectedMessageIds) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedMessageIds.map((id) => {
|
||||||
|
const message = selectChatMessage(global, chat.id, id);
|
||||||
|
if (!message?.senderId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return selectSender(global, message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectSenderFromMessage<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
chat: ApiChat | undefined,
|
||||||
|
selectedMessageId: number | undefined,
|
||||||
|
): ApiPeer | undefined {
|
||||||
|
if (!chat?.id || !selectedMessageId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = selectChatMessage(global, chat.id, selectedMessageId);
|
||||||
|
if (!message?.senderId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectPeer(global, message.senderId);
|
||||||
|
}
|
||||||
|
|
||||||
export function selectReplySender<T extends GlobalState>(
|
export function selectReplySender<T extends GlobalState>(
|
||||||
global: T, message: ApiMessage,
|
global: T, message: ApiMessage,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -100,6 +100,7 @@ import type {
|
|||||||
EmojiKeywords,
|
EmojiKeywords,
|
||||||
FocusDirection,
|
FocusDirection,
|
||||||
GlobalSearchContent,
|
GlobalSearchContent,
|
||||||
|
IAlbum,
|
||||||
IAnchorPosition,
|
IAnchorPosition,
|
||||||
InlineBotSettings,
|
InlineBotSettings,
|
||||||
ISettings,
|
ISettings,
|
||||||
@ -691,6 +692,13 @@ export type TabState = {
|
|||||||
prepaidGiveaway?: ApiPrepaidGiveaway;
|
prepaidGiveaway?: ApiPrepaidGiveaway;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
deleteMessageModal?: {
|
||||||
|
message?: ApiMessage;
|
||||||
|
isSchedule?: boolean;
|
||||||
|
album?: IAlbum;
|
||||||
|
onConfirm?: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
giftingModal?: {
|
giftingModal?: {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
};
|
};
|
||||||
@ -3125,6 +3133,14 @@ export interface ActionPayloads {
|
|||||||
openPremiumGiftingModal: WithTabId | undefined;
|
openPremiumGiftingModal: WithTabId | undefined;
|
||||||
closePremiumGiftingModal: WithTabId | undefined;
|
closePremiumGiftingModal: WithTabId | undefined;
|
||||||
|
|
||||||
|
openDeleteMessageModal: ({
|
||||||
|
message?: ApiMessage;
|
||||||
|
isSchedule?: boolean;
|
||||||
|
album?: IAlbum;
|
||||||
|
onConfirm?: NoneToVoidFunction;
|
||||||
|
} & WithTabId);
|
||||||
|
closeDeleteMessageModal: WithTabId | undefined;
|
||||||
|
|
||||||
transcribeAudio: {
|
transcribeAudio: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user