diff options
author | romain sanchez <romain.sanchez@proton.ch> | 2022-09-27 13:45:02 +0300 |
---|---|---|
committer | Richard <richard@protonmail.com> | 2022-09-28 10:55:01 +0300 |
commit | bdcfa3691ec9ee39fe5c9e40210e55f749c7fd37 (patch) | |
tree | 3b6d03feab3e5effc7aa3b79df4ace6ec579bf24 | |
parent | a6ef252c3a3b29ee7a5971f0cc626cf410ce5a1c (diff) |
Add back filter button in the message view
MAILWEB-3608
5 files changed, 313 insertions, 24 deletions
diff --git a/applications/mail/src/app/components/dropdown/CustomFilterDropdown.tsx b/applications/mail/src/app/components/dropdown/CustomFilterDropdown.tsx new file mode 100644 index 0000000000..affb9dc70d --- /dev/null +++ b/applications/mail/src/app/components/dropdown/CustomFilterDropdown.tsx @@ -0,0 +1,226 @@ +import { useEffect, useMemo, useState } from 'react'; +import * as React from 'react'; + +import { c } from 'ttag'; + +import { + Checkbox, + FilterConstants, + FilterUtils, + PrimaryButton, + useFilters, + useModalState, + useNotifications, + useUser, +} from '@proton/components'; +import { ConditionComparator, ConditionType, Filter } from '@proton/components/containers/filters/interfaces'; +import FilterModal from '@proton/components/containers/filters/modal/FilterModal'; +import { FILTER_STATUS } from '@proton/shared/lib/constants'; +import { Message } from '@proton/shared/lib/interfaces/mail/Message'; +import { isPaid } from '@proton/shared/lib/user/helpers'; +import identity from '@proton/utils/identity'; + +const { computeTree, newFilter } = FilterUtils; +const { OPERATORS } = FilterConstants; + +type FiltersState = { + [key in ConditionType]: boolean; +}; + +type FilterType = { + label: string; + value: ConditionType; + conditionLabel: string; +}; + +interface Props { + message: Message; + onClose: () => void; + onLock: (lock: boolean) => void; +} + +const CustomFilterDropdown = ({ message, onClose, onLock }: Props) => { + const [containFocus, setContainFocus] = useState(true); + + const [filterModalProps, setFilterModalOpen, renderFilterModal] = useModalState(); + + useEffect(() => onLock(!containFocus), [containFocus]); + + const [filtersState, setFiltersState] = useState<FiltersState>({ + [ConditionType.SELECT]: false, + [ConditionType.SUBJECT]: false, + [ConditionType.SENDER]: false, + [ConditionType.RECIPIENT]: false, + [ConditionType.ATTACHMENTS]: false, + }); + const { createNotification } = useNotifications(); + const [user] = useUser(); + const [filters = []] = useFilters() as [Filter[], boolean, Error]; + + const FILTER_TYPES: FilterType[] = [ + { + value: ConditionType.SUBJECT, + label: c('CustomFilter').t`Subject`, + conditionLabel: c('Filter modal type').t`If the subject`, + }, + { + value: ConditionType.SENDER, + label: c('CustomFilter').t`Sender`, + conditionLabel: c('Filter modal type').t`If the sender`, + }, + { + value: ConditionType.RECIPIENT, + label: c('CustomFilter').t`Recipient`, + conditionLabel: c('Filter modal type').t`If the recipient`, + }, + { + value: ConditionType.ATTACHMENTS, + label: c('CustomFilter').t`Attachment`, + conditionLabel: c('Filter modal type').t`If the attachments`, + }, + ]; + + const toggleFilterType = (filterType: ConditionType) => { + setFiltersState({ + ...filtersState, + [filterType]: !filtersState[filterType], + }); + }; + + const formatConditions = (conditions: ConditionType[]) => { + return conditions.map((condition) => { + const filterType = FILTER_TYPES.find((f) => f.value === condition) as FilterType; + let value; + + switch (condition) { + case ConditionType.SUBJECT: + value = message.Subject; + break; + case ConditionType.SENDER: + value = message.Sender ? message.Sender.Address : ''; + break; + case ConditionType.RECIPIENT: + value = message.ToList && message.ToList.length ? message.ToList[0].Address : ''; + break; + case ConditionType.ATTACHMENTS: + default: + value = ''; + break; + } + + return { + value, + Values: [value], + Type: { + label: filterType?.conditionLabel, + value: filterType?.value, + }, + Comparator: { label: 'contains', value: ConditionComparator.CONTAINS }, + }; + }); + }; + + const filter = useMemo(() => { + const filter = newFilter(); + const conditions = []; + let filterType: ConditionType; + + for (filterType in filtersState) { + if (filtersState[filterType]) { + conditions.push(filterType); + } + } + + filter.Simple = { + Operator: { + label: OPERATORS[0].label, + value: OPERATORS[0].value, + }, + Conditions: formatConditions(conditions), + Actions: { + FileInto: [], + Vacation: '', + Mark: { Read: false, Starred: false }, + }, + }; + + return filter; + }, [filtersState]); + + const handleNext = () => { + if (!isPaid(user) && filters.filter((filter) => filter.Status === FILTER_STATUS.ENABLED).length > 0) { + createNotification({ + text: c('Error').t`Too many active filters. Please upgrade to a paid plan to activate more filters.`, + type: 'error', + }); + onClose(); + return; + } + setContainFocus(false); + setFilterModalOpen(true); + }; + + const buttonDisabled = !Object.values(filtersState).some(identity); + + const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + handleNext(); + }; + + return ( + <> + <form onSubmit={handleSubmit}> + <div className="m1"> + <span className="text-bold" tabIndex={-2}> + {c('CustomFilter').t`Filter on`} + </span> + </div> + <ul className="unstyled mt1 mb1"> + {FILTER_TYPES.map((filterType: FilterType) => ( + <li + key={filterType.value} + className="dropdown-item w100 flex flex-nowrap flex-align-items-center p0-5 pl1 pr1" + > + <Checkbox + className="flex-item-noshrink mr0-5" + id={filterType.value} + checked={filtersState[filterType.value]} + onChange={() => toggleFilterType(filterType.value)} + /> + <label + htmlFor={filterType.value} + title={filterType.label} + className="flex-item-fluid text-ellipsis" + > + {filterType.label} + </label> + </li> + ))} + </ul> + <div className="m1"> + <PrimaryButton + className="w100" + disabled={buttonDisabled} + data-prevent-arrow-navigation + type="submit" + data-testid="filter-dropdown:next-button" + > + {c('CustomFilter').t`Next`} + </PrimaryButton> + </div> + </form> + {renderFilterModal && ( + <FilterModal + filter={{ + ...filter, + Tree: computeTree(filter), + }} + onCloseCustomAction={() => setContainFocus(true)} + {...filterModalProps} + /> + )} + </> + ); +}; + +export default CustomFilterDropdown; diff --git a/applications/mail/src/app/components/message/MessageView.tsx b/applications/mail/src/app/components/message/MessageView.tsx index 56dc13b6d6..f65af31570 100644 --- a/applications/mail/src/app/components/message/MessageView.tsx +++ b/applications/mail/src/app/components/message/MessageView.tsx @@ -278,30 +278,36 @@ const MessageView = ( } }, [hasProcessingErrors]); - const { labelDropdownToggleRef, moveDropdownToggleRef, moveScheduledModal, moveAllModal, moveToSpamModal } = - useMessageHotkeys( - elementRef, - { - labelID, - conversationIndex, - message, - bodyLoaded, - expanded, - messageLoaded, - draft, - conversationMode, - mailSettings, - messageRef: elementRef, - }, - { - hasFocus: !!hasFocus, - setExpanded, - toggleOriginalMessage, - handleLoadRemoteImages, - handleLoadEmbeddedImages, - onBack, - } - ); + const { + labelDropdownToggleRef, + moveDropdownToggleRef, + filterDropdownToggleRef, + moveScheduledModal, + moveAllModal, + moveToSpamModal, + } = useMessageHotkeys( + elementRef, + { + labelID, + conversationIndex, + message, + bodyLoaded, + expanded, + messageLoaded, + draft, + conversationMode, + mailSettings, + messageRef: elementRef, + }, + { + hasFocus: !!hasFocus, + setExpanded, + toggleOriginalMessage, + handleLoadRemoteImages, + handleLoadEmbeddedImages, + onBack, + } + ); function handleFocus(context: 'IFRAME'): () => void; function handleFocus(context: 'BUBBLED_EVENT'): (event: FocusEvent) => void; @@ -366,6 +372,7 @@ const MessageView = ( breakpoints={breakpoints} labelDropdownToggleRef={labelDropdownToggleRef} moveDropdownToggleRef={moveDropdownToggleRef} + filterDropdownToggleRef={filterDropdownToggleRef} parentMessageRef={elementRef} /> <MessageBody diff --git a/applications/mail/src/app/components/message/header/HeaderExpanded.tsx b/applications/mail/src/app/components/message/header/HeaderExpanded.tsx index 5584310103..6f0dd6de5e 100644 --- a/applications/mail/src/app/components/message/header/HeaderExpanded.tsx +++ b/applications/mail/src/app/components/message/header/HeaderExpanded.tsx @@ -64,6 +64,7 @@ interface Props { breakpoints: Breakpoints; labelDropdownToggleRef: React.MutableRefObject<() => void>; moveDropdownToggleRef: React.MutableRefObject<() => void>; + filterDropdownToggleRef: React.MutableRefObject<() => void>; parentMessageRef: React.RefObject<HTMLElement>; } @@ -86,6 +87,7 @@ const HeaderExpanded = ({ breakpoints, labelDropdownToggleRef, moveDropdownToggleRef, + filterDropdownToggleRef, parentMessageRef, }: Props) => { const [addresses = []] = useAddresses(); @@ -324,6 +326,7 @@ const HeaderExpanded = ({ onContactEdit={onContactEdit} labelDropdownToggleRef={labelDropdownToggleRef} moveDropdownToggleRef={moveDropdownToggleRef} + filterDropdownToggleRef={filterDropdownToggleRef} /> </div> {!isScheduledMessage && ( diff --git a/applications/mail/src/app/components/message/header/HeaderMoreDropdown.tsx b/applications/mail/src/app/components/message/header/HeaderMoreDropdown.tsx index 901d03287c..aeeac6c686 100644 --- a/applications/mail/src/app/components/message/header/HeaderMoreDropdown.tsx +++ b/applications/mail/src/app/components/message/header/HeaderMoreDropdown.tsx @@ -40,6 +40,7 @@ import { updateAttachment } from '../../../logic/attachments/attachmentsActions' import { MessageState, MessageStateWithData, MessageWithOptionalBody } from '../../../logic/messages/messagesTypes'; import { Element } from '../../../models/element'; import { Breakpoints } from '../../../models/utils'; +import CustomFilterDropdown from '../../dropdown/CustomFilterDropdown'; import LabelDropdown from '../../dropdown/LabelDropdown'; import MoveDropdown from '../../dropdown/MoveDropdown'; import MessageDetailsModal from '../modals/MessageDetailsModal'; @@ -67,6 +68,7 @@ interface Props { onContactEdit: (props: ContactEditProps) => void; labelDropdownToggleRef: React.MutableRefObject<() => void>; moveDropdownToggleRef: React.MutableRefObject<() => void>; + filterDropdownToggleRef: React.MutableRefObject<() => void>; } const HeaderMoreDropdown = ({ @@ -85,6 +87,7 @@ const HeaderMoreDropdown = ({ onContactEdit, labelDropdownToggleRef, moveDropdownToggleRef, + filterDropdownToggleRef, }: Props) => { const location = useLocation(); const api = useApi(); @@ -176,6 +179,9 @@ const HeaderMoreDropdown = ({ breakpoints={breakpoints} /> ), + ({ onClose, onLock }) => ( + <CustomFilterDropdown message={message.data as Message} onClose={onClose} onLock={onLock} /> + ), ] : undefined; @@ -233,6 +239,15 @@ const HeaderMoreDropdown = ({ ) : ( c('Title').t`Label as` ); + const titleFilterOn = Shortcuts ? ( + <> + {c('Title').t`Filter on...`} + <br /> + <kbd className="border-none">F</kbd> + </> + ) : ( + c('Title').t`Filter on...` + ); return ( <> @@ -322,6 +337,23 @@ const HeaderMoreDropdown = ({ /> )} </HeaderDropdown>, + <HeaderDropdown + key="message-header-expanded:filter-dropdown" + icon + autoClose={false} + noMaxSize + content={<Icon name="filter" alt={c('Action').t`Filter on...`} />} + className="messageFilterDropdownButton" + dropDownClassName="filter-dropdown" + title={titleFilterOn} + loading={!messageLoaded} + externalToggleRef={filterDropdownToggleRef} + data-testid="message-header-expanded:filter-dropdown" + > + {({ onClose, onLock }) => ( + <CustomFilterDropdown message={message.data as Message} onClose={onClose} onLock={onLock} /> + )} + </HeaderDropdown>, ]} <HeaderDropdown icon @@ -374,6 +406,16 @@ const HeaderMoreDropdown = ({ <span className="flex-item-fluid myauto">{c('Action').t`Label as...`}</span> </DropdownMenuButton> )} + {isNarrow && ( + <DropdownMenuButton + className="text-left flex flex-nowrap flex-align-items-center" + onClick={() => onOpenAdditionnal(2)} + > + <Icon name="filter" className="mr0-5" /> + <span className="flex-item-fluid mtauto mbauto">{c('Action') + .t`Filter on...`}</span> + </DropdownMenuButton> + )} {isSpam ? ( <DropdownMenuButton className="text-left flex flex-nowrap flex-align-items-center" diff --git a/applications/mail/src/app/hooks/message/useMessageHotkeys.tsx b/applications/mail/src/app/hooks/message/useMessageHotkeys.tsx index 5881941da6..b5c2c2bad4 100644 --- a/applications/mail/src/app/hooks/message/useMessageHotkeys.tsx +++ b/applications/mail/src/app/hooks/message/useMessageHotkeys.tsx @@ -77,6 +77,7 @@ export const useMessageHotkeys = ( const labelDropdownToggleRef = useRef<() => void>(noop); const moveDropdownToggleRef = useRef<() => void>(noop); + const filterDropdownToggleRef = useRef<() => void>(noop); const markAs = useMarkAs(); const { moveToFolder, moveScheduledModal, moveAllModal, moveToSpamModal } = useMoveToFolder(); @@ -306,6 +307,15 @@ export const useMessageHotkeys = ( } }, ], + [ + 'F', + (e) => { + if (hotkeysEnabledAndMessageReady) { + e.stopPropagation(); + filterDropdownToggleRef.current?.(); + } + }, + ], ]; useHotkeys(elementRef, shortcutHandlers, { @@ -315,6 +325,7 @@ export const useMessageHotkeys = ( return { labelDropdownToggleRef, moveDropdownToggleRef, + filterDropdownToggleRef, moveScheduledModal, moveAllModal, moveToSpamModal, |