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
23 changes: 5 additions & 18 deletions apps/mail/actions/notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,6 @@ export type ActionResult<T> = {
error?: string;
};

export async function fetchNotes(): Promise<ActionResult<Note[]>> {
try {
const result = await notes.getNotes();
return { success: true, data: result };
} catch (error: any) {
console.error('Error fetching notes:', error);
return {
success: false,
error: error.message || 'Failed to fetch notes'
};
}
}

export async function fetchThreadNotes(threadId: string): Promise<ActionResult<Note[]>> {
try {
const result = await notes.getThreadNotes(threadId);
Expand Down Expand Up @@ -88,17 +75,17 @@ export async function deleteNote(noteId: string): Promise<ActionResult<boolean>>
}

export async function reorderNotes(
notesArray: { id: string; order: number; isPinned?: boolean }[]
notesArray: { id: string; order: number; isPinned?: boolean | null }[]
): Promise<ActionResult<boolean>> {
try {
if (!notesArray || notesArray.length === 0) {
console.warn('Attempted to reorder an empty array of notes');
return { success: true, data: true };
}
console.log(`Reordering ${notesArray.length} notes:`,

console.log(`Reordering ${notesArray.length} notes:`,
notesArray.map(({id, order, isPinned}) => ({id, order, isPinned})));

const result = await notes.reorderNotes(notesArray);
return { success: true, data: result };
} catch (error: any) {
Expand All @@ -108,4 +95,4 @@ export async function reorderNotes(
error: error.message || 'Failed to reorder notes'
};
}
}
}
14 changes: 2 additions & 12 deletions apps/mail/app/api/notes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@ async function getCurrentUserId(): Promise<string> {
}

export const notes = {
async getNotes(): Promise<Note[]> {
try {
const userId = await getCurrentUserId();
return await notesManager.getNotes(userId);
} catch (error) {
console.error('Error getting notes:', error);
throw error;
}
},

async getThreadNotes(threadId: string): Promise<Note[]> {
try {
const userId = await getCurrentUserId();
Expand Down Expand Up @@ -79,7 +69,7 @@ export const notes = {
},

async reorderNotes(
notesArray: { id: string; order: number; isPinned?: boolean }[]
notesArray: { id: string; order: number; isPinned?: boolean | null }[]
): Promise<boolean> {
try {
const userId = await getCurrentUserId();
Expand All @@ -91,4 +81,4 @@ export const notes = {
}
};

export * from './types';
export * from './types';
28 changes: 14 additions & 14 deletions apps/mail/app/api/notes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ export interface Note {

export interface NotesManager {
getNotes(userId: string): Promise<Note[]>;

getThreadNotes(userId: string, threadId: string): Promise<Note[]>;

createNote(
userId: string,
threadId: string,
content: string,
color?: string,
userId: string,
threadId: string,
content: string,
color?: string,
isPinned?: boolean
): Promise<Note>;

updateNote(
userId: string,
noteId: string,
userId: string,
noteId: string,
data: Partial<Omit<Note, 'id' | 'userId' | 'threadId' | 'createdAt' | 'updatedAt'>>
): Promise<Note>;

deleteNote(userId: string, noteId: string): Promise<boolean>;

reorderNotes(
userId: string,
notes: { id: string; order: number; isPinned?: boolean }[]
userId: string,
notes: { id: string; order: number; isPinned?: boolean | null }[]
): Promise<boolean>;
}
}
31 changes: 5 additions & 26 deletions apps/mail/components/mail/mail-list.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
'use client';

import {
type ComponentProps,
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { ConditionalThreadProps, InitialThread, MailListProps, MailSelectMode } from '@/types';
import { AlertTriangle, Bell, Briefcase, StickyNote, Tag, User, Users } from 'lucide-react';
import { type ComponentProps, memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { EmptyState, type FolderType } from '@/components/mail/empty-state';
import { preloadThread, useThreads } from '@/hooks/use-threads';
import { cn, defaultPageSize, formatDate } from '@/lib/utils';
import { useHotKey, useKeyState } from '@/hooks/use-hot-key';
import { useSearchValue } from '@/hooks/use-search-value';
import { markAsRead, markAsUnread } from '@/actions/mail';
import { useFormatter, useTranslations } from 'next-intl';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useMail } from '@/components/mail/use-mail';
import type { VirtuosoHandle } from 'react-virtuoso';
import { useSession } from '@/lib/auth-client';
import { Badge } from '@/components/ui/badge';
import { useNotes } from '@/hooks/use-notes';
import { useParams } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Virtuoso } from 'react-virtuoso';
import items from './demo.json';
import { toast } from 'sonner';
Expand Down Expand Up @@ -55,18 +46,11 @@ const highlightText = (text: string, highlight: string) => {
const Thread = memo(
({ message, selectMode, demo, onClick, sessionData }: ConditionalThreadProps) => {
const [mail] = useMail();
const { hasNotes } = demo ? { hasNotes: () => false } : useNotes();
const [searchValue] = useSearchValue();
const t = useTranslations();
const format = useFormatter();
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const isHovering = useRef<boolean>(false);
const hasPrefetched = useRef<boolean>(false);
const isKeyPressed = useKeyState();

const threadHasNotes = useMemo(() => {
return !demo && hasNotes(message.threadId ?? message.id);
}, [demo, hasNotes, message.threadId, message.id]);

const isMailSelected = useMemo(() => {
return message.id === mail.selected;
Expand All @@ -75,12 +59,8 @@ const Thread = memo(
const isMailBulkSelected = mail.bulkSelected.includes(message.id);

const threadLabels = useMemo(() => {
const labels = [...(message.tags || [])];
if (threadHasNotes) {
labels.push('notes');
}
return labels;
}, [message.tags, threadHasNotes]);
return [...(message.tags || [])];
}, [message.tags]);

const handleMouseEnter = () => {
if (demo) return;
Expand Down Expand Up @@ -559,8 +539,7 @@ const MailLabels = memo(
MailLabels.displayName = 'MailLabels';

function getNormalizedLabelKey(label: string) {
const normalizedLabel = label.toLowerCase().replace(/^category_/i, '');
return normalizedLabel;
return label.toLowerCase().replace(/^category_/i, '');
}

function capitalize(str: string) {
Expand Down
97 changes: 45 additions & 52 deletions apps/mail/components/mail/note-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { createNote, deleteNote, reorderNotes, updateNote } from '@/actions/notes';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useState, useRef, useEffect, useMemo } from 'react';
import { useTranslations, useFormatter } from 'next-intl';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Textarea } from '@/components/ui/textarea';
import { useThreadNotes } from '@/hooks/use-notes';
import type { Note } from '@/app/api/notes/types';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
Expand All @@ -79,13 +81,6 @@ import { toast } from 'sonner';

interface NotesPanelProps {
threadId: string;
notes: Note[];
onAddNote: (threadId: string, content: string, color?: string) => Promise<Note | null>;
onEditNote: (noteId: string, content: string) => Promise<Note | null>;
onDeleteNote: (noteId: string) => Promise<boolean>;
onTogglePin?: (noteId: string) => Promise<Note | null>;
onChangeColor?: (noteId: string, color: string) => Promise<Note | null>;
onReorderNotes?: (notes: { id: string; order: number; isPinned?: boolean }[]) => Promise<boolean>;
}

function SortableNote({
Expand Down Expand Up @@ -247,16 +242,8 @@ function SortableNote({
);
}

export function NotesPanel({
threadId,
notes = [],
onAddNote,
onEditNote,
onDeleteNote,
onTogglePin,
onChangeColor,
onReorderNotes,
}: NotesPanelProps) {
export function NotesPanel({ threadId }: NotesPanelProps) {
const { data: notes, mutate } = useThreadNotes(threadId);
const [isOpen, setIsOpen] = useState(false);
const [editingNoteId, setEditingNoteId] = useState<string | null>(null);
const [newNoteContent, setNewNoteContent] = useState('');
Expand Down Expand Up @@ -300,12 +287,19 @@ export function NotesPanel({

const handleAddNote = async () => {
if (newNoteContent.trim()) {
await onAddNote(
threadId,
newNoteContent,
selectedColor !== 'default' ? selectedColor : undefined,
setIsAddingNewNote(true);
toast.promise(
createNote({
threadId,
color: selectedColor !== 'default' ? selectedColor : undefined,
content: newNoteContent,
}),
{
success: t('common.notes.noteUpdated'),
loading: 'loading',
},
);
toast.success(t('common.notes.noteUpdated'));
await mutate();
setNewNoteContent('');
setIsAddingNewNote(false);
setSelectedColor('default');
Expand All @@ -325,8 +319,16 @@ export function NotesPanel({

const handleEditNote = async () => {
if (editingNoteId && editContent.trim()) {
await onEditNote(editingNoteId, editContent);
toast.success(t('common.notes.noteUpdated'));
toast.promise(
updateNote(editingNoteId, {
content: editContent.trim(),
}),
{
success: t('common.notes.noteUpdated'),
loading: 'loading',
},
);
await mutate();
setEditingNoteId(null);
setEditContent('');
}
Expand All @@ -344,7 +346,11 @@ export function NotesPanel({

const handleDeleteNote = async () => {
if (noteToDelete) {
await onDeleteNote(noteToDelete);
toast.promise(deleteNote(noteToDelete), {
success: t('common.notes.noteDeleted'),
loading: 'loading',
});
await mutate();
toast.success(t('common.notes.noteDeleted'));
setDeleteConfirmOpen(false);
setNoteToDelete(null);
Expand All @@ -356,23 +362,25 @@ export function NotesPanel({
toast.success(t('common.notes.noteCopied'));
};

const togglePinNote = (noteId: string, isPinned: boolean) => {
if (onTogglePin) {
onTogglePin(noteId);
}
const togglePinNote = async (noteId: string, isPinned: boolean) => {
await updateNote(noteId, {
isPinned: !isPinned,
});
return await mutate();
};

const handleChangeNoteColor = (noteId: string, color: string) => {
if (onChangeColor) {
onChangeColor(noteId, color);
}
const handleChangeNoteColor = async (noteId: string, color: string) => {
await updateNote(noteId, {
color,
});
return await mutate();
};

const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};

const handleDragEnd = (event: DragEndEvent) => {
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;

if (over && active.id !== over.id) {
Expand All @@ -395,15 +403,7 @@ export function NotesPanel({
const reorderedPinnedNotes = assignOrdersAfterPinnedReorder(newPinnedNotes);

const newNotes = [...reorderedPinnedNotes, ...unpinnedNotes];
if (onReorderNotes) {
onReorderNotes(
newNotes.map((n) => ({
id: n.id,
order: n.order,
isPinned: n.isPinned ? true : false,
})),
);
}
await reorderNotes(newNotes);
} else {
const oldIndex = unpinnedNotes.findIndex((n) => n.id === active.id);
const newIndex = unpinnedNotes.findIndex((n) => n.id === over.id);
Expand All @@ -415,16 +415,9 @@ export function NotesPanel({
);

const newNotes = [...pinnedNotes, ...reorderedUnpinnedNotes];
if (onReorderNotes) {
onReorderNotes(
newNotes.map((n) => ({
id: n.id,
order: n.order,
isPinned: n.isPinned ? true : false,
})),
);
}
await reorderNotes(newNotes);
}
await mutate();
}

setActiveId(null);
Expand Down
Loading