Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
npm run check:i18n
npx lint-staged
npm run test:related
1,644 changes: 1,644 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"test:related": "vitest run --changed HEAD --passWithNoTests",
"prepare": "husky",
"format": "prettier --write **/*.{js,jsx,tsx,ts}",
"check:i18n": "node scripts/check-i18n.cjs",
"preinstall": "node scripts/check-package-manager.js"
"preinstall": "node scripts/check-package-manager.js",
"check:i18n": "node scripts/check-i18n.cjs"
},
"dependencies": {
"@internxt/css-config": "^1.1.0",
Expand Down Expand Up @@ -94,4 +94,4 @@
"prettier --write"
]
}
}
}
4 changes: 4 additions & 0 deletions src/assets/icons/move-to.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion src/errors/mail/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,22 @@ export class FetchMessageError extends Error {
export class UpdateMailError extends Error {
constructor(
errorMsg?: string,
action?: 'markAsRead' | 'markAsFlagged' | 'markAsUnflagged',
action?: 'markAsRead' | 'markAsFlagged' | 'markAsUnflagged' | 'moveToFolder',
Comment thread
xabg2 marked this conversation as resolved.
public requestId?: string,
) {
super(`Error while updating mail when ${action}: ` + errorMsg);

Object.setPrototypeOf(this, UpdateMailError.prototype);
}
}

export class DeleteEmailError extends Error {
constructor(
errorMsg?: string,
public requestId?: string,
) {
super('Error while deleting email: ' + errorMsg);

Object.setPrototypeOf(this, DeleteEmailError.prototype);
}
}
50 changes: 37 additions & 13 deletions src/features/mail/MailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { useTranslationContext } from '@/i18n';
import type { FolderType } from '@/types/mail';
import PreviewMail from './components/mail-preview';
import Settings from './components/settings';
import { useGetMailMessageQuery, useMarkAsReadMutation } from '@/store/api/mail';
import {
useDeleteMailsMutation,
useGetMailMessageQuery,
useMoveToFolderMutation,
useUpdateReadStatusMutation,
} from '@/store/api/mail';
import { ErrorService } from '@/services/error';
import useListFolderPaginated from '@/hooks/mail/useListFolderPaginated';
import { useUnreadByMailbox } from '@/hooks/mail/useUnreadByMailbox';
Expand All @@ -14,6 +19,8 @@ import { Tray } from '@internxt/ui';
import { TrayEmptyState } from './components/tray/tray-empty-state';
import { formatEmailsToList } from '@/utils/format-emails';
import { useListActionContext } from '@/hooks/mail/useListActionContext';
import { usePreviewMailActions } from '@/hooks/mail/usePreviewMailActions';
import ActionsBar from './components/mail-preview/actions-bar';

interface MailViewProps {
folder: FolderType;
Expand All @@ -22,32 +29,47 @@ interface MailViewProps {
const MailView = ({ folder }: MailViewProps) => {
const { translate } = useTranslationContext();
const [activeMailId, setActiveMailId] = useState<string | undefined>(undefined);
const [updateReadStatus] = useUpdateReadStatusMutation();
const [moveToFolder] = useMoveToFolderMutation();
const [deleteEmails] = useDeleteMailsMutation();

const { data: activeMailData } = useGetMailMessageQuery({ emailId: activeMailId! }, { skip: !activeMailId });
const activeMail = activeMailId ? activeMailData : undefined;
Comment thread
xabg2 marked this conversation as resolved.
const {
isLoadingListFolder,
listFolderEmails,
hasMoreEmails,
onLoadMore,
isUnreadFilter,
listEmailsCount,
onLoadMore,
toggleUnreadFilter,
applyUnreadFilter,
} = useListFolderPaginated(folder);

const { selectedEmails, selectAll, selectNone, selectRead, selectUnread, toggleSelectAll } =
useMailSelection(listFolderEmails);
const { listActionContext, bulkActionContext } = useListActionContext(folder, {
const { listActionContext, bulkActionContext } = useListActionContext(folder, selectedEmails, {
selectAll,
selectNone,
selectRead,
selectUnread,
applyUnreadFilter,
deleteEmails: (emailIds) => deleteEmails({ emailIds, sourceMailbox: folder }).unwrap(),
});
const previewActions = usePreviewMailActions({
activeMailId,
folder,
clearActiveMail: () => setActiveMailId(undefined),
updateReadStatus: async (args) => {
await updateReadStatus(args).unwrap();
},
moveToFolder: async (args) => {
await moveToFolder(args).unwrap();
},
deleteEmails: async (args) => {
await deleteEmails(args).unwrap();
},
});
Comment thread
xabg2 marked this conversation as resolved.
const { unreadByMailbox } = useUnreadByMailbox();

const listEmailsCount = listFolderEmails?.length;

const { data: activeMail } = useGetMailMessageQuery({ emailId: activeMailId! }, { skip: !activeMailId });
const [markAsRead] = useMarkAsReadMutation();

const folderName = translate(`mail.${folder}`);

const from = activeMail?.from[0];
Expand All @@ -61,9 +83,10 @@ const MailView = ({ folder }: MailViewProps) => {
if (isRead) return;

try {
await markAsRead({
await updateReadStatus({
emailId: id,
mailbox: folder,
isRead: true,
});
} catch (error) {
const err = ErrorService.instance.castError(error);
Expand All @@ -86,7 +109,7 @@ const MailView = ({ folder }: MailViewProps) => {
selectedCount={selectedEmails.length}
totalCount={listFolderEmails?.length ?? 0}
onCheckboxClicked={toggleSelectAll}
onToggleUnreadFilter={folder !== 'sent' ? toggleUnreadFilter : undefined}
onToggleUnreadFilter={folder === 'sent' ? undefined : toggleUnreadFilter}
onSearchEmailSelected={onSelectEmail}
/>
</div>
Expand All @@ -105,7 +128,8 @@ const MailView = ({ folder }: MailViewProps) => {
</div>
{/* Mail Preview */}
<div className="flex flex-col w-full">
<div className="flex w-full justify-end">
<div className="flex flex-row w-full pl-1 justify-between">
<ActionsBar isRead={activeMail?.isRead ?? false} optionsDisabled={!activeMailId} {...previewActions} />
<Settings />
</div>

Expand Down
118 changes: 118 additions & 0 deletions src/features/mail/components/mail-preview/actions-bar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
ArrowBendDoubleUpLeftIcon,
ArrowBendUpLeftIcon,
ArrowBendUpRightIcon,
EnvelopeOpenIcon,
TrashIcon,
} from '@phosphor-icons/react';
import MoveTo from '@/assets/icons/move-to.svg?react';
import { Dropdown } from '@internxt/ui';
import type { FolderType } from '@/types/mail';
import { useActionsBar } from '../../../../../hooks/mail/useActionsBar';

interface ActionsBarProps {
isRead: boolean;
optionsDisabled: boolean;
onMarkAsRead: () => void;
onMarkAsUnread: () => void;
onReply: () => void;
onReplyAll: () => void;
onForward: () => void;
onMove: (folder: FolderType) => Promise<null> | void;
onTrash: () => Promise<null> | void;
Comment thread
xabg2 marked this conversation as resolved.
}

const Separator = () => <div className="h-5 w-px bg-gray-10" />;

const ActionsBar = ({
isRead,
optionsDisabled,
onMarkAsRead,
onMarkAsUnread,
onTrash,
onMove,
onReply,
onReplyAll,
onForward,
}: ActionsBarProps) => {
const { translate, moveToItems, toggleReadTitle, onToggleRead } = useActionsBar({
isRead,
optionsDisabled,
onMarkAsRead,
onMarkAsUnread,
onMove,
onTrash,
});

const iconButtonClass =
'flex items-center justify-center rounded-lg p-2 text-gray-60 transition-colors hover:bg-gray-5 hover:text-gray-80 disabled:pointer-events-none disabled:opacity-40';

return (
<div className="flex items-center z-30 gap-5 pl-4">
<button
disabled={optionsDisabled}
type="button"
title={toggleReadTitle}
className={iconButtonClass}
onClick={onToggleRead}
>
<EnvelopeOpenIcon size={24} />
</button>

<div className="flex flex-row gap-1 items-center">
<button
disabled={optionsDisabled}
type="button"
title={translate('actions.trashEmail')}
className={iconButtonClass}
onClick={onTrash}
>
<TrashIcon size={24} />
</button>
<Separator />
<Dropdown
classButton={`${iconButtonClass}${optionsDisabled ? ' pointer-events-none opacity-40' : ''}`}
dropdownActionsContext={moveToItems}
openDirection="left"
classMenuItems="max-w-[224px] w-screen"
>
<MoveTo />
</Dropdown>
</div>

<div className="flex flex-row items-center gap-1">
<button
disabled={optionsDisabled}
type="button"
title={translate('actions.reply')}
className={iconButtonClass}
onClick={onReply}
>
<ArrowBendUpLeftIcon size={24} />
</button>
<Separator />
<button
disabled={optionsDisabled}
type="button"
title={translate('actions.replyAll')}
className={iconButtonClass}
onClick={onReplyAll}
>
<ArrowBendDoubleUpLeftIcon size={24} />
</button>
<Separator />
<button
disabled={optionsDisabled}
type="button"
title={translate('actions.forward')}
className={iconButtonClass}
onClick={onForward}
>
<ArrowBendUpRightIcon size={24} />
</button>
</div>
</div>
);
};

export default ActionsBar;
65 changes: 65 additions & 0 deletions src/hooks/mail/useActionsBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useTranslationContext } from '@/i18n';
import { isCurrentPath } from '@/utils/current-path';
import { useLocation } from 'react-router-dom';
import { useCallback, useMemo } from 'react';
import type { FolderType } from '@/types/mail';
import { TrayIcon, WarningOctagonIcon, TrashIcon } from '@phosphor-icons/react';
import type { MenuItemType } from '@internxt/ui';

interface UseActionsBarParams {
isRead: boolean;
optionsDisabled: boolean;
onMarkAsRead: () => void;
onMarkAsUnread: () => void;
onMove: (folder: FolderType) => Promise<null> | void;
onTrash: () => Promise<null> | void;
}

export const useActionsBar = ({
isRead,
optionsDisabled,
onMarkAsRead,
onMarkAsUnread,
onMove,
onTrash,
}: UseActionsBarParams) => {
const { translate } = useTranslationContext();
const { pathname } = useLocation();

const isActive = useCallback((path: string) => isCurrentPath(path, pathname), [pathname]);

const moveToItems: MenuItemType<unknown>[] = useMemo(
() => [
{
disabled: () => isActive('/inbox') || optionsDisabled,
name: translate('mail.inbox'),
icon: TrayIcon,
onClick: () => onMove('inbox'),
},
{
disabled: () => isActive('/spam') || optionsDisabled,
name: translate('mail.spam'),
icon: WarningOctagonIcon,
onClick: () => onMove('spam'),
},
{
disabled: () => isActive('/trash') || optionsDisabled,
name: translate('mail.trash'),
icon: TrashIcon,
onClick: () => onMove('trash'),
},
],
[optionsDisabled, isActive, onMove, translate],
);

const toggleReadTitle = isRead ? translate('actions.markAsUnread') : translate('actions.markAsRead');
const onToggleRead = isRead ? onMarkAsUnread : onMarkAsRead;

return {
translate,
moveToItems,
toggleReadTitle,
onToggleRead,
onTrash,
};
};
Loading
Loading