Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
143 changes: 143 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,143 @@
import { useTranslationContext } from '@/i18n';
import {
ArrowBendDoubleUpLeftIcon,
ArrowBendUpLeftIcon,
ArrowBendUpRightIcon,
EnvelopeOpenIcon,
TrashIcon,
TrayIcon,
WarningOctagonIcon,
} from '@phosphor-icons/react';
import MoveTo from '@/assets/icons/move-to.svg?react';
import { Dropdown, type MenuItemType } from '@internxt/ui';
import { isCurrentPath } from '@/utils/current-path';
import { useLocation } from 'react-router-dom';
import { useCallback, useMemo } from 'react';
import type { FolderType } from '@/types/mail';

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 } = useTranslationContext();
const { pathname } = useLocation();

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

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],
);
Comment thread
xabg2 marked this conversation as resolved.
Outdated
Comment thread
xabg2 marked this conversation as resolved.
Outdated

return (
<div className="flex items-center z-30 gap-5 pl-4">
<button
disabled={optionsDisabled}
type="button"
title={isRead ? translate('actions.markAsUnread') : translate('actions.markAsRead')}
className={iconButtonClass}
onClick={isRead ? onMarkAsUnread : onMarkAsRead}
>
<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}
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;
Loading
Loading