| 
									
										
										
										
											2025-08-10 12:22:11 +03:00
										 |  |  | import { Dispatch, StateUpdater, useEffect, useState } from "preact/hooks"; | 
					
						
							| 
									
										
										
										
											2025-08-10 00:32:26 +03:00
										 |  |  | import appContext from "../../components/app_context"; | 
					
						
							| 
									
										
										
										
											2025-08-10 12:22:11 +03:00
										 |  |  | import dialog from "../../services/dialog"; | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  | import { t } from "../../services/i18n"; | 
					
						
							|  |  |  | import server from "../../services/server"; | 
					
						
							|  |  |  | import toast from "../../services/toast"; | 
					
						
							|  |  |  | import Button from "../react/Button"; | 
					
						
							|  |  |  | import Modal from "../react/Modal"; | 
					
						
							|  |  |  | import ReactBasicWidget from "../react/ReactBasicWidget"; | 
					
						
							|  |  |  | import hoisted_note from "../../services/hoisted_note"; | 
					
						
							| 
									
										
										
										
											2025-08-10 13:02:17 +03:00
										 |  |  | import type { RecentChangeRow } from "@triliumnext/commons"; | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  | import froca from "../../services/froca"; | 
					
						
							|  |  |  | import { formatDateTime } from "../../utils/formatters"; | 
					
						
							|  |  |  | import link from "../../services/link"; | 
					
						
							|  |  |  | import RawHtml from "../react/RawHtml"; | 
					
						
							| 
									
										
										
										
											2025-08-07 22:31:51 +03:00
										 |  |  | import ws from "../../services/ws"; | 
					
						
							| 
									
										
										
										
											2025-08-10 00:32:26 +03:00
										 |  |  | import useTriliumEvent from "../react/hooks"; | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-10 00:32:26 +03:00
										 |  |  | function RecentChangesDialogComponent() { | 
					
						
							|  |  |  |     const [ ancestorNoteId, setAncestorNoteId ] = useState<string>(); | 
					
						
							| 
									
										
										
										
											2025-08-10 13:02:17 +03:00
										 |  |  |     const [ groupedByDate, setGroupedByDate ] = useState<Map<String, RecentChangeRow[]>>(); | 
					
						
							| 
									
										
										
										
											2025-08-10 00:32:26 +03:00
										 |  |  |     const [ needsRefresh, setNeedsRefresh ] = useState(false); | 
					
						
							|  |  |  |     const [ shown, setShown ] = useState(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => { | 
					
						
							|  |  |  |         setNeedsRefresh(true); | 
					
						
							|  |  |  |         setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId()); | 
					
						
							|  |  |  |         setShown(true); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (!groupedByDate || needsRefresh) { | 
					
						
							|  |  |  |         useEffect(() => { | 
					
						
							|  |  |  |             if (needsRefresh) { | 
					
						
							|  |  |  |                 setNeedsRefresh(false);    | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-10 13:02:17 +03:00
										 |  |  |             server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`) | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |                 .then(async (recentChanges) => { | 
					
						
							|  |  |  |                     // preload all notes into cache
 | 
					
						
							|  |  |  |                     await froca.getNotes( | 
					
						
							|  |  |  |                         recentChanges.map((r) => r.noteId), | 
					
						
							|  |  |  |                         true | 
					
						
							|  |  |  |                     ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     const groupedByDate = groupByDate(recentChanges); | 
					
						
							|  |  |  |                     setGroupedByDate(groupedByDate); | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-10 00:32:26 +03:00
										 |  |  |     return ( | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |         <Modal | 
					
						
							|  |  |  |             title={t("recent_changes.title")} | 
					
						
							|  |  |  |             className="recent-changes-dialog" | 
					
						
							|  |  |  |             size="lg" | 
					
						
							|  |  |  |             scrollable | 
					
						
							|  |  |  |             header={ | 
					
						
							|  |  |  |                 <Button | 
					
						
							|  |  |  |                     text={t("recent_changes.erase_notes_button")} | 
					
						
							|  |  |  |                     small style={{ padding: "0 10px" }} | 
					
						
							|  |  |  |                     onClick={() => { | 
					
						
							|  |  |  |                         server.post("notes/erase-deleted-notes-now").then(() => { | 
					
						
							|  |  |  |                             setNeedsRefresh(true); | 
					
						
							|  |  |  |                             toast.showMessage(t("recent_changes.deleted_notes_message")); | 
					
						
							|  |  |  |                         }); | 
					
						
							|  |  |  |                     }} | 
					
						
							|  |  |  |                 /> | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2025-08-10 00:32:26 +03:00
										 |  |  |             onHidden={() => setShown(false)} | 
					
						
							|  |  |  |             show={shown} | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |         > | 
					
						
							|  |  |  |             <div className="recent-changes-content"> | 
					
						
							|  |  |  |                 {groupedByDate?.size | 
					
						
							| 
									
										
										
										
											2025-08-10 12:22:11 +03:00
										 |  |  |                     ? <RecentChangesTimeline groupedByDate={groupedByDate} setShown={setShown} /> | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |                     : <>{t("recent_changes.no_changes_message")}</>} | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |         </Modal> | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-10 13:02:17 +03:00
										 |  |  | function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map<String, RecentChangeRow[]>, setShown: Dispatch<StateUpdater<boolean>> }) { | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |     return ( | 
					
						
							|  |  |  |         <> | 
					
						
							|  |  |  |             { Array.from(groupedByDate.entries()).map(([dateDay, dayChanges]) => { | 
					
						
							|  |  |  |                 const formattedDate = formatDateTime(dateDay as string, "full", "none"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return ( | 
					
						
							|  |  |  |                     <div> | 
					
						
							|  |  |  |                         <b>{formattedDate}</b> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                         <ul> | 
					
						
							|  |  |  |                             { dayChanges.map((change) => { | 
					
						
							|  |  |  |                                 const isDeleted = change.current_isDeleted; | 
					
						
							|  |  |  |                                 const formattedTime = formatDateTime(change.date, "none", "short"); | 
					
						
							|  |  |  |                                 const note = froca.getNoteFromCache(change.noteId); | 
					
						
							|  |  |  |                                 const notePath = note?.getBestNotePathString(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                                 return ( | 
					
						
							|  |  |  |                                     <li className={isDeleted ? "deleted-note" : ""}> | 
					
						
							|  |  |  |                                         <span title={change.date}>{formattedTime}</span> | 
					
						
							| 
									
										
										
										
											2025-08-07 22:31:51 +03:00
										 |  |  |                                         { !isDeleted | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |                                         ? <NoteLink notePath={notePath} title={change.current_title} /> | 
					
						
							| 
									
										
										
										
											2025-08-10 12:22:11 +03:00
										 |  |  |                                         : <DeletedNoteLink change={change} setShown={setShown} /> } | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |                                     </li> | 
					
						
							|  |  |  |                                 ); | 
					
						
							|  |  |  |                             })} | 
					
						
							|  |  |  |                         </ul> | 
					
						
							|  |  |  |                     </div> | 
					
						
							|  |  |  |                 ); | 
					
						
							|  |  |  |             })} | 
					
						
							|  |  |  |         </> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function NoteLink({ notePath, title }: { notePath: string, title: string }) { | 
					
						
							|  |  |  |     if (!notePath || !title) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null); | 
					
						
							|  |  |  |     useEffect(() => { | 
					
						
							|  |  |  |         link.createLink(notePath, { | 
					
						
							|  |  |  |             title, | 
					
						
							|  |  |  |             showNotePath: true | 
					
						
							|  |  |  |         }).then(setNoteLink); | 
					
						
							|  |  |  |     }, [notePath, title]); | 
					
						
							|  |  |  |     return ( | 
					
						
							| 
									
										
										
										
											2025-08-07 22:31:51 +03:00
										 |  |  |         noteLink ? <RawHtml className="note-title" html={noteLink[0].innerHTML} /> : <span className="note-title">{title}</span> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-10 13:02:17 +03:00
										 |  |  | function DeletedNoteLink({ change, setShown }: { change: RecentChangeRow, setShown: Dispatch<StateUpdater<boolean>> }) { | 
					
						
							| 
									
										
										
										
											2025-08-07 22:31:51 +03:00
										 |  |  |     return ( | 
					
						
							|  |  |  |         <> | 
					
						
							|  |  |  |             <span className="note-title">{change.current_title}</span> | 
					
						
							|  |  |  |               | 
					
						
							|  |  |  |             (<a href="javascript:" | 
					
						
							|  |  |  |                 onClick={() => { | 
					
						
							|  |  |  |                     async () => { | 
					
						
							|  |  |  |                         const text = t("recent_changes.confirm_undelete"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                         if (await dialog.confirm(text)) { | 
					
						
							|  |  |  |                             await server.put(`notes/${change.noteId}/undelete`); | 
					
						
							| 
									
										
										
										
											2025-08-10 12:22:11 +03:00
										 |  |  |                             setShown(false); | 
					
						
							| 
									
										
										
										
											2025-08-07 22:31:51 +03:00
										 |  |  |                             await ws.waitForMaxKnownEntityChangeId(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                             const activeContext = appContext.tabManager.getActiveContext(); | 
					
						
							|  |  |  |                             if (activeContext) { | 
					
						
							|  |  |  |                                 activeContext.setNote(change.noteId); | 
					
						
							|  |  |  |                             } | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 }}> | 
					
						
							|  |  |  |                 {t("recent_changes.undelete_link")}) | 
					
						
							|  |  |  |             </a> | 
					
						
							|  |  |  |         </> | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |     ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default class RecentChangesDialog extends ReactBasicWidget { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     get component() { | 
					
						
							| 
									
										
										
										
											2025-08-10 00:32:26 +03:00
										 |  |  |         return <RecentChangesDialogComponent /> | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-10 13:02:17 +03:00
										 |  |  | function groupByDate(rows: RecentChangeRow[]) { | 
					
						
							|  |  |  |     const groupedByDate = new Map<String, RecentChangeRow[]>(); | 
					
						
							| 
									
										
										
										
											2025-08-06 22:22:11 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for (const row of rows) { | 
					
						
							|  |  |  |         const dateDay = row.date.substr(0, 10); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!groupedByDate.has(dateDay)) { | 
					
						
							|  |  |  |             groupedByDate.set(dateDay, []); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         groupedByDate.get(dateDay)!.push(row); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return groupedByDate; | 
					
						
							|  |  |  | } |