Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ProtonMail/WebClients.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorromain sanchez <romain.sanchez@proton.ch>2022-09-27 13:45:02 +0300
committerRichard <richard@protonmail.com>2022-09-28 10:55:01 +0300
commitbdcfa3691ec9ee39fe5c9e40210e55f749c7fd37 (patch)
tree3b6d03feab3e5effc7aa3b79df4ace6ec579bf24
parenta6ef252c3a3b29ee7a5971f0cc626cf410ce5a1c (diff)
Add back filter button in the message view
MAILWEB-3608
-rw-r--r--applications/mail/src/app/components/dropdown/CustomFilterDropdown.tsx226
-rw-r--r--applications/mail/src/app/components/message/MessageView.tsx55
-rw-r--r--applications/mail/src/app/components/message/header/HeaderExpanded.tsx3
-rw-r--r--applications/mail/src/app/components/message/header/HeaderMoreDropdown.tsx42
-rw-r--r--applications/mail/src/app/hooks/message/useMessageHotkeys.tsx11
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,