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 Spinner from '../ui/Spinner';
|
||||
import Avatar from './Avatar';
|
||||
import DeleteMessageModal from './DeleteMessageModal.async';
|
||||
import Icon from './icons/Icon';
|
||||
import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji';
|
||||
|
||||
@ -586,7 +585,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag();
|
||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||
const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag();
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||
const [isHoverDisabled, disableHover, enableHover] = useFlag();
|
||||
|
||||
const {
|
||||
@ -756,7 +754,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
setHtml,
|
||||
editingMessage,
|
||||
resetComposer,
|
||||
openDeleteModal,
|
||||
chatId,
|
||||
threadId,
|
||||
messageListType,
|
||||
@ -1405,7 +1402,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}, [isInStoryViewer, slowMode?.nextSendDate, stealthMode?.activeUntil]);
|
||||
|
||||
const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen
|
||||
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isDeleteModalOpen || isBotCommandMenuOpen || isAttachMenuOpen
|
||||
|| isMentionTooltipOpen || isInlineBotTooltipOpen || isBotCommandMenuOpen || isAttachMenuOpen
|
||||
|| isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen
|
||||
|| isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus;
|
||||
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();
|
||||
scheduledDefaultDate.setSeconds(0);
|
||||
scheduledDefaultDate.setMilliseconds(0);
|
||||
@ -1663,14 +1657,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onClear={closePollModal}
|
||||
onSend={handlePollSend}
|
||||
/>
|
||||
{renderedEditedMessage && (
|
||||
<DeleteMessageModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
isSchedule={messageListType === 'scheduled'}
|
||||
onClose={closeDeleteModal}
|
||||
message={renderedEditedMessage}
|
||||
/>
|
||||
)}
|
||||
<SendAsMenu
|
||||
isOpen={isSendAsMenuOpen}
|
||||
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 React, { memo, useCallback } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import React, {
|
||||
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 { IRadioOption } from '../ui/CheckboxGroup';
|
||||
|
||||
import { REPLIES_USER_ID } from '../../config';
|
||||
import {
|
||||
getHasAdminRight,
|
||||
getPrivateChatUserId,
|
||||
getUserFirstOrLastName,
|
||||
isChatBasicGroup,
|
||||
isChatSuperGroup,
|
||||
getUserFirstOrLastName, getUserFullName,
|
||||
isChatBasicGroup, isChatChannel,
|
||||
isChatSuperGroup, isOwnMessage,
|
||||
isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectAllowedMessageActions,
|
||||
selectBot,
|
||||
selectChat,
|
||||
selectCurrentMessageList,
|
||||
selectChat, selectChatFullInfo, selectCurrentMessageIds,
|
||||
selectCurrentMessageList, selectSenderFromMessage, selectTabState,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
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 CheckboxGroup from '../ui/CheckboxGroup';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import Modal from '../ui/Modal';
|
||||
import Avatar from './Avatar';
|
||||
import Icon from './icons/Icon';
|
||||
|
||||
import styles from './DeleteMessageModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
isSchedule: boolean;
|
||||
message: ApiMessage;
|
||||
album?: IAlbum;
|
||||
onClose: NoneToVoidFunction;
|
||||
onConfirm?: NoneToVoidFunction;
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
isChannel?: boolean;
|
||||
isGroup?: boolean;
|
||||
isSuperGroup?: boolean;
|
||||
sender: ApiPeer | undefined;
|
||||
currentUserId?: string;
|
||||
canDeleteForAll?: boolean;
|
||||
contactName?: string;
|
||||
willDeleteForCurrentUserOnly?: 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> = ({
|
||||
isOpen,
|
||||
chat,
|
||||
isChannel,
|
||||
isGroup,
|
||||
isSuperGroup,
|
||||
sender,
|
||||
currentUserId,
|
||||
messageIdList,
|
||||
isSchedule,
|
||||
message,
|
||||
album,
|
||||
@ -52,89 +89,392 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
willDeleteForCurrentUserOnly,
|
||||
willDeleteForAll,
|
||||
onConfirm,
|
||||
onClose,
|
||||
adminMembersById,
|
||||
chatBot,
|
||||
isOwn,
|
||||
canBanUsers,
|
||||
}) => {
|
||||
const {
|
||||
deleteMessages,
|
||||
deleteScheduledMessages,
|
||||
reportMessages,
|
||||
deleteChatMember,
|
||||
updateChatMemberBannedRights,
|
||||
closeDeleteMessageModal,
|
||||
} = getActions();
|
||||
|
||||
const handleDeleteMessageForAll = useCallback(() => {
|
||||
onConfirm?.();
|
||||
const messageIds = album?.messages
|
||||
? album.messages.map(({ id }) => id)
|
||||
: [message.id];
|
||||
deleteMessages({ messageIds, shouldDeleteForAll: true });
|
||||
onClose();
|
||||
}, [onConfirm, album, message.id, deleteMessages, onClose]);
|
||||
const prevIsOpen = usePrevious(isOpen);
|
||||
|
||||
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?.();
|
||||
const messageIds = album?.messages
|
||||
? 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) {
|
||||
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 {
|
||||
deleteMessages({
|
||||
messageIds,
|
||||
shouldDeleteForAll: false,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}, [onConfirm, album, message.id, isSchedule, onClose, deleteScheduledMessages, deleteMessages]);
|
||||
closeDeleteMessageModal();
|
||||
});
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onEnter={isOpen && !canDeleteForAll ? handleDeleteMessageForSelf : undefined}
|
||||
onClose={handleClose}
|
||||
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
|
||||
className="delete"
|
||||
title={lang('DeleteSingleMessagesTitle')}
|
||||
>
|
||||
<p>{lang('AreYouSureDeleteSingleMessage')}</p>
|
||||
{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' : 'dialog-buttons'}>
|
||||
{canDeleteForAll && (
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||
{contactName && renderText(lang('Conversation.DeleteMessagesFor', contactName))}
|
||||
{!contactName && lang('Conversation.DeleteMessagesForEveryone')}
|
||||
</Button>
|
||||
<div className={buildClassName(styles.mainContainer, 'custom-scroll')}>
|
||||
{renderHeader()}
|
||||
{(shouldShowAdditionalOptions && !canDeleteForAll && !isSchedule && (isChannel || isGroup || isSuperGroup)) && (
|
||||
<>
|
||||
<p className={styles.actionTitle}>{lang('DeleteAdditionalActions')}</p>
|
||||
{renderAdditionalActionOptions()}
|
||||
{renderPartiallyRestrictedUser()}
|
||||
{
|
||||
chosenBanOption && canBanUsers && chosenBanOption?.length ? (
|
||||
<ListItem
|
||||
narrow
|
||||
className={styles.listItemButton}
|
||||
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}>
|
||||
{lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
|
||||
</Button>
|
||||
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
|
||||
{(chatBot || !shouldShowAdditionalOptions) && (
|
||||
<>
|
||||
<p>{lang('AreYouSureDeleteSingleMessage')}</p>
|
||||
{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>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message, isSchedule }): StateProps => {
|
||||
const { threadId } = selectCurrentMessageList(global) || {};
|
||||
const { canDeleteForAll } = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||
const chat = selectChat(global, message.chatId);
|
||||
(global): StateProps => {
|
||||
const {
|
||||
deleteMessageModal,
|
||||
} = 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)
|
||||
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
||||
: 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 willDeleteForAll = chat && isChatSuperGroup(chat);
|
||||
|
||||
return {
|
||||
canDeleteForAll: !isSchedule && canDeleteForAll,
|
||||
chat,
|
||||
isChannel,
|
||||
isGroup,
|
||||
isSuperGroup,
|
||||
currentUserId: global.currentUserId,
|
||||
sender,
|
||||
messageIdList,
|
||||
canDeleteForAll: deleteMessageModal && !deleteMessageModal.isSchedule && canDeleteForAll,
|
||||
contactName,
|
||||
willDeleteForCurrentUserOnly,
|
||||
willDeleteForAll,
|
||||
adminMembersById,
|
||||
chatBot,
|
||||
isSchedule: deleteMessageModal && deleteMessageModal.isSchedule,
|
||||
message: deleteMessageModal && deleteMessageModal.message,
|
||||
album: deleteMessageModal && deleteMessageModal.album,
|
||||
onConfirm: deleteMessageModal && deleteMessageModal.onConfirm,
|
||||
isOwn,
|
||||
canBanUsers,
|
||||
};
|
||||
},
|
||||
)(DeleteMessageModal));
|
||||
|
||||
@ -64,6 +64,7 @@ import GroupCall from '../calls/group/GroupCall.async';
|
||||
import PhoneCall from '../calls/phone/PhoneCall.async';
|
||||
import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async';
|
||||
import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
||||
import DeleteMessageModal from '../common/DeleteMessageModal.async';
|
||||
import StickerSetModal from '../common/StickerSetModal.async';
|
||||
import UnreadCount from '../common/UnreadCounter';
|
||||
import LeftColumn from '../left/LeftColumn';
|
||||
@ -143,6 +144,7 @@ type StateProps = {
|
||||
isReactionPickerOpen: boolean;
|
||||
isAppendModalOpen?: boolean;
|
||||
isGiveawayModalOpen?: boolean;
|
||||
isDeleteMessageModalOpen?: boolean;
|
||||
isPremiumGiftingModalOpen?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
noRightColumnAnimation?: boolean;
|
||||
@ -192,6 +194,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
requestedDraft,
|
||||
isPremiumModalOpen,
|
||||
isGiveawayModalOpen,
|
||||
isDeleteMessageModalOpen,
|
||||
isPremiumGiftingModalOpen,
|
||||
isPaymentModalOpen,
|
||||
isReceiptModalOpen,
|
||||
@ -577,6 +580,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||
<DeleteFolderDialog folder={deleteFolderDialog} />
|
||||
<ReactionPicker isOpen={isReactionPickerOpen} />
|
||||
<DeleteMessageModal isOpen={isDeleteMessageModalOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -610,6 +614,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
ratingPhoneCall,
|
||||
premiumModal,
|
||||
giveawayModal,
|
||||
deleteMessageModal,
|
||||
giftingModal,
|
||||
isMasterTab,
|
||||
payment,
|
||||
@ -665,6 +670,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
isPremiumModalOpen: premiumModal?.isOpen,
|
||||
isGiveawayModalOpen: giveawayModal?.isOpen,
|
||||
isDeleteMessageModalOpen: Boolean(deleteMessageModal),
|
||||
isPremiumGiftingModalOpen: giftingModal?.isOpen,
|
||||
limitReached: limitReachedModal?.limit,
|
||||
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 useZoomChange from './hooks/useZoomChangeSignal';
|
||||
|
||||
import DeleteMessageModal from '../common/DeleteMessageModal';
|
||||
import DeleteProfilePhotoModal from '../common/DeleteProfilePhotoModal';
|
||||
import Button from '../ui/Button';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
@ -90,6 +89,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
updateProfilePhoto,
|
||||
updateChatPhoto,
|
||||
openMediaViewer,
|
||||
openDeleteMessageModal,
|
||||
} = getActions();
|
||||
|
||||
const isMessage = item?.type === 'message';
|
||||
@ -163,31 +163,16 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}, []);
|
||||
|
||||
function renderDeleteModals() {
|
||||
if (item?.type === 'message') {
|
||||
return (
|
||||
<DeleteMessageModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
isSchedule={messageListType === 'scheduled'}
|
||||
onClose={closeDeleteModal}
|
||||
onConfirm={onBeforeDelete}
|
||||
message={item.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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 renderDeleteModal() {
|
||||
return (item?.type === 'avatar') ? (
|
||||
<DeleteProfilePhotoModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={closeDeleteModal}
|
||||
onConfirm={onBeforeDelete}
|
||||
profileId={item.avatarOwner.id}
|
||||
photo={item.avatarOwner.photos![item.mediaIndex!]}
|
||||
/>
|
||||
) : undefined;
|
||||
}
|
||||
|
||||
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) {
|
||||
const menuItems: MenuItemProps[] = [];
|
||||
if (isMessage && item.message.isForwardingAllowed && !item.message.content.action && !isChatProtected) {
|
||||
@ -268,7 +264,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
if (canDelete) {
|
||||
menuItems.push({
|
||||
icon: 'delete',
|
||||
onClick: openDeleteModal,
|
||||
onClick: openDeleteModalHandler,
|
||||
children: lang('Delete'),
|
||||
destructive: true,
|
||||
});
|
||||
@ -300,7 +296,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
))}
|
||||
</DropdownMenu>
|
||||
{isDownloading && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
|
||||
{canDelete && renderDeleteModals()}
|
||||
{canDelete && renderDeleteModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -365,7 +361,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
size="smaller"
|
||||
color="translucent-white"
|
||||
ariaLabel={lang('Delete')}
|
||||
onClick={openDeleteModal}
|
||||
onClick={openDeleteModalHandler}
|
||||
>
|
||||
<i className="icon icon-delete" />
|
||||
</Button>
|
||||
@ -379,7 +375,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<i className="icon icon-close" />
|
||||
</Button>
|
||||
{canDelete && renderDeleteModals()}
|
||||
{canDelete && renderDeleteModal()}
|
||||
</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 React, { memo, useEffect } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import React, {
|
||||
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 {
|
||||
getHasAdminRight,
|
||||
getPrivateChatUserId,
|
||||
getUserFirstOrLastName,
|
||||
getUserFullName,
|
||||
isChatBasicGroup,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectCanDeleteSelectedMessages, selectCurrentChat, selectTabState, selectUser,
|
||||
selectCanDeleteSelectedMessages,
|
||||
selectChatFullInfo,
|
||||
selectCurrentChat,
|
||||
selectCurrentMessageIds,
|
||||
selectCurrentMessageList,
|
||||
selectSenderFromMessage,
|
||||
selectSendersFromSelectedMessages,
|
||||
selectTabState,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { buildCollectionByCallback } from '../../util/iteratees';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
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 CheckboxGroup from '../ui/CheckboxGroup';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
import styles from './DeleteSelectedMessageModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
isSchedule: boolean;
|
||||
@ -28,39 +59,226 @@ export type OwnProps = {
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
isChannel?: boolean;
|
||||
isGroup?: boolean;
|
||||
isSuperGroup?: boolean;
|
||||
selectedMessageIds?: number[];
|
||||
canDeleteForAll?: boolean;
|
||||
contactName?: string;
|
||||
currentUserId?: string;
|
||||
willDeleteForCurrentUserOnly?: boolean;
|
||||
willDeleteForAll?: boolean;
|
||||
messageIds: number[] | undefined;
|
||||
adminMembersById?: Record<string, ApiChatMember>;
|
||||
canBanUsers?: boolean;
|
||||
};
|
||||
|
||||
const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
|
||||
chat,
|
||||
isChannel,
|
||||
isGroup,
|
||||
isSuperGroup,
|
||||
isOpen,
|
||||
isSchedule,
|
||||
currentUserId,
|
||||
selectedMessageIds,
|
||||
canDeleteForAll,
|
||||
contactName,
|
||||
willDeleteForCurrentUserOnly,
|
||||
willDeleteForAll,
|
||||
messageIds,
|
||||
onClose,
|
||||
adminMembersById,
|
||||
canBanUsers,
|
||||
}) => {
|
||||
const {
|
||||
deleteMessages,
|
||||
reportMessages,
|
||||
deleteChatMember,
|
||||
deleteScheduledMessages,
|
||||
exitMessageSelectMode,
|
||||
updateChatMemberBannedRights,
|
||||
} = getActions();
|
||||
|
||||
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(() => {
|
||||
onClose();
|
||||
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(() => {
|
||||
if (isSchedule) {
|
||||
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 {
|
||||
deleteMessages({ messageIds: selectedMessageIds!, shouldDeleteForAll: false });
|
||||
}
|
||||
@ -68,14 +286,97 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
|
||||
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(() => {
|
||||
if (!isOpen && prevIsOpen) {
|
||||
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) {
|
||||
return undefined;
|
||||
@ -87,27 +388,50 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
|
||||
onClose={onClose}
|
||||
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
|
||||
className="delete"
|
||||
title={lang('Conversation.DeleteManyMessages')}
|
||||
>
|
||||
<p>{lang('AreYouSureDeleteFewMessages')}</p>
|
||||
{willDeleteForCurrentUserOnly && (
|
||||
<p>This will delete them just for you, not for other participants in the chat.</p>
|
||||
)}
|
||||
{willDeleteForAll && (
|
||||
<p>This will delete them for everyone in this chat.</p>
|
||||
)}
|
||||
<div className={canDeleteForAll ? 'dialog-buttons-column' : 'dialog-buttons'}>
|
||||
{canDeleteForAll && (
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||
{contactName
|
||||
? renderText(lang('ChatList.DeleteForEveryone', contactName))
|
||||
: lang('Conversation.DeleteMessagesForEveryone')}
|
||||
</Button>
|
||||
<div className={styles.main}>
|
||||
{renderHeader()}
|
||||
{!showAdditionalOptions && <p>{lang('AreYouSureDeleteFewMessages')}</p>}
|
||||
{(showAdditionalOptions && !canDeleteForAll && !isSchedule && (isChannel || isGroup || isSuperGroup)) && (
|
||||
<>
|
||||
<p className={styles.actionTitle}>{oldLang('DeleteAdditionalActions')}</p>
|
||||
{renderAdditionalActionOptions()}
|
||||
{renderPartiallyRestrictedUser()}
|
||||
{
|
||||
chosenBanOption && canBanUsers && chosenBanOption?.length ? (
|
||||
<ListItem
|
||||
narrow
|
||||
className={styles.listItemButton}
|
||||
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}>
|
||||
{lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
|
||||
</Button>
|
||||
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
|
||||
{willDeleteForCurrentUserOnly && lang('DeleteForMeDescription')}
|
||||
{(willDeleteForAll && !showAdditionalOptions) && lang('DeleteForEveryoneDescription')}
|
||||
<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(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>
|
||||
</Modal>
|
||||
);
|
||||
@ -118,19 +442,34 @@ export default memo(withGlobal<OwnProps>(
|
||||
const { messageIds: selectedMessageIds } = selectTabState(global).selectedMessages || {};
|
||||
const { canDeleteForAll } = selectCanDeleteSelectedMessages(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)
|
||||
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
||||
: undefined;
|
||||
|
||||
const adminMembersById = chatFullInfo?.adminMembersById;
|
||||
const canBanUsers = chat && (chat.isCreator || getHasAdminRight(chat, 'banUsers'));
|
||||
const willDeleteForCurrentUserOnly = chat && isChatBasicGroup(chat) && !canDeleteForAll;
|
||||
const willDeleteForAll = chat && isChatSuperGroup(chat);
|
||||
|
||||
return {
|
||||
chat,
|
||||
isChannel,
|
||||
isGroup,
|
||||
isSuperGroup,
|
||||
selectedMessageIds,
|
||||
currentUserId: global.currentUserId,
|
||||
canDeleteForAll: !isSchedule && canDeleteForAll,
|
||||
contactName,
|
||||
willDeleteForCurrentUserOnly,
|
||||
willDeleteForAll,
|
||||
messageIds,
|
||||
adminMembersById,
|
||||
canBanUsers,
|
||||
};
|
||||
},
|
||||
)(DeleteSelectedMessageModal));
|
||||
|
||||
@ -29,14 +29,15 @@ const useEditing = (
|
||||
setHtml: (html: string) => void,
|
||||
editedMessage: ApiMessage | undefined,
|
||||
resetComposer: (shouldPreserveInput?: boolean) => void,
|
||||
openDeleteModal: () => void,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
type: MessageListType,
|
||||
draft?: ApiDraft,
|
||||
editingDraft?: ApiFormattedText,
|
||||
): [VoidFunction, VoidFunction, boolean] => {
|
||||
const { editMessage, setEditingDraft, toggleMessageWebPage } = getActions();
|
||||
const {
|
||||
editMessage, setEditingDraft, toggleMessageWebPage, openDeleteMessageModal,
|
||||
} = getActions();
|
||||
const [shouldForceShowEditing, setShouldForceShowEditing] = useState(false);
|
||||
|
||||
const replyingToId = draft?.replyInfo?.replyToMsgId;
|
||||
@ -152,7 +153,7 @@ const useEditing = (
|
||||
}
|
||||
|
||||
if (!text && !hasMessageMedia(editedMessage)) {
|
||||
openDeleteModal();
|
||||
openDeleteMessageModal({ isSchedule: type === 'scheduled', message: editedMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -64,7 +64,6 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
import useSchedule from '../../../hooks/useSchedule';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
|
||||
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
||||
import PinMessageModal from '../../common/PinMessageModal.async';
|
||||
import ReportModal from '../../common/ReportModal';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
@ -222,12 +221,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
openPremiumModal,
|
||||
loadOutboxReadDate,
|
||||
copyMessageLink,
|
||||
openDeleteMessageModal,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
||||
@ -333,24 +332,20 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
isMessageTranslated, message.content.text,
|
||||
]);
|
||||
|
||||
const handleDelete = useLastCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsDeleteModalOpen(true);
|
||||
});
|
||||
|
||||
const handleReport = useLastCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsReportModalOpen(true);
|
||||
});
|
||||
|
||||
const closeMenu = useLastCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
onClose();
|
||||
});
|
||||
|
||||
const closeDeleteModal = useLastCallback(() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
onClose();
|
||||
const handleDelete = useLastCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
closeMenu();
|
||||
openDeleteMessageModal({ isSchedule: messageListType === 'scheduled', album, message });
|
||||
});
|
||||
|
||||
const handleReport = useLastCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsReportModalOpen(true);
|
||||
});
|
||||
|
||||
const closeReportModal = useLastCallback(() => {
|
||||
@ -639,13 +634,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
onShowOriginal={handleShowOriginal}
|
||||
onSelectLanguage={handleSelectLanguage}
|
||||
/>
|
||||
<DeleteMessageModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
isSchedule={messageListType === 'scheduled'}
|
||||
onClose={closeDeleteModal}
|
||||
album={album}
|
||||
message={message}
|
||||
/>
|
||||
<ReportModal
|
||||
isOpen={isReportModalOpen}
|
||||
onClose={closeReportModal}
|
||||
|
||||
@ -109,11 +109,17 @@ export default function useManagePermissions(defaultPermissions: ApiChatBannedRi
|
||||
}));
|
||||
}, [defaultPermissions, permissions]);
|
||||
|
||||
const resetPermissions = useCallback(() => {
|
||||
setPermissions(defaultPermissions || {});
|
||||
setHavePermissionChanged(false);
|
||||
}, [defaultPermissions]);
|
||||
|
||||
return {
|
||||
permissions,
|
||||
isLoading,
|
||||
havePermissionChanged,
|
||||
handlePermissionChange,
|
||||
setIsLoading,
|
||||
resetPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,17 +7,15 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import type { ApiChat, ApiChatBannedRights, ApiChatMember } from '../../../api/types';
|
||||
import { ManagementScreens } from '../../../types';
|
||||
|
||||
import { isChatPublic } from '../../../global/helpers';
|
||||
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useManagePermissions from '../hooks/useManagePermissions';
|
||||
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import PermissionCheckboxList from '../../main/PermissionCheckboxList';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
@ -33,7 +31,6 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
currentUserId?: string;
|
||||
hasLinkedChat?: boolean;
|
||||
removedUsersCount: number;
|
||||
members?: ApiChatMember[];
|
||||
};
|
||||
@ -86,21 +83,17 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
|
||||
onChatMemberSelect,
|
||||
chat,
|
||||
currentUserId,
|
||||
hasLinkedChat,
|
||||
removedUsersCount,
|
||||
members,
|
||||
onClose,
|
||||
isActive,
|
||||
}) => {
|
||||
const { updateChatDefaultBannedRights, showNotification } = getActions();
|
||||
const { updateChatDefaultBannedRights } = getActions();
|
||||
|
||||
const {
|
||||
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
|
||||
} = useManagePermissions(chat?.defaultBannedRights);
|
||||
const lang = useOldLang();
|
||||
const { isForum } = chat || {};
|
||||
const isPublic = useMemo(() => chat && isChatPublic(chat), [chat]);
|
||||
const shouldDisablePermissionForPublicGroup = hasLinkedChat || isPublic;
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
@ -121,14 +114,6 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
|
||||
}, [currentUserId, onChatMemberSelect, onScreenSelect]);
|
||||
|
||||
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(() => {
|
||||
if (!chat) {
|
||||
@ -186,174 +171,19 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
|
||||
<div className="custom-scroll">
|
||||
<div className="section without-bottom-shadow">
|
||||
<h3 className="section-heading" dir="auto">{lang('ChannelPermissionsHeader')}</h3>
|
||||
|
||||
<div className="ListItem with-checkbox">
|
||||
<Checkbox
|
||||
name="sendPlain"
|
||||
checked={!permissions.sendPlain}
|
||||
label={lang('UserRestrictionsSend')}
|
||||
blocking
|
||||
onChange={handlePermissionChange}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<PermissionCheckboxList
|
||||
chatId={chat?.id}
|
||||
isMediaDropdownOpen={isMediaDropdownOpen}
|
||||
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
|
||||
handlePermissionChange={handlePermissionChange}
|
||||
permissions={permissions}
|
||||
dropdownClassName="DropdownListTrap"
|
||||
className={buildClassName(
|
||||
'DropdownList',
|
||||
isMediaDropdownOpen && 'DropdownList--open',
|
||||
)}
|
||||
</div>
|
||||
shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -425,12 +255,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const chat = selectChat(global, chatId);
|
||||
const fullInfo = selectChatFullInfo(global, chatId);
|
||||
const hasLinkedChat = Boolean(fullInfo?.linkedChatId);
|
||||
|
||||
return {
|
||||
chat,
|
||||
currentUserId: global.currentUserId,
|
||||
hasLinkedChat,
|
||||
removedUsersCount: fullInfo?.kickedMembers?.length || 0,
|
||||
members: fullInfo?.members,
|
||||
};
|
||||
|
||||
@ -9,15 +9,15 @@ import { ManagementScreens } from '../../../types';
|
||||
|
||||
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useManagePermissions from '../hooks/useManagePermissions';
|
||||
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import PermissionCheckboxList from '../../main/PermissionCheckboxList';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
@ -69,9 +69,8 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
||||
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
|
||||
} = useManagePermissions(selectedChatMember?.bannedRights || chat?.defaultBannedRights);
|
||||
const [isBanConfirmationDialogOpen, openBanConfirmationDialog, closeBanConfirmationDialog] = useFlag();
|
||||
const lang = useOldLang();
|
||||
|
||||
const { isForum } = chat || {};
|
||||
const lang = useLang();
|
||||
const oldLang = useOldLang();
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
@ -124,10 +123,6 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
||||
}, [chat, isFormFullyDisabled]);
|
||||
|
||||
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
||||
const handleOpenMediaDropdown = useCallback((e: React.MouseEvent) => {
|
||||
stopEvent(e);
|
||||
setIsMediaDropdownOpen(!isMediaDropdownOpen);
|
||||
}, [isMediaDropdownOpen]);
|
||||
|
||||
if (!selectedChatMember) {
|
||||
return undefined;
|
||||
@ -145,185 +140,21 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
||||
<PrivateChatInfo userId={selectedChatMember.userId} forceShowSelf />
|
||||
</ListItem>
|
||||
|
||||
<h3 className="section-heading mt-4" dir="auto">{lang('UserRestrictionsCanDo')}</h3>
|
||||
|
||||
<div className="ListItem">
|
||||
<Checkbox
|
||||
name="sendPlain"
|
||||
checked={!permissions.sendPlain}
|
||||
label={lang('UserRestrictionsSend')}
|
||||
blocking
|
||||
disabled={getControlIsDisabled('sendPlain')}
|
||||
onChange={handlePermissionChange}
|
||||
/>
|
||||
</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>
|
||||
<h3 className="section-heading mt-4" dir="auto">{oldLang('UserRestrictionsCanDo')}</h3>
|
||||
<PermissionCheckboxList
|
||||
chatId={chat?.id}
|
||||
isMediaDropdownOpen={isMediaDropdownOpen}
|
||||
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
|
||||
handlePermissionChange={handlePermissionChange}
|
||||
permissions={permissions}
|
||||
className={buildClassName(
|
||||
'DropdownList',
|
||||
isMediaDropdownOpen && 'DropdownList--open',
|
||||
)}
|
||||
</div>
|
||||
dropdownClassName="DropdownListTrap"
|
||||
shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')}
|
||||
getControlIsDisabled={getControlIsDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isFormFullyDisabled && (
|
||||
@ -334,7 +165,7 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
>
|
||||
<ListItem icon="delete-user" ripple destructive onClick={openBanConfirmationDialog}>
|
||||
{lang('UserRestrictionsBlock')}
|
||||
{oldLang('UserRestrictionsBlock')}
|
||||
</ListItem>
|
||||
</div>
|
||||
)}
|
||||
@ -343,7 +174,7 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
||||
<FloatingActionButton
|
||||
isShown={havePermissionChanged}
|
||||
onClick={handleSavePermissions}
|
||||
ariaLabel={lang('Save')}
|
||||
ariaLabel={oldLang('Save')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
@ -356,7 +187,7 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
|
||||
<ConfirmDialog
|
||||
isOpen={isBanConfirmationDialogOpen}
|
||||
onClose={closeBanConfirmationDialog}
|
||||
text="Are you sure you want to ban and remove this user from the group?"
|
||||
text={lang('GroupManagementBanUserConfirm')}
|
||||
confirmLabel="Remove"
|
||||
confirmHandler={handleBanFromGroup}
|
||||
confirmIsDestructive
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
var(--color-primary)
|
||||
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLjkuOEw1LjggOC45IDIuMSA1LjJjLS40LS40LTEuMS0uNC0xLjYgMC0uNC40LS40IDEuMSAwIDEuNkw1IDExLjJjLjQuNCAxLjEuNCAxLjYgMGw4LjktOC45Yy40LS40LjQtMS4xIDAtMS42LS41LS40LTEuMi0uNC0xLjYuMXoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=)
|
||||
no-repeat 50% 50%;
|
||||
background-size: 12px;
|
||||
background-size: 0.75rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
@ -75,14 +75,14 @@
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 1.1875rem;
|
||||
left: 0.6875rem;
|
||||
top: 0.1875rem;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border: 2px solid var(--color-borders-input);
|
||||
border: 0.125rem solid var(--color-borders-input);
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--color-background);
|
||||
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 {
|
||||
&::before {
|
||||
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 { 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 { IRadioOption } from './CheckboxGroup';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Button from './Button';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
import './Checkbox.scss';
|
||||
@ -18,6 +26,7 @@ type OwnProps = {
|
||||
name?: string;
|
||||
value?: string;
|
||||
label: TeactNode;
|
||||
labelText?: TeactNode;
|
||||
subLabel?: string;
|
||||
checked?: boolean;
|
||||
rightIcon?: IconName;
|
||||
@ -25,12 +34,18 @@ type OwnProps = {
|
||||
tabIndex?: number;
|
||||
round?: boolean;
|
||||
blocking?: boolean;
|
||||
permissionGroup?: boolean;
|
||||
isLoading?: boolean;
|
||||
withCheckedCallback?: boolean;
|
||||
className?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>, nestedOptionList?: IRadioOption) => void;
|
||||
onCheck?: (isChecked: boolean) => void;
|
||||
onClickLabel?: (e: React.MouseEvent, value?: string) => void;
|
||||
nestedCheckbox?: boolean;
|
||||
nestedCheckboxCount?: number | undefined;
|
||||
nestedOptionList?: IRadioOption;
|
||||
leftElement?: TeactNode;
|
||||
values?: string[];
|
||||
};
|
||||
|
||||
const Checkbox: FC<OwnProps> = ({
|
||||
@ -38,37 +53,48 @@ const Checkbox: FC<OwnProps> = ({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
labelText,
|
||||
subLabel,
|
||||
checked,
|
||||
tabIndex,
|
||||
disabled,
|
||||
round,
|
||||
blocking,
|
||||
permissionGroup,
|
||||
isLoading,
|
||||
className,
|
||||
rightIcon,
|
||||
onChange,
|
||||
onCheck,
|
||||
onClickLabel,
|
||||
nestedCheckbox,
|
||||
nestedCheckboxCount,
|
||||
nestedOptionList,
|
||||
leftElement,
|
||||
values = [],
|
||||
}) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-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) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(event);
|
||||
onChange(event, nestedOptionList);
|
||||
}
|
||||
|
||||
if (onCheck) {
|
||||
onCheck(event.currentTarget.checked);
|
||||
}
|
||||
}, [disabled, onChange, onCheck]);
|
||||
});
|
||||
|
||||
const toggleNested = useLastCallback(() => {
|
||||
setShowNested(!showNested);
|
||||
});
|
||||
|
||||
function handleClick(event: React.MouseEvent) {
|
||||
if (event.target !== labelRef.current) {
|
||||
@ -86,37 +112,70 @@ const Checkbox: FC<OwnProps> = ({
|
||||
round && 'round',
|
||||
isLoading && 'loading',
|
||||
blocking && 'blocking',
|
||||
nestedCheckbox && 'nested',
|
||||
permissionGroup && 'permission-group',
|
||||
Boolean(leftElement) && 'avatar',
|
||||
className,
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<label
|
||||
className={labelClassName}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onClick={onClickLabel ? handleClick : undefined}
|
||||
ref={labelRef}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
tabIndex={tabIndex}
|
||||
onChange={handleChange}
|
||||
onClick={onClickLabel ? handleInputClick : undefined}
|
||||
/>
|
||||
<div className="Checkbox-main">
|
||||
<span className="label" dir="auto">
|
||||
{typeof label === 'string' ? renderText(label) : label}
|
||||
{rightIcon && <i className={`icon icon-${rightIcon} right-icon`} />}
|
||||
</span>
|
||||
{subLabel && <span className="subLabel" dir="auto">{renderText(subLabel)}</span>}
|
||||
</div>
|
||||
{isLoading && <Spinner />}
|
||||
</label>
|
||||
<>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<label
|
||||
className={labelClassName}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onClick={onClickLabel ? handleClick : undefined}
|
||||
ref={labelRef}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
tabIndex={tabIndex}
|
||||
onChange={handleChange}
|
||||
onClick={onClickLabel ? handleInputClick : undefined}
|
||||
/>
|
||||
<div className={buildClassName('Checkbox-main', Boolean(leftElement) && 'Nested-avatar-list')}>
|
||||
<span className="label" dir="auto">
|
||||
{leftElement}
|
||||
{typeof label === 'string' ? renderText(label) : label}
|
||||
{labelText && <span className="ml-1">{renderText(labelText)}</span>}
|
||||
{rightIcon && <i className={`icon icon-${rightIcon} right-icon`} />}
|
||||
</span>
|
||||
{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 { 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';
|
||||
|
||||
@ -9,6 +13,8 @@ export type IRadioOption = {
|
||||
subLabel?: string;
|
||||
disabled?: boolean;
|
||||
value: string;
|
||||
nestedOptions?: IRadioOption[];
|
||||
user?: ApiUser;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
@ -17,6 +23,7 @@ type OwnProps = {
|
||||
selected?: string[];
|
||||
disabled?: boolean;
|
||||
round?: boolean;
|
||||
nestedCheckbox?: boolean;
|
||||
loadingOptions?: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
};
|
||||
@ -27,38 +34,71 @@ const CheckboxGroup: FC<OwnProps> = ({
|
||||
selected = [],
|
||||
disabled,
|
||||
round,
|
||||
nestedCheckbox,
|
||||
loadingOptions,
|
||||
onChange,
|
||||
}) => {
|
||||
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;
|
||||
let newValues: string[];
|
||||
|
||||
if (checked) {
|
||||
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 {
|
||||
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);
|
||||
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 (
|
||||
<div id={id} className="radio-group">
|
||||
{options.map((option) => (
|
||||
<Checkbox
|
||||
label={option.label}
|
||||
subLabel={option.subLabel}
|
||||
value={option.value}
|
||||
checked={selected.indexOf(option.value) !== -1}
|
||||
disabled={option.disabled || disabled}
|
||||
round={round}
|
||||
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
))}
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<Checkbox
|
||||
label={option.label}
|
||||
subLabel={option.subLabel}
|
||||
value={option.value}
|
||||
checked={selected.indexOf(option.value) !== -1}
|
||||
disabled={option.disabled || disabled}
|
||||
round={round}
|
||||
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
||||
onChange={handleChange}
|
||||
nestedCheckbox={nestedCheckbox}
|
||||
nestedCheckboxCount={getCheckedNestedCount(option.nestedOptions ?? [])}
|
||||
nestedOptionList={option}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -958,3 +958,29 @@ function copyTextForMessages(global: GlobalState, chatId: string, messageIds: nu
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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>(
|
||||
global: T, message: ApiMessage,
|
||||
) {
|
||||
|
||||
@ -100,6 +100,7 @@ import type {
|
||||
EmojiKeywords,
|
||||
FocusDirection,
|
||||
GlobalSearchContent,
|
||||
IAlbum,
|
||||
IAnchorPosition,
|
||||
InlineBotSettings,
|
||||
ISettings,
|
||||
@ -691,6 +692,13 @@ export type TabState = {
|
||||
prepaidGiveaway?: ApiPrepaidGiveaway;
|
||||
};
|
||||
|
||||
deleteMessageModal?: {
|
||||
message?: ApiMessage;
|
||||
isSchedule?: boolean;
|
||||
album?: IAlbum;
|
||||
onConfirm?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
giftingModal?: {
|
||||
isOpen?: boolean;
|
||||
};
|
||||
@ -3125,6 +3133,14 @@ export interface ActionPayloads {
|
||||
openPremiumGiftingModal: WithTabId | undefined;
|
||||
closePremiumGiftingModal: WithTabId | undefined;
|
||||
|
||||
openDeleteMessageModal: ({
|
||||
message?: ApiMessage;
|
||||
isSchedule?: boolean;
|
||||
album?: IAlbum;
|
||||
onConfirm?: NoneToVoidFunction;
|
||||
} & WithTabId);
|
||||
closeDeleteMessageModal: WithTabId | undefined;
|
||||
|
||||
transcribeAudio: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user