Merge branch 'staging' into feat/signatures

This commit is contained in:
plyght
2025-04-06 08:52:44 -04:00
committed by GitHub
24 changed files with 690 additions and 427 deletions

View File

@@ -23,6 +23,7 @@ import {
Star,
StarOff,
Trash,
MailOpen,
} from 'lucide-react';
import { moveThreadsTo, ThreadDestination } from '@/lib/thread-actions';
import { useSearchValue } from '@/hooks/use-search-value';
@@ -277,7 +278,7 @@ export function ThreadContextMenu({
{
id: 'toggle-read',
label: isUnread ? t('common.mail.markAsRead') : t('common.mail.markAsUnread'),
icon: <Mail className="mr-2.5 h-4 w-4" />,
icon: isUnread ? <MailOpen className="mr-2.5 h-4 w-4" /> : <Mail className="mr-2.5 h-4 w-4" />,
shortcut: 'U',
action: handleReadUnread,
disabled: false,

View File

@@ -556,7 +556,12 @@ export const MailList = memo(({ isCompact }: MailListProps) => {
const messageThreadId = message.threadId ?? message.id;
// Update local state immediately for optimistic UI
setMail((prev) => ({ ...prev, selected: messageThreadId }));
setMail((prev) => ({
...prev,
selected: messageThreadId,
replyComposerOpen: false,
forwardComposerOpen: false
}));
// Update URL param without navigation
void setThreadId(messageThreadId);

View File

@@ -15,6 +15,7 @@ import {
Archive,
RotateCw,
Mail,
MailOpen,
} from 'lucide-react';
import {
Dialog,
@@ -54,9 +55,9 @@ import { useSession } from '@/lib/auth-client';
import { useStats } from '@/hooks/use-stats';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { getMail } from '@/actions/mail';
import { getMail, markAsRead } from '@/actions/mail';
import { SearchBar } from './search-bar';
import { ParsedMessage } from '@/types';
import { useQueryState } from 'nuqs';
import { cn } from '@/lib/utils';
import items from './demo.json';
import { useAtom } from 'jotai';
@@ -72,8 +73,7 @@ export function DemoMailLayout() {
const isValidating = false;
const isLoading = false;
const isDesktop = true;
const searchParams = useSearchParams();
const threadIdParam = searchParams?.get('threadId');
const threadIdParam = useQueryState('threadId');
const [activeCategory, setActiveCategory] = useState('Primary');
const [filteredItems, setFilteredItems] = useState(items);
@@ -247,17 +247,12 @@ export function MailLayout() {
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
const searchParams = useSearchParams();
const threadIdParam = searchParams.get('threadId');
// No need to track threadIdParam with a separate state
const [threadId, setThreadId] = useQueryState('threadId');
const handleClose = useCallback(() => {
// Update URL to remove threadId parameter
const currentParams = new URLSearchParams(searchParams.toString());
currentParams.delete('threadId');
router.push(`/mail/${folder}?${currentParams.toString()}`);
}, [router, folder, searchParams]);
setThreadId(null);
router.push(`/mail/${folder}`);
}, [router, folder, setThreadId]);
// Search bar is always visible now, no need for keyboard shortcuts to toggle it
useHotKey('Esc', (event) => {
@@ -265,8 +260,6 @@ export function MailLayout() {
// Handle other Esc key functionality if needed
});
const searchIconRef = useRef<any>(null);
// Add mailto protocol handler registration
useEffect(() => {
// Register as a mailto protocol handler if browser supports it
@@ -298,7 +291,7 @@ export function MailLayout() {
className="rounded-inherit gap-1.5 overflow-hidden"
>
<ResizablePanel
className={cn('border-none !bg-transparent', threadIdParam ? 'md:hidden lg:block' : '')}
className={cn('border-none !bg-transparent', threadId ? 'md:hidden lg:block' : '')}
defaultSize={isMobile ? 100 : 25}
minSize={isMobile ? 100 : 25}
>
@@ -362,7 +355,7 @@ export function MailLayout() {
<div className="flex flex-1 justify-center">
<SearchBar />
</div>
{!threadIdParam && (
{!threadId && (
<div className="flex items-center">
<CategorySelect />
</div>
@@ -402,13 +395,13 @@ export function MailLayout() {
<ResizablePanel
className={cn(
'bg-offsetLight dark:bg-offsetDark shadow-sm md:rounded-2xl md:border md:shadow-sm',
threadIdParam ? 'md:flex' : 'hidden',
threadId ? 'md:flex' : 'hidden',
)}
defaultSize={75}
minSize={25}
>
<div className="relative hidden h-[calc(100vh-(12px+14px))] flex-1 md:block">
<ThreadDisplay onClose={handleClose} id={threadIdParam ?? undefined} />
<ThreadDisplay onClose={handleClose} id={threadId ?? undefined} />
</div>
</ResizablePanel>
</>
@@ -418,7 +411,7 @@ export function MailLayout() {
{/* Mobile Drawer */}
{isMobile && (
<Drawer
open={!!threadIdParam}
open={!!threadId}
onOpenChange={(isOpen) => {
if (!isOpen) handleClose();
}}
@@ -429,8 +422,8 @@ export function MailLayout() {
</DrawerHeader>
<div className="flex h-full flex-col overflow-hidden">
<div className="flex-1 overflow-hidden">
{threadIdParam ? (
<ThreadDisplay onClose={handleClose} isMobile={true} id={threadIdParam} />
{threadId ? (
<ThreadDisplay onClose={handleClose} isMobile={true} id={threadId} />
) : null}
</div>
</div>
@@ -480,6 +473,25 @@ function BulkSelectActions() {
const { mutate: mutateThreads } = useThreads();
const { mutate: mutateStats } = useStats();
const handleMarkAsRead = useCallback(async () => {
try {
const response = await markAsRead({ ids: mail.bulkSelected });
if (response.success) {
await mutateThreads();
await mutateStats();
setMail((prev) => ({
...prev,
bulkSelected: []
}));
toast.success(t('common.mail.markedAsRead'));
}
} catch (error) {
console.error("Error marking as read", error);
toast.error(t("common.mail.failedToMarkAsRead"));
}
}, [mail, setMail, mutateThreads, mutateStats, t]);
const onMoveSuccess = useCallback(async () => {
await mutateThreads();
await mutateStats();
@@ -548,6 +560,18 @@ function BulkSelectActions() {
</TooltipTrigger>
<TooltipContent>{t('common.mail.mute')}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="md:h-fit md:px-2"
onClick={handleMarkAsRead}
>
<MailOpen />
</Button>
</TooltipTrigger>
<TooltipContent>{t('common.mail.markAsRead')}</TooltipContent>
</Tooltip>
{availableActions.map((action) => (
<Tooltip key={action}>
@@ -632,18 +656,10 @@ const Categories = () => {
function CategorySelect() {
const [, setSearchValue] = useSearchValue();
const categories = Categories();
const [defaultCategory, setDefaultCategory] = useState('Primary');
// Safely access localStorage on the client side only
useEffect(() => {
// Check if we're in the browser environment
if (typeof window !== 'undefined') {
const savedCategory = localStorage.getItem('mailActiveCategory');
if (savedCategory) {
setDefaultCategory(savedCategory);
}
}
}, []);
const router = useRouter();
const [category, setCategory] = useQueryState('category', {
defaultValue: 'Primary'
});
return (
<Select
@@ -651,9 +667,6 @@ function CategorySelect() {
// Find the category and trigger its selection
const category = categories.find((cat) => cat.id === value);
// Always update the state to match the selected value
setDefaultCategory(value);
if (category) {
// Update search value based on category
const searchValueState = {
@@ -663,13 +676,11 @@ function CategorySelect() {
};
setSearchValue(searchValueState);
// Save to localStorage (safely on client-side)
if (typeof window !== 'undefined') {
localStorage.setItem('mailActiveCategory', value);
}
// Update category in URL - nuqs will preserve other params automatically
setCategory(value);
}
}}
defaultValue={defaultCategory}
value={category}
>
<SelectTrigger className="bg-popover h-9 w-36">
<SelectValue placeholder="Select category" />

View File

@@ -43,6 +43,8 @@ import { toast } from 'sonner';
import type { z } from 'zod';
import { useSettings } from '@/hooks/use-settings';
import { Switch } from '@/components/ui/switch';
import { useMail } from '@/components/mail/use-mail';
// Define state interfaces
interface ComposerState {
@@ -116,8 +118,6 @@ const aiReducer = (state: AIState, action: AIAction): AIState => {
interface ReplyComposeProps {
emailData: ParsedMessage[];
isOpen?: boolean;
setIsOpen?: Dispatch<SetStateAction<boolean>>;
mode?: 'reply' | 'forward';
}
@@ -126,9 +126,20 @@ type FormData = {
to: string;
};
export default function ReplyCompose({ emailData, isOpen, setIsOpen, mode = 'reply' }: ReplyComposeProps) {
export default function ReplyCompose({ emailData, mode = 'reply' }: ReplyComposeProps) {
const [attachments, setAttachments] = useState<File[]>([]);
const { data: session } = useSession();
const [mail, setMail] = useMail();
// Use global state instead of local state
const composerIsOpen = mode === 'reply' ? mail.replyComposerOpen : mail.forwardComposerOpen;
const setComposerIsOpen = (value: boolean) => {
setMail((prev: typeof mail) => ({
...prev,
replyComposerOpen: mode === 'reply' ? value : prev.replyComposerOpen,
forwardComposerOpen: mode === 'forward' ? value : prev.forwardComposerOpen,
}));
};
// Use reducers instead of multiple useState
const [composerState, composerDispatch] = useReducer(composerReducer, {
@@ -150,16 +161,6 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen, mode = 'rep
const composerRef = useRef<HTMLFormElement>(null);
const t = useTranslations();
// Use external state if provided, otherwise use internal state
const composerIsOpen = isOpen !== undefined ? isOpen : composerState.isComposerOpen;
const setComposerIsOpen = (value: boolean) => {
if (setIsOpen) {
setIsOpen(value);
} else {
composerDispatch({ type: 'SET_COMPOSER_OPEN', payload: value });
}
};
// Handle keyboard shortcuts for sending email
const handleKeyDown = (e: React.KeyboardEvent) => {
// Check for Cmd/Ctrl + Enter
@@ -644,11 +645,30 @@ ${email.decodedBody || 'No content'}
}
};
// Add this effect near other useEffects
useEffect(() => {
if (!composerIsOpen) {
// Reset form state
form.reset();
// Reset attachments
setAttachments([]);
// Reset AI state
aiDispatch({ type: 'RESET' });
// Reset to emails if in forward mode
if (mode === 'forward') {
setToEmails([]);
setToInput('');
}
// Reset editor key to force a fresh instance
composerDispatch({ type: 'INCREMENT_EDITOR_KEY' });
}
}, [composerIsOpen, form, mode]);
// Simplified composer visibility check
if (!composerIsOpen) {
if (mode === 'reply') {
return (
<div className="bg-offsetLight dark:bg-offsetDark w-full p-2">
<div className="bg-offsetLight dark:bg-offsetDark w-full px-2">
<Button
onClick={toggleComposer}
className="flex h-12 w-full items-center justify-center gap-2 rounded-md"
@@ -667,11 +687,11 @@ ${email.decodedBody || 'No content'}
}
return (
<div className="bg-offsetLight dark:bg-offsetDark w-full p-2">
<div className="bg-offsetLight dark:bg-offsetDark w-full px-2">
<form
ref={composerRef}
className={cn(
'border-border ring-offset-background flex flex-col space-y-2.5 rounded-[10px] border px-2 py-2 transition-all duration-300 ease-in-out',
'border-border ring-offset-background relative z-20 flex flex-col space-y-2.5 rounded-[10px] border px-2 py-2 transition-all duration-300 ease-in-out',
composerState.isEditorFocused ? 'ring-2 ring-[#3D3D3D] ring-offset-1' : '',
)}
style={{
@@ -896,14 +916,17 @@ ${email.decodedBody || 'No content'}
}
// Extract smaller components
const DragOverlay = () => (
<div className="bg-background/80 border-primary/30 absolute inset-0 z-50 m-4 flex items-center justify-center rounded-2xl border-2 border-dashed backdrop-blur-sm">
<div className="text-muted-foreground flex flex-col items-center gap-2">
<Paperclip className="text-muted-foreground h-12 w-12" />
<p className="text-lg font-medium">{t('common.replyCompose.dropFiles')}</p>
const DragOverlay = () => {
const t = useTranslations();
return (
<div className="bg-background/80 border-primary/30 absolute inset-0 z-50 m-4 flex items-center justify-center rounded-2xl border-2 border-dashed backdrop-blur-sm">
<div className="text-muted-foreground flex flex-col items-center gap-2">
<Paperclip className="text-muted-foreground h-12 w-12" />
<p className="text-lg font-medium">{t('common.replyCompose.dropFiles')}</p>
</div>
</div>
</div>
);
);
};
const CloseButton = ({ onClick }: { onClick: (e: React.MouseEvent) => void }) => (
<Button

View File

@@ -3,6 +3,7 @@ import {
ArchiveX,
Expand,
Forward,
ForwardIcon,
Mail,
MoreVertical,
Reply,
@@ -11,14 +12,13 @@ import {
StarOff,
X,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { DropdownMenu, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useSearchParams, useParams } from 'next/navigation';
import { ScrollArea } from '@/components/ui/scroll-area';
import { moveThreadsTo, ThreadDestination } from '@/lib/thread-actions';
import { markAsUnread } from '@/actions/mail';
import { MoreVerticalIcon } from '../icons/animated/more-vertical';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useThread, useThreads } from '@/hooks/use-threads';
@@ -27,12 +27,13 @@ import { ExpandIcon } from '../icons/animated/expand';
import { MailDisplaySkeleton } from './mail-skeleton';
import { ReplyIcon } from '../icons/animated/reply';
import { Button } from '@/components/ui/button';
import { markAsUnread } from '@/actions/mail';
import { modifyLabels } from '@/actions/mail';
import { useStats } from '@/hooks/use-stats';
import ThreadSubject from './thread-subject';
import { XIcon } from '../icons/animated/x';
import ReplyCompose from './reply-composer';
import { useTranslations } from 'next-intl';
import { useMail } from '../mail/use-mail';
import { NotesPanel } from './note-panel';
import { cn, FOLDERS } from '@/lib/utils';
import MailDisplay from './mail-display';
@@ -40,7 +41,6 @@ import { ParsedMessage } from '@/types';
import { Inbox } from 'lucide-react';
import { toast } from 'sonner';
import Link from 'next/link';
import { useMail } from '../mail/use-mail';
interface ThreadDisplayProps {
threadParam?: any;
@@ -52,6 +52,7 @@ interface ThreadDisplayProps {
export function ThreadDemo({ messages, isMobile }: ThreadDisplayProps) {
const isFullscreen = false;
const [mail, setMail] = useMail();
return (
<div
@@ -68,69 +69,6 @@ export function ThreadDemo({ messages, isMobile }: ThreadDisplayProps) {
isFullscreen ? 'fixed inset-0 z-50' : '',
)}
>
<div className="flex flex-shrink-0 items-center border-b p-2">
<div className="flex flex-1 items-center gap-2">
<Button variant="ghost" className="md:h-fit md:px-2" disabled={!messages}>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
<ThreadSubject subject={'Join the Email Revolution with Zero!'} />
</div>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="md:h-fit md:px-2" disabled={!messages}>
{isFullscreen ? (
<ExpandIcon className="h-4 w-4" />
) : (
<ExpandIcon className="h-4 w-4" />
)}
<span className="sr-only">
{isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="md:h-fit md:px-2" disabled={!messages}>
<ArchiveIcon className="relative top-0.5 h-4 w-4" />
<span className="sr-only">Archive</span>
</Button>
</TooltipTrigger>
<TooltipContent>Archive</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="md:h-fit md:px-2" disabled={!messages}>
<ReplyIcon className="h-4 w-4" />
<span className="sr-only">Reply</span>
</Button>
</TooltipTrigger>
<TooltipContent>Reply</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="md:h-fit md:px-2" disabled={!messages}>
<MoreVerticalIcon className="h-4 w-4" />
<span className="sr-only">More</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<ArchiveX className="mr-2 h-4 w-4" /> Move to spam
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="mr-2 h-4 w-4" /> Forward
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<ScrollArea className="flex-1" type="scroll">
<div className="pb-4">
@@ -154,9 +92,12 @@ export function ThreadDemo({ messages, isMobile }: ThreadDisplayProps) {
))}
</div>
</ScrollArea>
<div className="relative flex-shrink-0 md:top-2">
<div className="relative flex-shrink-0 md:top-1">
{messages ? (
<ReplyCompose emailData={messages} isOpen={false} setIsOpen={() => {}} />
<ReplyCompose
emailData={messages}
mode={mail.forwardComposerOpen ? 'forward' : 'reply'}
/>
) : null}
</div>
</div>
@@ -184,17 +125,24 @@ function ThreadActionButton({
const iconRef = useRef<any>(null);
return (
<Button
disabled={disabled}
onClick={onClick}
variant="ghost"
className={cn('md:h-fit md:px-2', className)}
onMouseEnter={() => iconRef.current?.startAnimation?.()}
onMouseLeave={() => iconRef.current?.stopAnimation?.()}
>
<Icon ref={iconRef} className="h-4 w-4" />
<span className="sr-only">{label}</span>
</Button>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={disabled}
onClick={onClick}
variant="ghost"
className={cn('md:h-fit md:px-2', className)}
onMouseEnter={() => iconRef.current?.startAnimation?.()}
onMouseLeave={() => iconRef.current?.stopAnimation?.()}
>
<Icon ref={iconRef} className="h-4 w-4" />
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
@@ -203,15 +151,13 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
const { mutate: mutateThreads } = useThreads();
const searchParams = useSearchParams();
const [isMuted, setIsMuted] = useState(false);
const [isReplyOpen, setIsReplyOpen] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isForwardOpen, setIsForwardOpen] = useState(false);
const [mail, setMail] = useMail();
const t = useTranslations();
const { mutate: mutateStats } = useStats();
const { folder } = useParams<{ folder: string }>();
const threadIdParam = searchParams.get('threadId');
const threadId = threadParam ?? threadIdParam ?? '';
const [, setMailState] = useMail();
const moreVerticalIconRef = useRef<any>(null);
@@ -220,8 +166,14 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
const isInSpam = folder === FOLDERS.SPAM;
const handleClose = useCallback(() => {
// Reset reply composer state when closing thread display
setMail((prev) => ({
...prev,
replyComposerOpen: false,
forwardComposerOpen: false
}));
onClose?.();
}, [onClose]);
}, [onClose, setMail]);
const moveThreadTo = useCallback(
async (destination: ThreadDestination) => {
@@ -237,11 +189,12 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
toast.promise(promise(), {
loading: t('common.actions.moving'),
success: destination === 'inbox'
? t('common.actions.movedToInbox')
: destination === 'spam'
? t('common.actions.movedToSpam')
: t('common.actions.archived'),
success:
destination === 'inbox'
? t('common.actions.movedToInbox')
: destination === 'spam'
? t('common.actions.movedToSpam')
: t('common.actions.archived'),
error: t('common.actions.failedToMove'),
});
},
@@ -250,12 +203,12 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
const handleMarkAsUnread = useCallback(async () => {
if (!emailData || !threadId) return;
const promise = async () => {
const result = await markAsUnread({ ids: [threadId] });
if (!result.success) throw new Error('Failed to mark as unread');
setMailState(prev => ({ ...prev, bulkSelected: [] }));
setMail((prev) => ({ ...prev, bulkSelected: [] }));
await Promise.all([mutateStats(), mutateThreads()]);
handleClose();
};
@@ -265,7 +218,7 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
success: t('common.mail.markedAsUnread'),
error: t('common.mail.failedToMarkAsUnread'),
});
}, [emailData, threadId, mutateStats, mutateThreads, t, handleClose, setMailState]);
}, [emailData, threadId, mutateStats, mutateThreads, t, handleClose, setMail]);
const handleFavourites = async () => {
if (!emailData || !threadId) return;
@@ -274,27 +227,23 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
toast.promise(
modifyLabels({ threadId: [threadId], removeLabels: ['STARRED'] }).then(() => done),
{
success: 'Removed from favourites.',
loading: 'Removing from favourites',
error: 'Failed to remove from favourites.',
success: t('common.actions.removedFromFavorites'),
loading: t('common.actions.removingFromFavorites'),
error: t('common.actions.failedToRemoveFromFavorites'),
},
);
} else {
toast.promise(
modifyLabels({ threadId: [threadId], addLabels: ['STARRED'] }).then(() => done),
{
success: 'Added to favourites.',
loading: 'Adding to favourites.',
error: 'Failed to add to favourites.',
success: t('common.actions.addedToFavorites'),
loading: t('common.actions.addingToFavorites'),
error: t('common.actions.failedToAddToFavorites'),
},
);
}
};
const handleForward = () => {
setIsForwardOpen(true);
};
useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
@@ -321,67 +270,6 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
isFullscreen ? 'fixed inset-0 z-50' : '',
)}
>
<div className="flex flex-shrink-0 items-center border-b px-1 pb-1 md:px-3 md:pb-2 md:pt-[10px]">
<div className="flex flex-1 items-center">
<ThreadActionButton
icon={XIcon}
label={t('common.actions.close')}
onClick={handleClose}
/>
</div>
<div className="flex items-center gap-1 sm:gap-2 md:gap-6">
<ThreadActionButton
icon={isFullscreen ? ExpandIcon : ExpandIcon}
label={
isFullscreen
? t('common.threadDisplay.exitFullscreen')
: t('common.threadDisplay.enterFullscreen')
}
onClick={() => setIsFullscreen(!isFullscreen)}
/>
<ThreadActionButton
icon={ArchiveIcon}
label={t('common.threadDisplay.archive')}
disabled={true}
className="relative top-0.5"
/>
<ThreadActionButton
icon={!emailData || emailData[0]?.tags?.includes('STARRED') ? StarOff : Star}
label={t('common.threadDisplay.favourites')}
onClick={handleFavourites}
className="relative top-0.5"
/>
<ThreadActionButton
icon={ReplyIcon}
label={t('common.threadDisplay.reply')}
disabled={true}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0 md:h-fit md:w-auto md:px-2"
disabled={true}
>
<MoreVerticalIcon className="h-4 w-4" />
<span className="sr-only">{t('common.threadDisplay.moreOptions')}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<ArchiveX className="mr-2 h-4 w-4" /> {t('common.threadDisplay.moveToSpam')}
</DropdownMenuItem>
<DropdownMenuItem>
<ReplyAll className="mr-2 h-4 w-4" /> {t('common.threadDisplay.replyAll')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<ScrollArea className="h-full flex-1" type="auto">
<div className="pb-4">
@@ -402,7 +290,7 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
>
<div
className={cn(
'bg-offsetLight dark:bg-offsetDark relative flex flex-col overflow-hidden transition-all duration-300',
'bg-offsetLight dark:bg-offsetDark relative flex flex-col transition-all duration-300',
isMobile ? 'h-full' : 'h-full',
!isMobile && !isFullscreen && 'rounded-r-lg',
isFullscreen ? 'fixed inset-0 z-50' : '',
@@ -410,15 +298,16 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
>
<div className="flex flex-shrink-0 items-center border-b px-1 pb-1 md:px-3 md:pb-2 md:pt-[10px]">
<div className="flex flex-1 items-center gap-2">
<Link prefetch href={`/mail/${folder}`}>
<Button variant="ghost" className="md:h-fit md:px-2">
<X className="h-4 w-4 hover:text-red-500" />
</Button>
</Link>
<ThreadActionButton
icon={X}
label={t('common.actions.close')}
onClick={handleClose}
/>
<ThreadSubject subject={emailData[0]?.subject} />
</div>
<div className="flex items-center md:gap-2">
<NotesPanel threadId={threadId} />
{/* disable notes for now, it's still a bit buggy and not ready for prod. */}
{/* <NotesPanel threadId={threadId} /> */}
<ThreadActionButton
icon={Expand}
label={
@@ -429,7 +318,7 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
disabled={!emailData}
onClick={() => setIsFullscreen(!isFullscreen)}
/>
{(isInSpam || isInArchive) ? (
{isInSpam || isInArchive ? (
<ThreadActionButton
icon={Inbox}
label={t('common.mail.moveToInbox')}
@@ -462,53 +351,49 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
icon={Reply}
label={t('common.threadDisplay.reply')}
disabled={!emailData}
onClick={() => setIsReplyOpen(true)}
className={cn(mail.replyComposerOpen && "bg-primary/10")}
onClick={() => {
if (mail.forwardComposerOpen) {
// If forward is open, close it and open reply
setMail((prev) => ({
...prev,
forwardComposerOpen: false,
replyComposerOpen: true
}));
} else {
// Toggle reply
setMail((prev) => ({
...prev,
replyComposerOpen: !prev.replyComposerOpen
}));
}
}}
/>
<ThreadActionButton
icon={Forward}
label={t('common.threadDisplay.forward')}
disabled={!emailData}
className={cn(mail.forwardComposerOpen && "bg-primary/10")}
onClick={() => {
if (mail.replyComposerOpen) {
// If reply is open, close it and open forward
setMail((prev) => ({
...prev,
replyComposerOpen: false,
forwardComposerOpen: true
}));
} else {
// Toggle forward
setMail((prev) => ({
...prev,
forwardComposerOpen: !prev.forwardComposerOpen
}));
}
}}
/>
{/* <DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="md:h-fit md:px-2"
disabled={!emailData}
onMouseEnter={() => moreVerticalIconRef.current?.startAnimation?.()}
onMouseLeave={() => moreVerticalIconRef.current?.stopAnimation?.()}
>
<MoreVertical ref={moreVerticalIconRef} className="h-4 w-4" />
<span className="sr-only">{t('common.threadDisplay.moreOptions')}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{isInInbox && (
<DropdownMenuItem onClick={() => moveThreadTo('spam')}>
<ArchiveX className="mr-2 h-4 w-4" /> {t('common.threadDisplay.moveToSpam')}
</DropdownMenuItem>
)}
{isInSpam && (
<DropdownMenuItem onClick={() => moveThreadTo('inbox')}>
<Inbox className="mr-2 h-4 w-4" /> {t('common.mail.moveToInbox')}
</DropdownMenuItem>
)}
{isInArchive && (
<DropdownMenuItem onClick={() => moveThreadTo('inbox')}>
<Inbox className="mr-2 h-4 w-4" /> {t('common.mail.moveToInbox')}
</DropdownMenuItem>
)}
<DropdownMenuItem>
<ReplyAll className="mr-2 h-4 w-4" /> {t('common.threadDisplay.replyAll')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleForward}>
<Forward className="mr-2 h-4 w-4" /> {t('common.threadDisplay.forward')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleMarkAsUnread}>
<Mail className="mr-2 h-4 w-4" /> {t('common.mail.markAsUnread')}
</DropdownMenuItem>
<DropdownMenuItem>{t('common.threadDisplay.addLabel')}</DropdownMenuItem>
<DropdownMenuItem>{t('common.threadDisplay.muteThread')}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu> */}
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 flex-col">
<ScrollArea className="h-full flex-1" type="auto">
<div className="pb-4">
{(emailData || []).map((message, index) => (
@@ -531,18 +416,13 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
))}
</div>
</ScrollArea>
<div className={`relative ${isFullscreen ? '' : 'top-1'} flex-shrink-0`}>
<div className={cn(
'relative z-10 bg-offsetLight dark:bg-offsetDark',
isFullscreen ? 'mb-2' : ''
)}>
<ReplyCompose
emailData={emailData}
isOpen={isReplyOpen || isForwardOpen}
setIsOpen={(open) => {
if (isForwardOpen) {
setIsForwardOpen(open);
} else {
setIsReplyOpen(open);
}
}}
mode={isForwardOpen ? 'forward' : 'reply'}
mode={mail.forwardComposerOpen ? 'forward' : 'reply'}
/>
</div>
</div>

View File

@@ -5,11 +5,15 @@ import { type Mail } from "@/components/mail/data";
type Config = {
selected: Mail["id"] | null;
bulkSelected: Mail["id"][];
replyComposerOpen: boolean;
forwardComposerOpen: boolean;
};
const configAtom = atom<Config>({
selected: null,
bulkSelected: [],
replyComposerOpen: false,
forwardComposerOpen: false,
});
export function useMail() {

View File

@@ -153,7 +153,7 @@ export const navigationConfig: Record<string, NavConfig> = {
},
{
title: 'navigation.settings.security',
url: '#',
url: '/settings/security',
icon: ShieldCheckIcon,
disabled: true,
},
@@ -169,7 +169,7 @@ export const navigationConfig: Record<string, NavConfig> = {
},
{
title: 'navigation.settings.shortcuts',
url: '#',
url: '/settings/shortcuts',
icon: KeyboardIcon,
disabled: true,
},

View File

@@ -16,6 +16,7 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa
const [quickActionIndex, setQuickActionIndex] = useState(0);
const hoveredMailRef = useRef<string | null>(null);
const keyboardActiveRef = useRef(false);
const lastMoveTime = useRef(0);
useEffect(() => {
if (!keyboardActiveRef.current) {
@@ -186,6 +187,64 @@ export function useMailNavigation({ items, containerRef, onNavigate }: UseMailNa
}
}, []);
useEffect(() => {
let isProcessingKey = false;
const MOVE_DELAY = 100; // Decreased from 150ms to 100ms for faster movement
const handleKeyDown = async (event: KeyboardEvent) => {
if (isQuickActionMode) return;
// For non-repeat events (initial press), let the useHotKey handlers manage it
if (!event.repeat) return;
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault(); // Prevent default browser behavior
// If we're already processing a previous key event, don't stack them
if (isProcessingKey) return;
// Check if enough time has passed since the last movement
const now = Date.now();
if (now - lastMoveTime.current < MOVE_DELAY) return;
isProcessingKey = true;
lastMoveTime.current = now;
await new Promise<void>(resolve => {
requestAnimationFrame(() => {
if (event.key === 'ArrowUp') {
setFocusedIndex(prev => {
const newIndex = prev === null ? items.length - 1 : Math.max(0, prev - 1);
const threadElement = getThreadElement(newIndex);
if (threadElement && containerRef.current) {
threadElement.scrollIntoView({ block: 'nearest', behavior: 'auto' });
}
return newIndex;
});
} else if (event.key === 'ArrowDown') {
setFocusedIndex(prev => {
const newIndex = prev === null ? 0 : Math.min(items.length - 1, prev + 1);
const threadElement = getThreadElement(newIndex);
if (threadElement && containerRef.current) {
threadElement.scrollIntoView({ block: 'nearest', behavior: 'auto' });
}
return newIndex;
});
}
isProcessingKey = false;
resolve();
});
});
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [handleArrowUp, handleArrowDown, isQuickActionMode, items.length, getThreadElement, containerRef]);
return {
focusedIndex,
isQuickActionMode,

View File

@@ -1,18 +1,36 @@
{
"common": {
"actions": {
"logout": "Logout",
"back": "Back",
"create": "Create Email",
"saveChanges": "Save changes",
"saving": "Saving...",
"resetToDefaults": "Reset to Defaults",
"close": "Close",
"signingOut": "Signing out...",
"signedOutSuccess": "Signed out successfully!",
"logout": "خروج",
"back": "رجوع",
"create": "إنشاء بريد إلكتروني",
"saveChanges": "حفظ التغييرات",
"saving": "جاري الحفظ...",
"resetToDefaults": "إعادة التعيين إلى الوضع الافتراضي",
"close": "غلق",
"signingOut": "تسجيل الخروج...",
"signedOutSuccess": "تم تسجيل الخروج بنجاح!",
"signOutError": "Error signing out",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Dark",
@@ -25,9 +43,9 @@
"placeholder": "Type a command or search...",
"noResults": "No results found",
"groups": {
"mail": "Mail",
"settings": "Settings",
"actions": "Actions",
"mail": "البريد",
"settings": "الإعدادات",
"actions": "الإجراءات",
"help": "Help",
"navigation": "Navigation"
},
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Primary",
"allMail": "All Mail",
"important": "Important",
"personal": "Personal",
"updates": "Updates",
@@ -219,7 +238,8 @@
"moveToTrash": "Move to Trash",
"markAsUnread": "Mark as Unread",
"markAsRead": "Mark as Read",
"addStar": "Add Star",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Mute Thread",
"moving": "Moving...",
"moved": "Moved",
@@ -242,7 +262,6 @@
"archive": "Archive",
"bin": "Bin",
"feedback": "Feedback",
"contact": "Contact",
"settings": "Settings"
},
"settings": {

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "S'ha tancat la sessió amb èxit!",
"signOutError": "Error tancant la sessió",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Fosc",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Primari",
"allMail": "All Mail",
"important": "Important",
"personal": "Personal",
"updates": "Actualitzacions",
@@ -219,7 +238,8 @@
"moveToTrash": "Move to Trash",
"markAsUnread": "Mark as Unread",
"markAsRead": "Mark as Read",
"addStar": "Add Star",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Mute Thread",
"moving": "Moving...",
"moved": "Moved",
@@ -242,7 +262,6 @@
"archive": "Arxivats",
"bin": "Paperera",
"feedback": "Feedback",
"contact": "Contact",
"settings": "Configuració"
},
"settings": {

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "Odhlášení proběhlo úspěšně!",
"signOutError": "Chyba při odhlášení",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Tmavé",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Hlavní",
"allMail": "All Mail",
"important": "Důležité",
"personal": "Osobní",
"updates": "Aktualizace",
@@ -219,7 +238,8 @@
"moveToTrash": "Přesunout do Odstraněné pošty",
"markAsUnread": "Označit jako nepřečtené",
"markAsRead": "Označit jako přečtené",
"addStar": "Přidat hvězdu",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Ztlumit vlákno",
"moving": "Přesouvání...",
"moved": "Přesunuto",
@@ -242,7 +262,6 @@
"archive": "Archiv",
"bin": "Odstraněná pošta",
"feedback": "Zpětná vazba",
"contact": "Kontakt",
"settings": "Nastavení"
},
"settings": {

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "Erfolgreich abgemeldet!",
"signOutError": "Fehler beim Abmelden",
"refresh": "Aktualisieren",
"loading": "Wird geladen..."
"loading": "Wird geladen...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Dunkel",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Primär",
"allMail": "All Mail",
"important": "Wichtig",
"personal": "Persönlich",
"updates": "Updates",
@@ -219,7 +238,8 @@
"moveToTrash": "In den Papierkorb verschieben",
"markAsUnread": "Als ungelesen markieren",
"markAsRead": "Als gelesen markieren",
"addStar": "Stern hinzufügen",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Thread stummschalten",
"moving": "Wird verschoben...",
"moved": "Verschoben",
@@ -242,7 +262,6 @@
"archive": "Archiv",
"bin": "Papierkorb",
"feedback": "Rückmeldung",
"contact": "Kontakt",
"settings": "Einstellungen"
},
"settings": {

View File

@@ -26,6 +26,8 @@
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "Sesión cerrada correctamente",
"signOutError": "Error al cerrar sesión",
"refresh": "Actualizar",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Oscuro",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Principal",
"allMail": "All Mail",
"important": "Importante",
"personal": "Personal",
"updates": "Actualizaciones",
@@ -219,7 +238,8 @@
"moveToTrash": "Mover a la papelera",
"markAsUnread": "Marcar como no leído",
"markAsRead": "Marcar como leído",
"addStar": "Destacar",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Silencia hilo",
"moving": "Moviendo...",
"moved": "Movido",
@@ -242,7 +262,6 @@
"archive": "Archivar",
"bin": "Papelera de reciclaje",
"feedback": "Sugerencias",
"contact": "Contacto",
"settings": "Configuración"
},
"settings": {

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "Déconnecté avec succès !",
"signOutError": "Erreur de déconnexion",
"refresh": "Actualiser",
"loading": "Loading..."
"loading": "Chargement...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Sombre",
@@ -67,12 +85,12 @@
"starred": "Étoilé",
"applyFilters": "Appliquer les filtres",
"reset": "Réinitialiser",
"searching": "Searching...",
"aiSuggestions": "AI Suggestions",
"aiSearching": "AI is searching...",
"aiSearchError": "AI search failed. Please try again.",
"aiNoResults": "No AI suggestions found",
"aiEnhancedQuery": "Enhanced search query"
"searching": "Recherche en cours...",
"aiSuggestions": "Suggestions de l'IA",
"aiSearching": "L'IA est en cours de recherche...",
"aiSearchError": "La recherche de l'IA a échouée. Veuillez réessayer.",
"aiNoResults": "Aucune suggestion de l'IA trouvée",
"aiEnhancedQuery": "Requête de recherche avancée"
},
"navUser": {
"customerSupport": "Support aux clients",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Principale",
"allMail": "All Mail",
"important": "Important",
"personal": "Personnel",
"updates": "Mises à jour",
@@ -91,14 +110,14 @@
},
"replyCompose": {
"replyTo": "Répondre à",
"thisEmail": "cet email",
"thisEmail": "ce courriel",
"dropFiles": "Déposer les fichiers à joindre",
"attachments": "Pièces jointes",
"attachmentCount": "{count, plural, =0 {pièces jointes} one {pièce jointe} other {pièces jointes}}",
"fileCount": "{count, plural, =0 {fichiers} one {fichier} other {fichiers}}",
"saveDraft": "Enregistrer le brouillon",
"send": "Envoyer",
"forward": "Forward"
"forward": "Transférer"
},
"mailDisplay": {
"details": "Détails",
@@ -106,11 +125,11 @@
"to": "À",
"cc": "Cc",
"date": "Date",
"mailedBy": "Expédié par",
"mailedBy": "Envoyé par",
"signedBy": "Signé par",
"security": "Sécurité",
"standardEncryption": "Chiffrement standard (TLS)",
"loadingMailContent": "Chargement du contenu de l'email...",
"standardEncryption": "Encryptage standard (TLS)",
"loadingMailContent": "Chargement du contenu du courriel...",
"unsubscribe": "Se désabonner",
"unsubscribed": "Désabonné",
"unsubscribeDescription": "Êtes-vous sûr de vouloir vous désabonner de cette liste de diffusion ?",
@@ -125,14 +144,14 @@
"archive": "Archiver",
"reply": "Répondre",
"moreOptions": "Plus d'options",
"moveToSpam": "Move to Spam",
"moveToSpam": "Déplacer dans les indésirables",
"replyAll": "Répondre à tous",
"forward": "Transférer",
"markAsUnread": "Marquer comme non lu",
"markAsRead": "Mark as Read",
"addLabel": "Ajouter un label",
"markAsRead": "Marquer comme lu",
"addLabel": "Ajouter une étiquette",
"muteThread": "Mettre en sourdine",
"favourites": "Favourites"
"favourites": "Favoris"
},
"notes": {
"title": "Notes",
@@ -195,10 +214,10 @@
}
},
"settings": {
"notFound": "Settings not found",
"notFound": "Paramètres introuvables",
"saved": "Paramètres enregistrés",
"failedToSave": "Failed to save settings",
"languageChanged": "Language changed to {locale}"
"failedToSave": "Échec de la sauvegarde des paramètres",
"languageChanged": "Langue changée en {locale}"
},
"mail": {
"replies": "{count, plural, =0 {réponses} one {# réponse} other {# réponses}}",
@@ -218,17 +237,18 @@
"archive": "Archiver",
"moveToTrash": "Déplacer dans la Corbeille",
"markAsUnread": "Marquer comme non lu",
"markAsRead": "Mark as Read",
"addStar": "Add Star",
"markAsRead": "Marquer comme lu",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Mettre en sourdine",
"moving": "Déplacement...",
"moved": "Déplacé",
"errorMoving": "Erreur lors du déplacement",
"reply": "Reply",
"reply": "Répondre",
"replyAll": "Répondre à tous",
"forward": "Transférer",
"labels": "Labels",
"createNewLabel": "Create New Label",
"labels": "Étiquettes",
"createNewLabel": "Créer une nouvelle étiquette",
"noLabelsAvailable": "Aucune étiquette disponible",
"loadMore": "Charger plus d'éléments"
}
@@ -242,7 +262,6 @@
"archive": "Archives",
"bin": "Corbeille",
"feedback": "Vos commentaires",
"contact": "Contact",
"settings": "Paramètres"
},
"settings": {
@@ -265,18 +284,18 @@
"settings": {
"general": {
"title": "Paramètres généraux",
"description": "Gérez les paramètres de votre langue et vos préférences d'affichage des courriels.",
"description": "Gérez les paramètres de langue et vos préférences d'affichage des courriels.",
"language": "Langue",
"selectLanguage": "Select a language",
"selectLanguage": "Sélectionnez la langue",
"timezone": "Fuseau horaire",
"selectTimezone": "Select a timezone",
"selectTimezone": "Sélectionner un fuseau horaire",
"dynamicContent": "Contenu dynamique",
"dynamicContentDescription": "Autoriser les courriels à afficher du contenu dynamique.",
"externalImages": "Afficher les images externes",
"externalImagesDescription": "Autoriser les courriels à afficher des images provenant de sources externes.",
"languageChangedTo": "Language changed to {locale}",
"customPrompt": "Custom AI Prompt",
"customPromptPlaceholder": "",
"languageChangedTo": "Langue changée en {locale}",
"customPrompt": "Requêtes IA personnalisées",
"customPromptPlaceholder": "Entrez votre requête personnalisée pour l'IA...",
"customPromptDescription": "Personnalisez la façon dont l'IA écrit vos réponses. Cela sera ajouté à la requête par défaut."
},
"connections": {
@@ -312,8 +331,8 @@
"title": "Raccourcis clavier",
"description": "Consultez et personnalisez les raccourcis clavier pour des actions rapides.",
"actions": {
"newEmail": "Nouvel email",
"sendEmail": "Envoyer l'email",
"newEmail": "Nouveau courriel",
"sendEmail": "Envoyer le courriel",
"reply": "Répondre",
"replyAll": "Répondre à tous",
"forward": "Transférer",

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "सफलतापूर्वक साइन आउट हो गया!",
"signOutError": "साइन आउट करने में एरर",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "काला",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "प्राइमरी",
"allMail": "All Mail",
"important": "महत्वपूर्ण",
"personal": "पर्सनल",
"updates": "अपडेट",
@@ -219,7 +238,8 @@
"moveToTrash": "Move to Trash",
"markAsUnread": "Mark as Unread",
"markAsRead": "Mark as Read",
"addStar": "Add Star",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Mute Thread",
"moving": "Moving...",
"moved": "Moved",
@@ -242,7 +262,6 @@
"archive": "आर्काइव",
"bin": "बिन",
"feedback": "Feedback",
"contact": "Contact",
"settings": "सेटिंग्स"
},
"settings": {

View File

@@ -11,8 +11,26 @@
"signingOut": "ログアウトしています...",
"signedOutSuccess": "正常にログアウトしました!",
"signOutError": "サインアウトエラー",
"refresh": "Refresh",
"loading": "Loading..."
"refresh": "更新",
"loading": "読み込み中...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "ダーク",
@@ -67,12 +85,12 @@
"starred": "スター付き",
"applyFilters": "フィルタを適用",
"reset": "リセット",
"searching": "Searching...",
"aiSuggestions": "AI Suggestions",
"aiSearching": "AI is searching...",
"aiSearchError": "AI search failed. Please try again.",
"aiNoResults": "No AI suggestions found",
"aiEnhancedQuery": "Enhanced search query"
"searching": "検索中...",
"aiSuggestions": "AIの提案",
"aiSearching": "AIが検索中...",
"aiSearchError": "AI検索に失敗しました。もう一度お試しください。",
"aiNoResults": "AIの提案が見つかりませんでした",
"aiEnhancedQuery": "拡張検索クエリ"
},
"navUser": {
"customerSupport": "カスタマーサポート",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "プライマリ",
"allMail": "All Mail",
"important": "重要",
"personal": "個人用",
"updates": "更新",
@@ -98,7 +117,7 @@
"fileCount": "{count, plural, =0 {files} other {files}}",
"saveDraft": "下書きを保存",
"send": "送信",
"forward": "Forward"
"forward": "転送"
},
"mailDisplay": {
"details": "詳細",
@@ -219,7 +238,8 @@
"moveToTrash": "ゴミ箱へ移動",
"markAsUnread": "未読にする",
"markAsRead": "既読にする",
"addStar": "スターを追加",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "スレッドをミュート",
"moving": "移動中...",
"moved": "移動しました",
@@ -230,7 +250,7 @@
"labels": "ラベル",
"createNewLabel": "新しいラベルを作成",
"noLabelsAvailable": "利用可能なラベルがありません",
"loadMore": "Load more"
"loadMore": "さらに読み込む"
}
},
"navigation": {
@@ -242,7 +262,6 @@
"archive": "アーカイブ",
"bin": "ごみ箱",
"feedback": "フィードバック",
"contact": "コンタクト",
"settings": "設定"
},
"settings": {
@@ -274,10 +293,10 @@
"dynamicContentDescription": "電子メールの動的なコンテンツの表示を許可します。",
"externalImages": "外部画像を表示する",
"externalImagesDescription": "電子メールによる外部ソースからの画像の表示を許可します。",
"languageChangedTo": "Language changed to {locale}",
"customPrompt": "Custom AI Prompt",
"customPromptPlaceholder": "Enter your custom prompt for the AI...",
"customPromptDescription": "Customize how the AI writes your email replies. This will be added to the base prompt."
"languageChangedTo": "言語を {locale}に変更しました",
"customPrompt": "カスタムAIプロンプト",
"customPromptPlaceholder": "AIのカスタムプロンプトを入力...",
"customPromptDescription": "AIがメールの返信を書く方法をカスタマイズします。これはベースプロンプトに追加されます。"
},
"connections": {
"title": "メールの接続",

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "로그아웃에 성공했습니다!",
"signOutError": "로그아웃 오류 발생",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "다크 모드",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "기본",
"allMail": "All Mail",
"important": "중요",
"personal": "개인",
"updates": "업데이트",
@@ -219,7 +238,8 @@
"moveToTrash": "휴지통으로 이동",
"markAsUnread": "읽지 않음으로 표시",
"markAsRead": "읽음으로 표시",
"addStar": "별표 표시",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "스레드 음소거",
"moving": "이동 중...",
"moved": "이동됨",
@@ -242,7 +262,6 @@
"archive": "보관함",
"bin": "휴지통",
"feedback": "피드백",
"contact": "Contact",
"settings": "설정"
},
"settings": {

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "Veiksmīgi izrakstījies!",
"signOutError": "Kļūda, izrakstoties no sistēmas",
"refresh": "Atjaunot",
"loading": "Ielādē..."
"loading": "Ielādē...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Tumšs",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Galvenā",
"allMail": "All Mail",
"important": "Svarīgi",
"personal": "Personīgi",
"updates": "Atjauninājumi",
@@ -219,7 +238,8 @@
"moveToTrash": "Pārvietot uz Miskasti",
"markAsUnread": "Atzīmēt kā nelasītu",
"markAsRead": "Atzīmēt kā lasītu",
"addStar": "Pievienot Zvaigzni",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Apklusināt pavedienu",
"moving": "Pārvieto...",
"moved": "Pārvietots",
@@ -242,7 +262,6 @@
"archive": "Arhīvs",
"bin": "Miskaste",
"feedback": "Atsauksmes",
"contact": "Kontakti",
"settings": "Iestatījumi"
},
"settings": {

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "Signed out successfully!",
"signOutError": "Error signing out",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Dark",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Primary",
"allMail": "All Mail",
"important": "Important",
"personal": "Personal",
"updates": "Updates",
@@ -219,7 +238,8 @@
"moveToTrash": "Move to Trash",
"markAsUnread": "Mark as Unread",
"markAsRead": "Mark as Read",
"addStar": "Add Star",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Mute Thread",
"moving": "Moving...",
"moved": "Moved",
@@ -242,7 +262,6 @@
"archive": "Archive",
"bin": "Bin",
"feedback": "Feedback",
"contact": "Contact",
"settings": "Settings"
},
"settings": {

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": "Sessão terminada com sucesso!",
"signOutError": "Erro ao sair",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Escuro",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Principal",
"allMail": "All Mail",
"important": "Importante",
"personal": "Pessoal",
"updates": "Atualizações",
@@ -219,7 +238,8 @@
"moveToTrash": "Mover para o Lixo",
"markAsUnread": "Marcar como não lido",
"markAsRead": "Mark as Read",
"addStar": "Adicionar Estrela",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Silenciar esta conversa",
"moving": "Movendo...",
"moved": "Movido",
@@ -242,7 +262,6 @@
"archive": "Arquivados",
"bin": "Lixeira",
"feedback": "Feedback",
"contact": "Contact",
"settings": "Configurações"
},
"settings": {

View File

@@ -11,8 +11,26 @@
"signingOut": "Выход...",
"signedOutSuccess": "Выход успешен",
"signOutError": "Ошибка при выходе",
"refresh": "Refresh",
"loading": "Loading..."
"refresh": "Обновить",
"loading": "Загрузка...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Тёмный",
@@ -21,7 +39,7 @@
},
"commandPalette": {
"title": "Команда",
"description": "Быстрый переход и действия для Mail-0",
"description": "Выход выполнен успешно",
"placeholder": "Введите команду или поиск...",
"noResults": "Нет результатов",
"groups": {
@@ -64,15 +82,15 @@
"allMail": "Все Письма",
"unread": "Непрочитанные",
"hasAttachment": "Есть вложения",
"starred": "Звездочечка",
"starred": "Отмеченные",
"applyFilters": "Применить фильтры",
"reset": "Сбросить",
"searching": "Searching...",
"aiSuggestions": "AI Suggestions",
"aiSearching": "AI is searching...",
"aiSearchError": "AI search failed. Please try again.",
"aiNoResults": "No AI suggestions found",
"aiEnhancedQuery": "Enhanced search query"
"searching": "Поиск...",
"aiSuggestions": "Предложения ИИ",
"aiSearching": "ИИ ищет...",
"aiSearchError": "Поиск ИИ не удался. Пожалуйста, попробуйте снова.",
"aiNoResults": "ИИ предложения не найдены",
"aiEnhancedQuery": "Расширенный поиск"
},
"navUser": {
"customerSupport": "Поддержка",
@@ -83,8 +101,9 @@
},
"mailCategories": {
"primary": "Основные",
"allMail": "All Mail",
"important": "Важные",
"personal": "Персональные",
"personal": "Личные",
"updates": "Обновления",
"promotions": "Промо",
"social": "Социальные"
@@ -94,11 +113,11 @@
"thisEmail": "это письмо",
"dropFiles": "Поместите файлы, чтобы прикрепить",
"attachments": "Приложения",
"attachmentCount": "{count, plural, =0 {attachments} one {attachment} other {attachments}}",
"fileCount": "{count, plural, =0 {files} one {file} other {files}}",
"attachmentCount": "{count, plural, =0 {вложений} one {вложение} few {вложения} many {вложений} other {вложений}}",
"fileCount": "{count, plural, =0 {файлов} one {файл} few {файла} many {файлов} other {файла}}",
"saveDraft": "Сохранить черновик",
"send": "Отправить",
"forward": "Forward"
"forward": "Переслать"
},
"mailDisplay": {
"details": "Подробнее",
@@ -125,14 +144,14 @@
"archive": "Архив",
"reply": "Ответить",
"moreOptions": "Больше опций",
"moveToSpam": "Move to Spam",
"moveToSpam": "Переместить в Спам",
"replyAll": "Ответить всем",
"forward": "Переслать",
"markAsUnread": "Mark as Unread",
"markAsRead": "Mark as Read",
"markAsUnread": "Отметить как непрочитанное",
"markAsRead": "Отметить как прочитанное",
"addLabel": "Добавить метку",
"muteThread": "Выключить поток",
"favourites": "Favourites"
"favourites": "Избранное"
},
"notes": {
"title": "Заметки",
@@ -195,10 +214,10 @@
}
},
"settings": {
"notFound": "Settings not found",
"saved": "Settings saved",
"failedToSave": "Failed to save settings",
"languageChanged": "Language changed to {locale}"
"notFound": "Настройки не найдены",
"saved": "Настройки сохранены",
"failedToSave": "Не удалось сохранить настройки",
"languageChanged": "Язык изменен на {locale}"
},
"mail": {
"replies": "{count, plural, =0 {ответы} one {# ответ} few {# ответа} many {# ответов} other {# ответы}}",
@@ -218,19 +237,20 @@
"archive": "Архивировать",
"moveToTrash": "Переместить в Корзину",
"markAsUnread": "Отметить как непрочитанное",
"markAsRead": "Mark as Read",
"addStar": "В избранное",
"markAsRead": "Отметить как прочитанное",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Заглушить цепочку",
"moving": "Перемещение...",
"moved": "Перемещено",
"errorMoving": "Ошибка при перемещении",
"reply": "Reply",
"replyAll": "Reply All",
"forward": "Forward",
"labels": "Labels",
"createNewLabel": "Create New Label",
"noLabelsAvailable": "No labels available",
"loadMore": "Load more"
"reply": "Ответить",
"replyAll": "Ответить всем",
"forward": "Переслать",
"labels": "Ярлыки",
"createNewLabel": "Создать новый ярлык",
"noLabelsAvailable": "Нет доступных ярлыков",
"loadMore": "Загрузить ещё"
}
},
"navigation": {
@@ -242,7 +262,6 @@
"archive": "Архив",
"bin": "Корзина",
"feedback": "Отзывы",
"contact": "Контакты",
"settings": "Настройки"
},
"settings": {
@@ -267,17 +286,17 @@
"title": "Общие",
"description": "Управление настройками языка и отображения электронной почты.",
"language": "Язык",
"selectLanguage": "Select a language",
"selectLanguage": "Выберите язык",
"timezone": "Часовой пояс",
"selectTimezone": "Select a timezone",
"selectTimezone": "Выбрать часовой пояс",
"dynamicContent": "Динамический контент",
"dynamicContentDescription": "Позволяет электронной почте отображать динамический контент.",
"externalImages": "Отображать внешние изображения",
"externalImagesDescription": "Позволяет электронной почте отображать изображения из внешних источников.",
"languageChangedTo": "Language changed to {locale}",
"customPrompt": "Custom AI Prompt",
"customPromptPlaceholder": "Enter your custom prompt for the AI...",
"customPromptDescription": "Customize how the AI writes your email replies. This will be added to the base prompt."
"languageChangedTo": "Язык изменен на {locale}",
"customPrompt": "Пользовательский ИИ Запрос",
"customPromptPlaceholder": "Введите ваш запрос для ИИ...",
"customPromptDescription": "Настройте, как ИИ будет писать ваши ответы на письма. Этот текст будет добавлен к основному запросу."
},
"connections": {
"title": "Подключения",

View File

@@ -12,7 +12,25 @@
"signedOutSuccess": ıkış yapıldı!",
"signOutError": ıkış yapılırken hata oluştu",
"refresh": "Refresh",
"loading": "Loading..."
"loading": "Loading...",
"featureNotImplemented": "This feature is not implemented yet",
"moving": "Moving...",
"movedToInbox": "Moved to inbox",
"movingToInbox": "Moving to inbox...",
"movedToSpam": "Moved to spam",
"movingToSpam": "Moving to spam...",
"archiving": "Archiving...",
"archived": "Archived",
"failedToMove": "Failed to move message",
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
"removedFromFavorites": "Removed from favorites",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
"markingAsRead": "Marking as read...",
"markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Koyu",
@@ -83,6 +101,7 @@
},
"mailCategories": {
"primary": "Birincil",
"allMail": "All Mail",
"important": "Önemli",
"personal": "Kişisel",
"updates": "Güncellemeler",
@@ -219,7 +238,8 @@
"moveToTrash": "Çöp Kutusuna Taşı",
"markAsUnread": "Okunmamış Olarak İşaretle",
"markAsRead": "Mark as Read",
"addStar": "Yıldız Ekle",
"addFavorite": "Favorite",
"removeFavorite": "Unfavorite",
"muteThread": "Konuyu Sessize Al",
"moving": "Taşınıyor...",
"moved": "Taşındı",
@@ -242,7 +262,6 @@
"archive": "Arşiv",
"bin": "Çöp",
"feedback": "Geribildirim",
"contact": "İletişim",
"settings": "Ayarlar"
},
"settings": {

View File

@@ -1,8 +1,15 @@
import { EU_COUNTRIES } from './constants/countries';
import { navigationConfig } from '@/config/navigation';
import { geolocation } from '@vercel/functions';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
const disabledRoutes = Object.values(navigationConfig)
.flatMap(section => section.sections)
.flatMap(group => group.items)
.filter(item => item.disabled && item.url !== '#')
.map(item => item.url);
export function middleware(request: NextRequest) {
const response = NextResponse.next();
const geo = geolocation(request);
@@ -17,6 +24,11 @@ export function middleware(request: NextRequest) {
response.headers.set('x-user-eu-region', 'true');
}
const pathname = request.nextUrl.pathname;
if (disabledRoutes.some(route => pathname.startsWith(route))) {
return NextResponse.redirect(new URL('/mail/inbox', request.url));
}
return response;
}