import React, { useState, useEffect, useMemo } from 'react'; import { Task } from '../../entities/Task'; import { Project } from '../../entities/Project'; import { Note } from '../../entities/Note'; import { loadInboxItemsToStore, processInboxItemWithStore, deleteInboxItemWithStore, updateInboxItemWithStore, } from '../../utils/inboxService'; import InboxItemDetail from './InboxItemDetail'; import { useToast } from '../Shared/ToastContext'; import { useTranslation } from 'react-i18next'; import { InboxIcon } from '@heroicons/react/24/outline'; import LoadingScreen from '../Shared/LoadingScreen'; import TaskModal from '../Task/TaskModal'; import ProjectModal from '../Project/ProjectModal'; import NoteModal from '../Note/NoteModal'; import InboxModal from './InboxModal'; import { createTask } from '../../utils/tasksService'; import { createProject } from '../../utils/projectsService'; import { createNote } from '../../utils/notesService'; import { isUrl } from '../../utils/urlService'; import { fetchAreas } from '../../utils/areasService'; import { fetchProjects } from '../../utils/projectsService'; import { useStore } from '../../store/useStore'; const InboxItems: React.FC = () => { const { t } = useTranslation(); const { showSuccessToast, showErrorToast } = useToast(); // Access store data const { inboxItems, isLoading } = useStore((state) => state.inboxStore); const { areas, setAreas, setError: setAreasError, } = useStore((state) => state.areasStore); const { loadTags, hasLoaded: tagsHasLoaded, isLoading: tagsLoading, } = useStore((state) => state.tagsStore); // Local projects state (keep main's approach) const [projects, setProjects] = useState([]); // Modal states const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isNoteModalOpen, setIsNoteModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isInfoExpanded, setIsInfoExpanded] = useState(false); // Data for modals const [taskToEdit, setTaskToEdit] = useState(null); const [projectToEdit, setProjectToEdit] = useState(null); const [noteToEdit, setNoteToEdit] = useState(null); // Track the current inbox item ID being converted (for task/project/note conversion) const [currentConversionItemId, setCurrentConversionItemId] = useState< number | null >(null); // Track the current inbox item being edited const [itemToEdit, setItemToEdit] = useState(null); // Create stable default task object to prevent infinite re-renders const defaultTask = useMemo( () => ({ name: '', status: 'not_started' as const, priority: 'low' as const, completed_at: null, }), [] ); useEffect(() => { // Initial data loading loadInboxItemsToStore(true); // Load projects initially const loadInitialProjects = async () => { try { const projectData = await fetchProjects(); setProjects(Array.isArray(projectData) ? projectData : []); } catch (error) { console.error('Failed to load initial projects:', error); setProjects([]); } }; loadInitialProjects(); // Load areas initially const loadInitialAreas = async () => { try { const areasData = await fetchAreas(); setAreas(areasData); } catch (error) { console.error('Failed to load initial areas:', error); setAreasError(true); } }; loadInitialAreas(); // Load tags initially const loadInitialTags = async () => { if (!tagsHasLoaded && !tagsLoading) { try { await loadTags(); } catch (error) { console.error('Failed to load initial tags:', error); } } }; loadInitialTags(); // Set up an event listener for force reload const handleForceReload = () => { // Wait a short time to ensure the backend has processed the new item setTimeout(() => { loadInboxItemsToStore(false); // Don't show loading state during forced reload }, 500); }; // Handler for the inboxItemsUpdated custom event const handleInboxItemsUpdated = ( event: CustomEvent<{ count: number; firstItemContent: string }> ) => { // Show toast notifications for new items if (event.detail.count > 0) { // Show notification for the first new item showSuccessToast( t( 'inbox.newTelegramItem', 'New item from Telegram: {{content}}', { content: event.detail.firstItemContent, } ) ); // If multiple new items, show a summary notification as well if (event.detail.count > 1) { showSuccessToast( t( 'inbox.multipleNewItems', '{{count}} more new items added', { count: event.detail.count - 1, } ) ); } } }; // Set up polling for new inbox items (especially from Telegram) // This ensures real-time updates when items are added externally // Use a reasonable interval that balances responsiveness with performance const pollInterval = setInterval(() => { loadInboxItemsToStore(false); // Don't show loading state during polling }, 15000); // Check for new items every 15 seconds // Add event listeners window.addEventListener('forceInboxReload', handleForceReload); window.addEventListener( 'inboxItemsUpdated', handleInboxItemsUpdated as EventListener ); return () => { clearInterval(pollInterval); window.removeEventListener('forceInboxReload', handleForceReload); window.removeEventListener( 'inboxItemsUpdated', handleInboxItemsUpdated as EventListener ); }; }, [t, showSuccessToast]); // Include dependencies that are actually used const handleProcessItem = async (id: number, showToast: boolean = true) => { try { await processInboxItemWithStore(id); if (showToast) { showSuccessToast(t('inbox.itemProcessed')); } } catch (error) { console.error('Failed to process inbox item:', error); showErrorToast(t('inbox.processError')); } }; const handleUpdateItem = async (id: number): Promise => { // When edit button is clicked, we open the InboxModal instead of doing inline editing setItemToEdit(id); setIsEditModalOpen(true); }; const handleSaveEditedItem = async (text: string) => { try { if (itemToEdit !== null) { await updateInboxItemWithStore(itemToEdit, text); showSuccessToast(t('inbox.itemUpdated')); } setIsEditModalOpen(false); setItemToEdit(null); } catch (error) { console.error('Failed to update inbox item:', error); showErrorToast(t('inbox.updateError')); } }; const handleDeleteItem = async (id: number) => { try { await deleteInboxItemWithStore(id); showSuccessToast(t('inbox.itemDeleted')); } catch (error) { console.error('Failed to delete inbox item:', error); showErrorToast(t('inbox.deleteError')); } }; // Modal handlers const handleOpenTaskModal = async (task: Task, inboxItemId?: number) => { try { // Load projects first before opening the modal try { const projectData = await fetchProjects(); // Make sure we always set an array setProjects(Array.isArray(projectData) ? projectData : []); } catch (error) { console.error('Failed to load projects:', error); showErrorToast( t('project.loadError', 'Failed to load projects') ); setProjects([]); // Ensure we have an empty array even on error } setTaskToEdit(task); if (inboxItemId) { setCurrentConversionItemId(inboxItemId); } setIsTaskModalOpen(true); } catch (error) { console.error('Failed to open task modal:', error); } }; const handleOpenProjectModal = async ( project: Project | null, inboxItemId?: number ) => { try { // Load areas first before opening the modal (similar to task modal) try { const areasData = await fetchAreas(); setAreas(areasData); } catch (error) { console.error('Failed to load areas:', error); showErrorToast(t('area.loadError', 'Failed to load areas')); setAreas([]); // Ensure we have an empty array even on error } setProjectToEdit(project); if (inboxItemId) { setCurrentConversionItemId(inboxItemId); } setIsProjectModalOpen(true); } catch (error) { console.error('Failed to open project modal:', error); } }; const handleOpenNoteModal = async ( note: Note | null, inboxItemId?: number ) => { // Set up the note data first if (note && note.content && isUrl(note.content.trim())) { if (!note.tags) { note.tags = [{ name: 'bookmark' }]; } else if (!note.tags.some((tag) => tag.name === 'bookmark')) { note.tags.push({ name: 'bookmark' }); } } setNoteToEdit(note); if (inboxItemId) { setCurrentConversionItemId(inboxItemId); } // Projects are already available from the store setIsNoteModalOpen(true); }; const handleSaveTask = async (task: Task) => { try { const createdTask = await createTask(task); const taskLink = ( {t('task.created', 'Task')}{' '} {createdTask.name} {' '} {t('task.createdSuccessfully', 'created successfully!')} ); showSuccessToast(taskLink); // Process the inbox item after successful task creation if (currentConversionItemId !== null) { await handleProcessItem(currentConversionItemId, false); setCurrentConversionItemId(null); } setIsTaskModalOpen(false); } catch (error) { console.error('Failed to create task:', error); showErrorToast(t('task.createError')); } }; const handleSaveProject = async (project: Project) => { try { await createProject(project); showSuccessToast(t('project.createSuccess')); // Process the inbox item after successful project creation if (currentConversionItemId !== null) { await handleProcessItem(currentConversionItemId, false); setCurrentConversionItemId(null); } setIsProjectModalOpen(false); } catch (error) { console.error('Failed to create project:', error); showErrorToast(t('project.createError')); } }; const handleSaveNote = async (note: Note) => { try { // Check if the content appears to be a URL and add the bookmark tag const noteContent = note.content || ''; const isBookmarkContent = isUrl(noteContent.trim()); // Ensure tags property exists if (!note.tags) { note.tags = []; } // Add a bookmark tag if content is a URL and doesn't already have the tag if ( isBookmarkContent && !note.tags.some((tag) => tag.name === 'bookmark') ) { // Use spread operator to create a new array with the bookmark tag added note.tags = [...note.tags, { name: 'bookmark' }]; } // Create the note with proper tags await createNote(note); showSuccessToast( t('note.createSuccess', 'Note created successfully') ); // Process the inbox item after successful note creation if (currentConversionItemId !== null) { await handleProcessItem(currentConversionItemId, false); setCurrentConversionItemId(null); } setIsNoteModalOpen(false); } catch (error) { console.error('Failed to create note:', error); showErrorToast(t('note.createError', 'Failed to create note')); } }; const handleCreateProject = async (name: string): Promise => { try { const project = await createProject({ name, active: true }); showSuccessToast(t('project.createSuccess')); return project; } catch (error) { console.error('Failed to create project:', error); showErrorToast(t('project.createError')); throw error; } }; if (isLoading) { return ; } return (
{/* Title row with info button on the right */}

{t('inbox.title')}

{/* Info section below title row */}
{/* Large low-opacity info icon */}

{t( 'taskViews.inbox', "Inbox is where all uncategorized tasks are located. Tasks that have not been assigned to a project or don't have a due date will appear here. This is your 'brain dump' area where you can quickly note down tasks and organize them later." )}

{inboxItems.length === 0 ? (

{t('inbox.empty')}

{t('inbox.emptyDescription')}

) : (
{inboxItems.map((item) => ( ))}
)} {/* Task Modal - Always render it but control visibility with isOpen */} {/* Add error boundary protection for modal rendering */} {(() => { try { return ( { setIsTaskModalOpen(false); setTaskToEdit(null); }} task={taskToEdit || defaultTask} onSave={handleSaveTask} onDelete={async () => {}} // No need to delete since it's a new task projects={ Array.isArray(projects) ? projects : [] } onCreateProject={handleCreateProject} showToast={false} /> ); } catch (error) { console.error('TaskModal rendering error:', error); return null; } })()} {/* Project Modal - Only render when needed to prevent infinite loops */} {(() => { return ( isProjectModalOpen && (() => { try { return ( { setIsProjectModalOpen(false); setProjectToEdit(null); }} onSave={handleSaveProject} project={projectToEdit || undefined} areas={ Array.isArray(areas) ? areas : [] } /> ); } catch (error) { console.error( 'ProjectModal rendering error:', error ); return null; } })() ); })()} {/* Note Modal - Always render it but control visibility with isOpen */} {(() => { try { return ( { setIsNoteModalOpen(false); setNoteToEdit(null); }} onSave={handleSaveNote} note={noteToEdit} projects={ Array.isArray(projects) ? projects : [] } onCreateProject={handleCreateProject} /> ); } catch (error) { console.error('NoteModal rendering error:', error); return null; } })()} {/* Edit Inbox Item Modal */} {isEditModalOpen && itemToEdit !== null && ( { setIsEditModalOpen(false); setItemToEdit(null); }} onSave={handleSaveTask} onSaveNote={handleSaveNote} initialText={ inboxItems.find((item) => item.id === itemToEdit) ?.content || '' } editMode={true} onEdit={handleSaveEditedItem} onConvertToTask={async () => { if (itemToEdit !== null) { await handleProcessItem(itemToEdit); } }} onConvertToNote={async () => { if (itemToEdit !== null) { await handleProcessItem(itemToEdit); } }} projects={projects} /> )}
); }; export default InboxItems;