import { allViewTypes, ViewModeProps, ViewTypeOptions } from "./interface"; import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks"; import FNote from "../../entities/fnote"; import "./NoteList.css"; import { ListView, GridView } from "./legacy/ListOrGridView"; import { useEffect, useRef, useState } from "preact/hooks"; import GeoView from "./geomap"; import ViewModeStorage from "./view_mode_storage"; import CalendarView from "./calendar"; import TableView from "./table"; import BoardView from "./board"; import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws"; import { WebSocketMessage } from "@triliumnext/commons"; import froca from "../../services/froca"; import PresentationView from "./presentation"; interface NoteListProps { note: FNote | null | undefined; notePath: string | null | undefined; highlightedTokens?: string[] | null; /** if set to `true` then only collection-type views are displayed such as geo-map and the calendar. The original book types grid and list will be ignored. */ displayOnlyCollections?: boolean; isEnabled: boolean; ntxId: string | null | undefined; } export default function NoteList(props: Pick) { const { note, noteContext, notePath, ntxId } = useNoteContext(); const isEnabled = noteContext?.hasNoteList(); return } export function SearchNoteList(props: Omit) { return } export function CustomNoteList({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId }: NoteListProps) { const widgetRef = useRef(null); const viewType = useNoteViewType(note); const noteIds = useNoteIds(note, viewType, ntxId); const isFullHeight = (viewType && viewType !== "list" && viewType !== "grid"); const [ isIntersecting, setIsIntersecting ] = useState(false); const shouldRender = (isFullHeight || isIntersecting || note?.type === "book"); const isEnabled = (note && shouldEnable && !!viewType && shouldRender); useEffect(() => { if (isFullHeight || displayOnlyCollections || note?.type === "book") { // Double role: no need to check if the note list is visible if the view is full-height or book, but also prevent legacy views if `displayOnlyCollections` is true. return; } const observer = new IntersectionObserver( (entries) => { if (!isIntersecting) { setIsIntersecting(entries[0].isIntersecting); observer.disconnect(); } }, { rootMargin: "50px", threshold: 0.1 } ); // there seems to be a race condition on Firefox which triggers the observer only before the widget is visible // (intersection is false). https://github.com/zadam/trilium/issues/4165 setTimeout(() => widgetRef.current && observer.observe(widgetRef.current), 10); return () => observer.disconnect(); }, [ widgetRef, isFullHeight, displayOnlyCollections, note ]); // Preload the configuration. let props: ViewModeProps | undefined | null = null; const viewModeConfig = useViewModeConfig(note, viewType); if (note && notePath && viewModeConfig) { props = { note, noteIds, notePath, highlightedTokens, viewConfig: viewModeConfig[0], saveConfig: viewModeConfig[1] } } return (
{props && isEnabled && (
{getComponentByViewType(viewType, props)}
)}
); } function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps) { switch (viewType) { case "list": return ; case "grid": return ; case "geoMap": return ; case "calendar": return case "table": return case "board": return case "presentation": return } } function useNoteViewType(note?: FNote | null): ViewTypeOptions | undefined { const [ viewType ] = useNoteLabel(note, "viewType"); if (!note) { return undefined; } else if (!(allViewTypes as readonly string[]).includes(viewType || "")) { // when not explicitly set, decide based on the note type return note.type === "search" ? "list" : "grid"; } else { return viewType as ViewTypeOptions; } } export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined, ntxId: string | null | undefined) { const [ noteIds, setNoteIds ] = useState([]); const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived"); async function refreshNoteIds() { if (!note) { setNoteIds([]); } else { setNoteIds(await getNoteIds(note)); } } async function getNoteIds(note: FNote) { if (viewType === "list" || viewType === "grid") { return note.getChildNoteIds(); } else { return await note.getSubtreeNoteIds(includeArchived); } } // Refresh on note switch. useEffect(() => { refreshNoteIds() }, [ note, includeArchived ]); // Refresh on alterations to the note subtree. useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (note && loadResults.getBranchRows().some(branch => branch.parentNoteId === note.noteId || noteIds.includes(branch.parentNoteId ?? "")) || loadResults.getAttributeRows().some(attr => attr.name === "archived" && attr.noteId && noteIds.includes(attr.noteId)) ) { refreshNoteIds(); } }) // Refresh on search. useTriliumEvent("searchRefreshed", ({ ntxId: eventNtxId }) => { if (eventNtxId !== ntxId) return; refreshNoteIds(); }); // Refresh on import. useEffect(() => { async function onImport(message: WebSocketMessage) { if (!("taskType" in message) || message.taskType !== "importNotes" || message.type !== "taskSucceeded") return; const { parentNoteId, importedNoteId } = message.result; if (!parentNoteId || !importedNoteId) return; if (importedNoteId && (parentNoteId === note?.noteId || noteIds.includes(parentNoteId))) { const importedNote = await froca.getNote(importedNoteId); if (!importedNote) return; setNoteIds([ ...noteIds, ...await getNoteIds(importedNote), importedNoteId ]) } } subscribeToMessages(onImport); return () => unsubscribeFromMessage(onImport); }, [ note, noteIds, setNoteIds ]) return noteIds; } export function useViewModeConfig(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) { const [ viewConfig, setViewConfig ] = useState<[T | undefined, (data: T) => void]>(); useEffect(() => { if (!note || !viewType) return; setViewConfig(undefined); const viewStorage = new ViewModeStorage(note, viewType); viewStorage.restore().then(config => { const storeFn = (config: T) => { setViewConfig([ config, storeFn ]); viewStorage.store(config); }; setViewConfig([ config, storeFn ]); }); }, [ note, viewType ]); return viewConfig; }