Add chat to folder from context menu (#1477)

This commit is contained in:
Alexander Zinchuk 2021-09-30 14:03:00 +03:00
parent 54d6b581da
commit 609c0cb68e
12 changed files with 204 additions and 18 deletions

View File

@ -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';

View File

@ -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 {

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const ChatFolderModal = useModuleLoader(Bundles.Extra, 'ChatFolderModal', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return ChatFolderModal ? <ChatFolderModal {...props} /> : undefined;
};
export default memo(ChatFolderModalAsync);

View File

@ -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<number, ApiChatFolder>;
folderOrderedIds?: number[];
};
type DispatchProps = Pick<GlobalActions, 'editChatFolders'>;
const ChatFolderModal: FC<OwnProps & StateProps & DispatchProps> = ({
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<string[]>(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 (
<Modal
isOpen={isOpen}
onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd}
onEnter={handleSubmit}
className="delete"
title={lang('FilterAddTo')}
>
<CheckboxGroup
options={folders}
selected={selectedFolderIds}
onChange={setSelectedFolderIds}
round
/>
<Button color="primary" className="confirm-dialog-button" isText onClick={handleSubmit}>
{lang('FilterAddTo')}
</Button>
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { byId: foldersById, orderedIds: folderOrderedIds } = global.chatFolders;
return {
foldersById,
folderOrderedIds,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['editChatFolders']),
)(ChatFolderModal));

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
const ref = useRef<HTMLDivElement>(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<OwnProps & StateProps & DispatchProps> = ({
openDeleteModal();
}
function handleChatFolderChange() {
markRenderChatFolderModal();
openChatFolderModal();
}
const contextActions = useChatContextActions({
chat,
privateChatUser,
handleDelete,
handleChatFolderChange,
folderId,
isPinned,
isMuted,
@ -299,6 +308,14 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
chat={chat}
/>
)}
{shouldRenderChatFolderModal && (
<ChatFolderModal
isOpen={isChatFolderModalOpen}
onClose={closeChatFolderModal}
onCloseAnimationEnd={unmarkRenderChatFolderModal}
chatId={chatId}
/>
)}
</ListItem>
);
};

View File

@ -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<OwnProps & StateProps> = ({
onClick,
}) => {
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag();
const contextActions = useChatContextActions({
chat,
@ -48,6 +50,7 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
isPinned,
isMuted,
handleDelete: openDeleteModal,
handleChatFolderChange: openChatFolderModal,
}, true);
const handleClick = () => {
@ -77,6 +80,11 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
onClose={closeDeleteModal}
chat={chat}
/>
<ChatFolderModal
isOpen={isChatFolderModalOpen}
onClose={closeChatFolderModal}
chatId={chatId}
/>
</ListItem>
);
};

View File

@ -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<OwnProps> = ({
onChange={handleChange}
/>
<div className="Checkbox-main">
<span className="label" dir="auto">{label}</span>
{subLabel && <span className="subLabel" dir="auto">{subLabel}</span>}
<span className="label" dir="auto">{renderText(label)}</span>
{subLabel && <span className="subLabel" dir="auto">{renderText(subLabel)}</span>}
</div>
{isLoading && <Spinner />}
</label>

View File

@ -30,7 +30,7 @@ const CheckboxGroup: FC<OwnProps> = ({
loadingOptions,
onChange,
}) => {
const [values, setValues] = useState<string[]>([]);
const [values, setValues] = useState<string[]>(selected || []);
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { value, checked } = event.currentTarget;

View File

@ -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' |

View File

@ -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,
]);
};

View File

@ -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);

View File

@ -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;