diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 1e8f177de..2cd1cb1c4 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -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 = ({ 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 = ({ setHtml, editingMessage, resetComposer, - openDeleteModal, chatId, threadId, messageListType, @@ -1405,7 +1402,7 @@ const Composer: FC = ({ }, [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 = ({ } }); - 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 = ({ onClear={closePollModal} onSend={handlePollSend} /> - {renderedEditedMessage && ( - - )} ; + chatBot?: boolean; + isSchedule?: boolean; + message?: ApiMessage; + album?: IAlbum; + onConfirm?: NoneToVoidFunction; + isOwn?: boolean; + canBanUsers?: boolean; }; const DeleteMessageModal: FC = ({ isOpen, + chat, + isChannel, + isGroup, + isSuperGroup, + sender, + currentUserId, + messageIdList, isSchedule, message, album, @@ -52,89 +89,392 @@ const DeleteMessageModal: FC = ({ 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(undefined); + const [chosenBanOption, setChosenBanOptions] = useState(undefined); + const [chosenSpanOption, setChosenSpanOptions] = useState(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 ( +
+ {(shouldShowAdditionalOptions && !canDeleteForAll && !isSchedule) && ( + + )} +

{lang('DeleteSingleMessagesTitle')} +

+
+ ); + } + + function renderAdditionalActionOptions() { + return ( +
+ + + {!isSenderOwner && canBanUsers && ( + + )} +
+ ); + } + + function renderPartiallyRestrictedUser() { + return ( +
+

+ {lang('UserRestrictionsCanDoUsers', 1)} +

+ +
+ ); + } return ( -

{lang('AreYouSureDeleteSingleMessage')}

- {willDeleteForCurrentUserOnly && ( -

{lang('lng_delete_for_me_chat_hint', 1, 'i')}

- )} - {willDeleteForAll && ( -

{lang('lng_delete_for_everyone_hint', 1, 'i')}

- )} -
- {canDeleteForAll && ( - +
+ {renderHeader()} + {(shouldShowAdditionalOptions && !canDeleteForAll && !isSchedule && (isChannel || isGroup || isSuperGroup)) && ( + <> +

{lang('DeleteAdditionalActions')}

+ {renderAdditionalActionOptions()} + {renderPartiallyRestrictedUser()} + { + chosenBanOption && canBanUsers && chosenBanOption?.length ? ( + + {lang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')} + + + ) : setIsAdditionalOptionsVisible(false) + } + )} - - + {(chatBot || !shouldShowAdditionalOptions) && ( + <> +

{lang('AreYouSureDeleteSingleMessage')}

+ {willDeleteForCurrentUserOnly && ( +

{lang('lng_delete_for_me_chat_hint', 1, 'i')}

+ )} + {willDeleteForAll && ( +

{lang('lng_delete_for_everyone_hint', 1, 'i')}

+ )} + + )} +
+ {canDeleteForAll && ( + + )} + + +
); }; export default memo(withGlobal( - (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)); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index fc9ee73ad..e730d4e58 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -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 = ({ requestedDraft, isPremiumModalOpen, isGiveawayModalOpen, + isDeleteMessageModalOpen, isPremiumGiftingModalOpen, isPaymentModalOpen, isReceiptModalOpen, @@ -577,6 +580,7 @@ const Main: FC = ({ +
); }; @@ -610,6 +614,7 @@ export default memo(withGlobal( ratingPhoneCall, premiumModal, giveawayModal, + deleteMessageModal, giftingModal, isMasterTab, payment, @@ -665,6 +670,7 @@ export default memo(withGlobal( isCurrentUserPremium: selectIsCurrentUserPremium(global), isPremiumModalOpen: premiumModal?.isOpen, isGiveawayModalOpen: giveawayModal?.isOpen, + isDeleteMessageModalOpen: Boolean(deleteMessageModal), isPremiumGiftingModalOpen: giftingModal?.isOpen, limitReached: limitReachedModal?.limit, isPaymentModalOpen: payment.isPaymentModalOpen, diff --git a/src/components/main/PermissionCheckboxList.tsx b/src/components/main/PermissionCheckboxList.tsx new file mode 100644 index 000000000..0cceb452d --- /dev/null +++ b/src/components/main/PermissionCheckboxList.tsx @@ -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) => void; + permissions: ApiChatBannedRights; + isMediaDropdownOpen: boolean; + setIsMediaDropdownOpen: (open: boolean) => void; + className?: string; + shiftedClassName?: string; + dropdownClassName?: string; + withCheckbox?: boolean; + permissionGroup?: boolean; + getControlIsDisabled?: (key: Exclude) => boolean | undefined; +}; + +type StateProps = { + chat?: ApiChat; + hasLinkedChat?: boolean; +}; + +const permissionKeyList: (keyof ApiChatBannedRights)[] = [ + 'sendPhotos', 'sendVideos', 'sendStickers', + 'sendAudios', 'sendDocs', 'sendVoices', 'sendRoundvideos', 'embedLinks', 'sendPolls', +]; + +const PermissionCheckboxList: FC = ({ + 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 ( + <> +
+ +
+
+ +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+ {isForum && ( +
+ +
+ )} +
+ + ); +}; + +export default memo(withGlobal( + (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)); diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index f917e7d13..9d2d38024 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -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 = ({ updateProfilePhoto, updateChatPhoto, openMediaViewer, + openDeleteMessageModal, } = getActions(); const isMessage = item?.type === 'message'; @@ -163,31 +163,16 @@ const MediaViewerActions: FC = ({ ); }, []); - function renderDeleteModals() { - if (item?.type === 'message') { - return ( - - ); - } - if (item?.type === 'avatar') { - return ( - - ); - } - - return undefined; + function renderDeleteModal() { + return (item?.type === 'avatar') ? ( + + ) : undefined; } function renderDownloadButton() { @@ -223,6 +208,17 @@ const MediaViewerActions: FC = ({ ); } + 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 = ({ if (canDelete) { menuItems.push({ icon: 'delete', - onClick: openDeleteModal, + onClick: openDeleteModalHandler, children: lang('Delete'), destructive: true, }); @@ -300,7 +296,7 @@ const MediaViewerActions: FC = ({ ))} {isDownloading && } - {canDelete && renderDeleteModals()} + {canDelete && renderDeleteModal()} ); } @@ -365,7 +361,7 @@ const MediaViewerActions: FC = ({ size="smaller" color="translucent-white" ariaLabel={lang('Delete')} - onClick={openDeleteModal} + onClick={openDeleteModalHandler} > @@ -379,7 +375,7 @@ const MediaViewerActions: FC = ({ > - {canDelete && renderDeleteModals()} + {canDelete && renderDeleteModal()} ); }; diff --git a/src/components/middle/DeleteSelectedMessageModal.module.scss b/src/components/middle/DeleteSelectedMessageModal.module.scss new file mode 100644 index 000000000..9bb3406f7 --- /dev/null +++ b/src/components/middle/DeleteSelectedMessageModal.module.scss @@ -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; +} diff --git a/src/components/middle/DeleteSelectedMessageModal.tsx b/src/components/middle/DeleteSelectedMessageModal.tsx index 55140a124..1732af817 100644 --- a/src/components/middle/DeleteSelectedMessageModal.tsx +++ b/src/components/middle/DeleteSelectedMessageModal.tsx @@ -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; + canBanUsers?: boolean; }; const DeleteSelectedMessageModal: FC = ({ + 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(undefined); + const [chosenBanOption, setChosenBanOptions] = useState(undefined); + const [chosenSpanOption, setChosenSpanOptions] = useState(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: , + })); + }); + + 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 = ({ 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 `` 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 ( +
+ {(showAdditionalOptions && !canDeleteForAll && !isSchedule) && ( + + )} +

{oldLang('Chat.DeleteMessagesConfirmation', selectedMessageIds?.length)} +

+
+ ); + } + + function renderAdditionalActionOptions() { + return ( +
+ = 2} + /> + = 2} + /> + {!isSenderOwner && canBanUsers && ( + = 2} + /> + )} +
+ ); + } + + function renderPartiallyRestrictedUser() { + return ( +
+

+ {oldLang('UserRestrictionsCanDoUsers', userList.length)} +

+ +
+ ); + } if (!selectedMessageIds) { return undefined; @@ -87,27 +388,50 @@ const DeleteSelectedMessageModal: FC = ({ onClose={onClose} onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf} className="delete" - title={lang('Conversation.DeleteManyMessages')} > -

{lang('AreYouSureDeleteFewMessages')}

- {willDeleteForCurrentUserOnly && ( -

This will delete them just for you, not for other participants in the chat.

- )} - {willDeleteForAll && ( -

This will delete them for everyone in this chat.

- )} -
- {canDeleteForAll && ( - +
+ {renderHeader()} + {!showAdditionalOptions &&

{lang('AreYouSureDeleteFewMessages')}

} + {(showAdditionalOptions && !canDeleteForAll && !isSchedule && (isChannel || isGroup || isSuperGroup)) && ( + <> +

{oldLang('DeleteAdditionalActions')}

+ {renderAdditionalActionOptions()} + {renderPartiallyRestrictedUser()} + { + chosenBanOption && canBanUsers && chosenBanOption?.length ? ( + + {oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')} + + + ) : setIsAdditionalOptionsVisible(false) + } + )} - - + {willDeleteForCurrentUserOnly && lang('DeleteForMeDescription')} + {(willDeleteForAll && !showAdditionalOptions) && lang('DeleteForEveryoneDescription')} +
+ {canDeleteForAll && ( + + )} + + +
); @@ -118,19 +442,34 @@ export default memo(withGlobal( 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)); diff --git a/src/components/middle/composer/hooks/useEditing.ts b/src/components/middle/composer/hooks/useEditing.ts index 27559ae2c..967666879 100644 --- a/src/components/middle/composer/hooks/useEditing.ts +++ b/src/components/middle/composer/hooks/useEditing.ts @@ -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; } diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index c0cf9a9f5..9aebe6ded 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -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 = ({ 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 = ({ 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 = ({ onShowOriginal={handleShowOriginal} onSelectLanguage={handleSelectLanguage} /> - { + setPermissions(defaultPermissions || {}); + setHavePermissionChanged(false); + }, [defaultPermissions]); + return { permissions, isLoading, havePermissionChanged, handlePermissionChange, setIsLoading, + resetPermissions, }; } diff --git a/src/components/right/management/ManageGroupPermissions.tsx b/src/components/right/management/ManageGroupPermissions.tsx index 89728f9ec..956420550 100644 --- a/src/components/right/management/ManageGroupPermissions.tsx +++ b/src/components/right/management/ManageGroupPermissions.tsx @@ -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 = ({ 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 = ({ }, [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 = ({

{lang('ChannelPermissionsHeader')}

- -
- -
-
- -
-
-
-
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
-
- -
-
- -
-
- -
-
- -
- {isForum && ( -
- -
+ + shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')} + />
( (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, }; diff --git a/src/components/right/management/ManageGroupUserPermissions.tsx b/src/components/right/management/ManageGroupUserPermissions.tsx index a36a9272b..23d99660a 100644 --- a/src/components/right/management/ManageGroupUserPermissions.tsx +++ b/src/components/right/management/ManageGroupUserPermissions.tsx @@ -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 = ({ 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 = ({ }, [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 = ({ -

{lang('UserRestrictionsCanDo')}

- -
- -
- -
- -
- -
-
-
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
-
- -
- -
- -
-
- -
-
- -
- {isForum && ( -
- -
+

{oldLang('UserRestrictionsCanDo')}

+ + dropdownClassName="DropdownListTrap" + shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')} + getControlIsDisabled={getControlIsDisabled} + />
{!isFormFullyDisabled && ( @@ -334,7 +165,7 @@ const ManageGroupUserPermissions: FC = ({ )} > - {lang('UserRestrictionsBlock')} + {oldLang('UserRestrictionsBlock')}
)} @@ -343,7 +174,7 @@ const ManageGroupUserPermissions: FC = ({ {isLoading ? ( @@ -356,7 +187,7 @@ const ManageGroupUserPermissions: FC = ({ ) => void; + onChange?: (e: ChangeEvent, 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 = ({ @@ -38,37 +53,48 @@ const Checkbox: FC = ({ 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(null); + const [showNested, setShowNested] = useState(false); - const handleChange = useCallback((event: ChangeEvent) => { + const handleChange = useLastCallback((event: ChangeEvent) => { 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 = ({ 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 - + <> + {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} + + {nestedCheckbox && ( +
+ {nestedOptionList?.nestedOptions?.map((nestedOption) => ( + + ))} +
+ )} + ); }; diff --git a/src/components/ui/CheckboxGroup.tsx b/src/components/ui/CheckboxGroup.tsx index 89064c40c..82f9ea13a 100644 --- a/src/components/ui/CheckboxGroup.tsx +++ b/src/components/ui/CheckboxGroup.tsx @@ -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 = ({ selected = [], disabled, round, + nestedCheckbox, loadingOptions, onChange, }) => { const [values, setValues] = useState(selected || []); - const handleChange = useCallback((event: ChangeEvent) => { + const handleChange = useLastCallback((event: ChangeEvent, 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 (
- {options.map((option) => ( - - ))} + {options.map((option) => { + return ( + + ); + })}
); }; diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index bd354c46a..eeb37e4ac 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -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); +}); diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 3f628ac5b..79be10460 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -423,6 +423,42 @@ export function selectSender(global: T, message: ApiMessa return selectPeer(global, senderId); } +export function selectSendersFromSelectedMessages( + global: T, + chat: ApiChat | undefined, + ...[tabId = getCurrentTabId()]: TabArgs +) { + 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( + 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( global: T, message: ApiMessage, ) { diff --git a/src/global/types.ts b/src/global/types.ts index 9875b49b9..cb072d161 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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;