From 609c0cb68eba9edcaf0f74d8fd7233890959bc12 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 30 Sep 2021 14:03:00 +0300 Subject: [PATCH] Add chat to folder from context menu (#1477) --- src/bundles/extra.ts | 1 + .../helpers/renderActionMessageText.tsx | 8 +- src/components/left/ChatFolderModal.async.tsx | 15 +++ src/components/left/ChatFolderModal.tsx | 110 ++++++++++++++++++ src/components/left/main/Chat.tsx | 17 +++ .../left/search/LeftSearchResultChat.tsx | 8 ++ src/components/ui/Checkbox.tsx | 5 +- src/components/ui/CheckboxGroup.tsx | 2 +- src/global/types.ts | 2 +- src/hooks/useChatContextActions.ts | 15 ++- src/modules/actions/api/chats.ts | 31 +++++ src/modules/helpers/chats.ts | 8 -- 12 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 src/components/left/ChatFolderModal.async.tsx create mode 100644 src/components/left/ChatFolderModal.tsx diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 9f85bb9a..5178070f 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -19,6 +19,7 @@ export { default as NewChat } from '../components/left/newChat/NewChat'; export { default as NewChatStep1 } from '../components/left/newChat/NewChatStep1'; export { default as NewChatStep2 } from '../components/left/newChat/NewChatStep2'; export { default as ArchivedChats } from '../components/left/ArchivedChats'; +export { default as ChatFolderModal } from '../components/left/ChatFolderModal'; export { default as ContextMenuContainer } from '../components/middle/message/ContextMenuContainer'; export { default as StickerSetModal } from '../components/common/StickerSetModal'; diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index 00f1a20b..07c35c8f 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -7,7 +7,7 @@ import { getMessageContent, getMessageSummaryText, getUserFullName, - isChat, + isChatPrivate, } from '../../../modules/helpers'; import trimText from '../../../util/trimText'; import { formatCurrency } from '../../../util/formatCurrency'; @@ -167,9 +167,9 @@ function renderMessageContent(lang: LangFn, message: ApiMessage, options: Action } function renderOriginContent(lang: LangFn, origin: ApiUser | ApiChat, asPlain?: boolean) { - return isChat(origin) - ? renderChatContent(lang, origin, asPlain) - : renderUserContent(origin, asPlain); + return isChatPrivate(origin.id) + ? renderUserContent(origin as ApiUser, asPlain) + : renderChatContent(lang, origin as ApiChat, asPlain); } function renderUserContent(sender: ApiUser, asPlain?: boolean): string | TextPart | undefined { diff --git a/src/components/left/ChatFolderModal.async.tsx b/src/components/left/ChatFolderModal.async.tsx new file mode 100644 index 00000000..15d10171 --- /dev/null +++ b/src/components/left/ChatFolderModal.async.tsx @@ -0,0 +1,15 @@ +import React, { FC, memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; +import { OwnProps } from './ChatFolderModal'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const ChatFolderModalAsync: FC = (props) => { + const { isOpen } = props; + const ChatFolderModal = useModuleLoader(Bundles.Extra, 'ChatFolderModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ChatFolderModal ? : undefined; +}; + +export default memo(ChatFolderModalAsync); diff --git a/src/components/left/ChatFolderModal.tsx b/src/components/left/ChatFolderModal.tsx new file mode 100644 index 00000000..d0564802 --- /dev/null +++ b/src/components/left/ChatFolderModal.tsx @@ -0,0 +1,110 @@ +import React, { + FC, useCallback, memo, useMemo, useState, +} from '../../lib/teact/teact'; +import { withGlobal } from '../../lib/teact/teactn'; + +import { GlobalActions } from '../../global/types'; +import { ApiChatFolder } from '../../api/types'; + +import { pick } from '../../util/iteratees'; +import useLang from '../../hooks/useLang'; + +import Modal from '../ui/Modal'; +import Button from '../ui/Button'; +import CheckboxGroup from '../ui/CheckboxGroup'; + +export type OwnProps = { + isOpen: boolean; + chatId: number; + onClose: () => void; + onCloseAnimationEnd?: () => void; +}; + +type StateProps = { + foldersById?: Record; + folderOrderedIds?: number[]; +}; + +type DispatchProps = Pick; + +const ChatFolderModal: FC = ({ + isOpen, + chatId, + foldersById, + folderOrderedIds, + onClose, + onCloseAnimationEnd, + editChatFolders, +}) => { + const lang = useLang(); + + const initialSelectedFolderIds = useMemo(() => { + if (!foldersById) { + return []; + } + + return Object.keys(foldersById).reduce((result, folderId) => { + const { includedChatIds, pinnedChatIds } = foldersById[Number(folderId)]; + if (includedChatIds.includes(chatId) || pinnedChatIds?.includes(chatId)) { + result.push(folderId); + } + + return result; + }, [] as string[]); + }, [chatId, foldersById]); + + const [selectedFolderIds, setSelectedFolderIds] = useState(initialSelectedFolderIds); + + const folders = useMemo(() => { + return folderOrderedIds?.map((folderId) => ({ + label: foldersById ? foldersById[folderId].title : '', + value: String(folderId), + })) || []; + }, [folderOrderedIds, foldersById]); + + const handleSubmit = useCallback(() => { + const idsToRemove = initialSelectedFolderIds.filter((id) => !selectedFolderIds.includes(id)).map(Number); + const idsToAdd = selectedFolderIds.filter((id) => !initialSelectedFolderIds.includes(id)).map(Number); + + editChatFolders({ chatId, idsToRemove, idsToAdd }); + onClose(); + }, [chatId, editChatFolders, initialSelectedFolderIds, onClose, selectedFolderIds]); + + if (!foldersById || !folderOrderedIds) { + return undefined; + } + + return ( + + + + + + ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { byId: foldersById, orderedIds: folderOrderedIds } = global.chatFolders; + + return { + foldersById, + folderOrderedIds, + }; + }, + (setGlobal, actions): DispatchProps => pick(actions, ['editChatFolders']), +)(ChatFolderModal)); diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 7d5fb09d..51348f0e 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -50,6 +50,7 @@ import LastMessageMeta from '../../common/LastMessageMeta'; import DeleteChatModal from '../../common/DeleteChatModal'; import ListItem from '../../ui/ListItem'; import Badge from './Badge'; +import ChatFolderModal from '../ChatFolderModal.async'; import './Chat.scss'; @@ -111,7 +112,9 @@ const Chat: FC = ({ const ref = useRef(null); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); + const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag(); const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag(); + const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag(); const { lastMessage, typingStatus } = chat || {}; const isAction = lastMessage && isActionMessage(lastMessage); @@ -185,10 +188,16 @@ const Chat: FC = ({ openDeleteModal(); } + function handleChatFolderChange() { + markRenderChatFolderModal(); + openChatFolderModal(); + } + const contextActions = useChatContextActions({ chat, privateChatUser, handleDelete, + handleChatFolderChange, folderId, isPinned, isMuted, @@ -299,6 +308,14 @@ const Chat: FC = ({ chat={chat} /> )} + {shouldRenderChatFolderModal && ( + + )} ); }; diff --git a/src/components/left/search/LeftSearchResultChat.tsx b/src/components/left/search/LeftSearchResultChat.tsx index e5b28baf..27d353bc 100644 --- a/src/components/left/search/LeftSearchResultChat.tsx +++ b/src/components/left/search/LeftSearchResultChat.tsx @@ -17,6 +17,7 @@ import PrivateChatInfo from '../../common/PrivateChatInfo'; import GroupChatInfo from '../../common/GroupChatInfo'; import DeleteChatModal from '../../common/DeleteChatModal'; import ListItem from '../../ui/ListItem'; +import ChatFolderModal from '../ChatFolderModal.async'; type OwnProps = { chatId: number; @@ -41,6 +42,7 @@ const LeftSearchResultChat: FC = ({ onClick, }) => { const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); + const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag(); const contextActions = useChatContextActions({ chat, @@ -48,6 +50,7 @@ const LeftSearchResultChat: FC = ({ isPinned, isMuted, handleDelete: openDeleteModal, + handleChatFolderChange: openChatFolderModal, }, true); const handleClick = () => { @@ -77,6 +80,11 @@ const LeftSearchResultChat: FC = ({ onClose={closeDeleteModal} chat={chat} /> + ); }; diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx index 2e9d12af..fe7dd9ad 100644 --- a/src/components/ui/Checkbox.tsx +++ b/src/components/ui/Checkbox.tsx @@ -3,6 +3,7 @@ import React, { FC, memo, useCallback } from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; import useLang from '../../hooks/useLang'; +import renderText from '../common/helpers/renderText'; import Spinner from './Spinner'; @@ -69,8 +70,8 @@ const Checkbox: FC = ({ onChange={handleChange} />
- {label} - {subLabel && {subLabel}} + {renderText(label)} + {subLabel && {renderText(subLabel)}}
{isLoading && } diff --git a/src/components/ui/CheckboxGroup.tsx b/src/components/ui/CheckboxGroup.tsx index f571c48a..71a7762e 100644 --- a/src/components/ui/CheckboxGroup.tsx +++ b/src/components/ui/CheckboxGroup.tsx @@ -30,7 +30,7 @@ const CheckboxGroup: FC = ({ loadingOptions, onChange, }) => { - const [values, setValues] = useState([]); + const [values, setValues] = useState(selected || []); const handleChange = useCallback((event: ChangeEvent) => { const { value, checked } = event.currentTarget; diff --git a/src/global/types.ts b/src/global/types.ts index 947de12f..55a0c42d 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -460,7 +460,7 @@ export type ActionTypes = ( 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' | 'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' | - 'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | + 'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | // messages 'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' | 'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' | diff --git a/src/hooks/useChatContextActions.ts b/src/hooks/useChatContextActions.ts index b8d64a2c..78461d45 100644 --- a/src/hooks/useChatContextActions.ts +++ b/src/hooks/useChatContextActions.ts @@ -12,6 +12,7 @@ export default ({ chat, privateChatUser, handleDelete, + handleChatFolderChange, folderId, isPinned, isMuted, @@ -19,6 +20,7 @@ export default ({ chat: ApiChat | undefined; privateChatUser: ApiUser | undefined; handleDelete: () => void; + handleChatFolderChange: () => void; folderId?: number; isPinned?: boolean; isMuted?: boolean; @@ -37,6 +39,12 @@ export default ({ toggleChatUnread, } = getDispatch(); + const actionAddToFolder = { + title: lang('ChatList.Filter.AddToFolder'), + icon: 'folder', + handler: handleChatFolderChange, + }; + const actionPin = isPinned ? { title: lang('UnpinFromTop'), @@ -46,7 +54,7 @@ export default ({ : { title: lang('PinToTop'), icon: 'pin', handler: () => toggleChatPinned({ id: chat.id, folderId }) }; if (isInSearch) { - return [actionPin]; + return [actionPin, actionAddToFolder]; } const actionUnreadMark = chat.unreadCount || chat.hasUnreadMark @@ -81,6 +89,7 @@ export default ({ }; return [ + actionAddToFolder, actionUnreadMark, actionPin, ...(!privateChatUser?.isSelf ? [ @@ -89,5 +98,7 @@ export default ({ ] : []), actionDelete, ]; - }, [chat, isPinned, lang, isInSearch, isMuted, handleDelete, privateChatUser?.isSelf, folderId]); + }, [ + chat, isPinned, lang, isInSearch, isMuted, handleDelete, handleChatFolderChange, privateChatUser?.isSelf, folderId, + ]); }; diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 56c93272..56399a0f 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -435,6 +435,37 @@ addReducer('loadRecommendedChatFolders', () => { void loadRecommendedChatFolders(); }); +addReducer('editChatFolders', (global, actions, payload) => { + const { chatId, idsToRemove, idsToAdd } = payload!; + + (idsToRemove as number[]).forEach(async (id) => { + const folder = selectChatFolder(global, id); + if (folder) { + await callApi('editChatFolder', { + id, + folderUpdate: { + ...folder, + pinnedChatIds: folder.pinnedChatIds?.filter((pinnedId) => pinnedId !== chatId), + includedChatIds: folder.includedChatIds.filter((includedId) => includedId !== chatId), + }, + }); + } + }); + + (idsToAdd as number[]).forEach(async (id) => { + const folder = selectChatFolder(global, id); + if (folder) { + await callApi('editChatFolder', { + id, + folderUpdate: { + ...folder, + includedChatIds: folder.includedChatIds.concat(chatId), + }, + }); + } + }); +}); + addReducer('editChatFolder', (global, actions, payload) => { const { id, folderUpdate } = payload!; const folder = selectChatFolder(global, id); diff --git a/src/modules/helpers/chats.ts b/src/modules/helpers/chats.ts index 543f28f9..997b0839 100644 --- a/src/modules/helpers/chats.ts +++ b/src/modules/helpers/chats.ts @@ -498,14 +498,6 @@ function getFolderChatsCount( return pinnedChats.length + otherChats.length; } -export function isChat(chatOrUser?: ApiUser | ApiChat): chatOrUser is ApiChat { - if (!chatOrUser) { - return false; - } - - return chatOrUser.id < 0; -} - export function getMessageSenderName(lang: LangFn, chatId: number, sender?: ApiUser) { if (!sender || isChatPrivate(chatId)) { return undefined;