2025-08-30 15:44:49 +03:00
import { allViewTypes , ViewModeProps , ViewTypeOptions } from "./interface" ;
2025-09-12 17:57:58 +03:00
import { useNoteContext , useNoteLabel , useNoteLabelBoolean , useTriliumEvent } from "../react/hooks" ;
2025-08-30 15:11:49 +03:00
import FNote from "../../entities/fnote" ;
import "./NoteList.css" ;
2025-08-30 19:21:26 +03:00
import { ListView , GridView } from "./legacy/ListOrGridView" ;
2025-09-14 10:53:54 +03:00
import { useEffect , useRef , useState } from "preact/hooks" ;
2025-09-03 23:17:35 +03:00
import GeoView from "./geomap" ;
2025-09-14 10:53:54 +03:00
import ViewModeStorage from "./view_mode_storage" ;
2025-09-05 16:02:35 +03:00
import CalendarView from "./calendar" ;
2025-09-06 18:48:58 +03:00
import TableView from "./table" ;
2025-09-10 20:18:17 +03:00
import BoardView from "./board" ;
2025-09-13 14:42:02 +03:00
import { subscribeToMessages , unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws" ;
import { WebSocketMessage } from "@triliumnext/commons" ;
2025-09-13 14:47:40 +03:00
import froca from "../../services/froca" ;
2025-10-15 18:49:29 +03:00
import PresentationView from "./presentation" ;
2025-08-30 15:11:49 +03:00
2025-10-15 18:49:06 +03:00
interface NoteListProps {
2025-09-23 21:44:39 +03:00
note : FNote | null | undefined ;
notePath : string | null | undefined ;
highlightedTokens? : string [ ] | null ;
2025-08-30 19:50:20 +03:00
/** 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. */
2025-08-30 14:29:54 +03:00
displayOnlyCollections? : boolean ;
2025-09-23 21:44:39 +03:00
isEnabled : boolean ;
2025-09-23 21:55:39 +03:00
ntxId : string | null | undefined ;
2025-09-23 21:44:39 +03:00
}
2025-10-15 18:49:06 +03:00
export default function NoteList < T extends object > ( props : Pick < NoteListProps , "displayOnlyCollections" > ) {
2025-09-23 21:55:39 +03:00
const { note , noteContext , notePath , ntxId } = useNoteContext ( ) ;
2025-09-23 21:44:39 +03:00
const isEnabled = noteContext ? . hasNoteList ( ) ;
2025-09-23 21:55:39 +03:00
return < CustomNoteList note = { note } isEnabled = { ! ! isEnabled } notePath = { notePath } ntxId = { ntxId } { ...props } / >
2025-09-23 21:44:39 +03:00
}
2025-10-15 18:49:06 +03:00
export function SearchNoteList < T extends object > ( props : Omit < NoteListProps , "isEnabled" > ) {
2025-09-23 21:44:39 +03:00
return < CustomNoteList { ...props } isEnabled = { true } / >
2025-08-30 14:29:54 +03:00
}
2025-10-18 21:35:19 +03:00
export function CustomNoteList < T extends object > ( { note , isEnabled : shouldEnable , notePath , highlightedTokens , displayOnlyCollections , ntxId } : NoteListProps ) {
2025-08-30 19:42:16 +03:00
const widgetRef = useRef < HTMLDivElement > ( null ) ;
2025-08-30 15:11:49 +03:00
const viewType = useNoteViewType ( note ) ;
2025-09-23 21:55:39 +03:00
const noteIds = useNoteIds ( note , viewType , ntxId ) ;
2025-09-13 11:46:17 +03:00
const isFullHeight = ( viewType && viewType !== "list" && viewType !== "grid" ) ;
2025-08-30 19:42:16 +03:00
const [ isIntersecting , setIsIntersecting ] = useState ( false ) ;
2025-08-30 19:50:20 +03:00
const shouldRender = ( isFullHeight || isIntersecting || note ? . type === "book" ) ;
2025-09-23 21:44:39 +03:00
const isEnabled = ( note && shouldEnable && ! ! viewType && shouldRender ) ;
2025-08-30 19:42:16 +03:00
useEffect ( ( ) = > {
2025-08-30 19:50:20 +03:00
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.
2025-08-30 19:48:05 +03:00
return ;
}
2025-08-30 19:42:16 +03:00
const observer = new IntersectionObserver (
( entries ) = > {
if ( ! isIntersecting ) {
setIsIntersecting ( entries [ 0 ] . isIntersecting ) ;
2025-09-13 11:46:17 +03:00
observer . disconnect ( ) ;
2025-08-30 19:42:16 +03:00
}
} ,
{
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 ( ) ;
2025-09-13 11:46:17 +03:00
} , [ widgetRef , isFullHeight , displayOnlyCollections , note ] ) ;
2025-08-30 15:11:49 +03:00
2025-09-04 15:13:48 +03:00
// Preload the configuration.
let props : ViewModeProps < any > | undefined | null = null ;
const viewModeConfig = useViewModeConfig ( note , viewType ) ;
2025-09-07 20:38:16 +03:00
if ( note && notePath && viewModeConfig ) {
2025-09-04 15:13:48 +03:00
props = {
2025-09-07 20:38:16 +03:00
note , noteIds , notePath ,
2025-09-04 15:13:48 +03:00
highlightedTokens ,
viewConfig : viewModeConfig [ 0 ] ,
saveConfig : viewModeConfig [ 1 ]
}
}
2025-09-03 23:57:38 +03:00
2025-08-30 15:11:49 +03:00
return (
2025-09-07 20:38:16 +03:00
< div ref = { widgetRef } className = { ` note-list-widget component ${ isFullHeight ? "full-height" : "" } ` } >
2025-09-04 15:13:48 +03:00
{ props && isEnabled && (
2025-08-30 15:11:49 +03:00
< div className = "note-list-widget-content" >
2025-09-04 15:13:48 +03:00
{ getComponentByViewType ( viewType , props ) }
2025-08-30 15:11:49 +03:00
< / div >
) }
< / div >
) ;
}
2025-09-04 15:13:48 +03:00
function getComponentByViewType ( viewType : ViewTypeOptions , props : ViewModeProps < any > ) {
2025-08-30 15:11:49 +03:00
switch ( viewType ) {
case "list" :
2025-08-30 15:44:49 +03:00
return < ListView { ...props } / > ;
2025-08-30 17:21:22 +03:00
case "grid" :
return < GridView { ...props } / > ;
2025-09-03 23:17:35 +03:00
case "geoMap" :
return < GeoView { ...props } / > ;
2025-09-05 16:02:35 +03:00
case "calendar" :
return < CalendarView { ...props } / >
2025-09-06 18:48:58 +03:00
case "table" :
return < TableView { ...props } / >
2025-09-10 20:18:17 +03:00
case "board" :
return < BoardView { ...props } / >
2025-10-15 18:49:29 +03:00
case "presentation" :
return < PresentationView { ...props } / >
2025-08-30 15:11:49 +03:00
}
}
function useNoteViewType ( note? : FNote | null ) : ViewTypeOptions | undefined {
const [ viewType ] = useNoteLabel ( note , "viewType" ) ;
2025-08-30 17:21:22 +03:00
2025-08-30 15:11:49 +03:00
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 ;
}
}
2025-10-18 21:19:53 +03:00
export function useNoteIds ( note : FNote | null | undefined , viewType : ViewTypeOptions | undefined , ntxId : string | null | undefined ) {
2025-08-30 15:11:49 +03:00
const [ noteIds , setNoteIds ] = useState < string [ ] > ( [ ] ) ;
2025-09-12 17:57:58 +03:00
const [ includeArchived ] = useNoteLabelBoolean ( note , "includeArchived" ) ;
2025-08-30 17:21:22 +03:00
2025-08-30 15:11:49 +03:00
async function refreshNoteIds() {
if ( ! note ) {
setNoteIds ( [ ] ) ;
} else {
2025-09-13 14:47:40 +03:00
setNoteIds ( await getNoteIds ( note ) ) ;
}
}
async function getNoteIds ( note : FNote ) {
if ( viewType === "list" || viewType === "grid" ) {
return note . getChildNoteIds ( ) ;
} else {
return await note . getSubtreeNoteIds ( includeArchived ) ;
2025-08-30 15:11:49 +03:00
}
}
// Refresh on note switch.
2025-09-12 17:57:58 +03:00
useEffect ( ( ) = > { refreshNoteIds ( ) } , [ note , includeArchived ] ) ;
2025-08-30 15:11:49 +03:00
// Refresh on alterations to the note subtree.
useTriliumEvent ( "entitiesReloaded" , ( { loadResults } ) = > {
if ( note && loadResults . getBranchRows ( ) . some ( branch = >
branch . parentNoteId === note . noteId
2025-09-12 17:20:22 +03:00
|| noteIds . includes ( branch . parentNoteId ? ? "" ) )
|| loadResults . getAttributeRows ( ) . some ( attr = > attr . name === "archived" && attr . noteId && noteIds . includes ( attr . noteId ) )
) {
2025-08-30 17:21:22 +03:00
refreshNoteIds ( ) ;
2025-08-30 15:11:49 +03:00
}
} )
2025-09-23 21:55:39 +03:00
// Refresh on search.
useTriliumEvent ( "searchRefreshed" , ( { ntxId : eventNtxId } ) = > {
if ( eventNtxId !== ntxId ) return ;
refreshNoteIds ( ) ;
} ) ;
2025-09-13 14:42:02 +03:00
// Refresh on import.
useEffect ( ( ) = > {
2025-09-13 14:47:40 +03:00
async function onImport ( message : WebSocketMessage ) {
2025-09-13 14:42:02 +03:00
if ( ! ( "taskType" in message ) || message . taskType !== "importNotes" || message . type !== "taskSucceeded" ) return ;
const { parentNoteId , importedNoteId } = message . result ;
2025-09-13 14:47:40 +03:00
if ( ! parentNoteId || ! importedNoteId ) return ;
if ( importedNoteId && ( parentNoteId === note ? . noteId || noteIds . includes ( parentNoteId ) ) ) {
const importedNote = await froca . getNote ( importedNoteId ) ;
if ( ! importedNote ) return ;
2025-09-13 14:42:02 +03:00
setNoteIds ( [
. . . noteIds ,
2025-09-13 14:47:40 +03:00
. . . await getNoteIds ( importedNote ) ,
2025-09-13 14:42:02 +03:00
importedNoteId
] )
}
}
subscribeToMessages ( onImport ) ;
return ( ) = > unsubscribeFromMessage ( onImport ) ;
} , [ note , noteIds , setNoteIds ] )
2025-08-30 15:11:49 +03:00
return noteIds ;
2025-08-30 17:21:22 +03:00
}
2025-09-04 15:13:48 +03:00
2025-10-18 21:19:53 +03:00
export function useViewModeConfig < T extends object > ( note : FNote | null | undefined , viewType : ViewTypeOptions | undefined ) {
2025-09-04 15:13:48 +03:00
const [ viewConfig , setViewConfig ] = useState < [ T | undefined , ( data : T ) = > void ] > ( ) ;
useEffect ( ( ) = > {
if ( ! note || ! viewType ) return ;
2025-09-29 17:40:40 +03:00
setViewConfig ( undefined ) ;
2025-09-04 15:13:48 +03:00
const viewStorage = new ViewModeStorage < T > ( note , viewType ) ;
viewStorage . restore ( ) . then ( config = > {
2025-09-11 18:42:32 +03:00
const storeFn = ( config : T ) = > {
setViewConfig ( [ config , storeFn ] ) ;
viewStorage . store ( config ) ;
} ;
2025-09-04 15:13:48 +03:00
setViewConfig ( [ config , storeFn ] ) ;
} ) ;
} , [ note , viewType ] ) ;
return viewConfig ;
}