mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
refactor(collab): share the notes list and attachment upload between desktop and phone
useCollabNotesData holds the note list with its first load and the live note events, and the attachment upload both shells run after saving a note. Writing notes, their colours and categories stays in each shell, since the desktop panel keeps empty categories and colour batches the phone tab does not have. A failed load still empties the desktop list only.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { collabApi } from '../../api/client';
|
||||
import { addListener, removeListener } from '../../api/websocket';
|
||||
import type { useTranslation } from '../../i18n';
|
||||
import type { useToast } from '../shared/Toast';
|
||||
import type { CollabNoteData } from './collabModel';
|
||||
|
||||
export interface CollabNotesDataOptions {
|
||||
tripId: number;
|
||||
t: ReturnType<typeof useTranslation>['t'];
|
||||
toast: ReturnType<typeof useToast>;
|
||||
/** The desktop panel outlives a trip change, so a failed load there empties the list. */
|
||||
resetOnLoadError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The trip notes behind the desktop Collab panel and the phone's notes tab, each reading
|
||||
* the rows through its own note type: the list as the server has it, kept current by the
|
||||
* WebSocket note events, and the attachment upload both run after a note is saved. Writing
|
||||
* a note, its colours and its categories stays with each shell, which differ there. Collab
|
||||
* has no store slice, so the notes live here.
|
||||
*/
|
||||
export function useCollabNotesData<N extends { id: number } = CollabNoteData>({
|
||||
tripId,
|
||||
t,
|
||||
toast,
|
||||
resetOnLoadError = false,
|
||||
}: CollabNotesDataOptions) {
|
||||
const [notes, setNotes] = useState<N[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tripId) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
collabApi
|
||||
.getNotes(tripId)
|
||||
.then((data: N[] | { notes?: N[] } | null) => {
|
||||
if (!cancelled) setNotes(Array.isArray(data) ? data : data?.notes || []);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled && resetOnLoadError) setNotes([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [tripId, resetOnLoadError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tripId) return;
|
||||
const handler = (msg: Record<string, unknown>) => {
|
||||
// An event still in flight from a trip just left must not land in this list.
|
||||
if (String(msg?.tripId) !== String(tripId)) return;
|
||||
const note = msg.note as N | undefined;
|
||||
if (msg.type === 'collab:note:created' && note) {
|
||||
setNotes((prev) => (prev.some((n) => n.id === note.id) ? prev : [note, ...prev]));
|
||||
}
|
||||
if (msg.type === 'collab:note:updated' && note) {
|
||||
setNotes((prev) => prev.map((n) => (n.id === note.id ? { ...n, ...note } : n)));
|
||||
}
|
||||
if (msg.type === 'collab:note:deleted') {
|
||||
const deletedId = (msg.noteId || msg.id) as number | undefined;
|
||||
if (deletedId) setNotes((prev) => prev.filter((n) => n.id !== deletedId));
|
||||
}
|
||||
};
|
||||
addListener(handler);
|
||||
return () => removeListener(handler);
|
||||
}, [tripId]);
|
||||
|
||||
/**
|
||||
* Uploads a saved note's attachments one by one; a file the server refuses is toasted
|
||||
* and the rest still go. `onFileError` sees each refusal before the toast.
|
||||
*/
|
||||
const uploadNoteFiles = useCallback(
|
||||
async (noteId: number, files: File[], onFileError?: (err: unknown) => void) => {
|
||||
for (const file of files) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
try {
|
||||
await collabApi.uploadNoteFile(tripId, noteId, fd);
|
||||
} catch (err) {
|
||||
onFileError?.(err);
|
||||
toast.error(t('common.error'));
|
||||
}
|
||||
}
|
||||
},
|
||||
[tripId, toast, t]
|
||||
);
|
||||
|
||||
return { notes, setNotes, loading, uploadNoteFiles };
|
||||
}
|
||||
Reference in New Issue
Block a user