diff --git a/apps/client/src/stylesheets/theme-next/dialogs.css b/apps/client/src/stylesheets/theme-next/dialogs.css
index 9b8fb0306..985239d49 100644
--- a/apps/client/src/stylesheets/theme-next/dialogs.css
+++ b/apps/client/src/stylesheets/theme-next/dialogs.css
@@ -242,7 +242,7 @@ div.tn-tool-dialog {
}
/* Item title for deleted notes */
-.recent-changes-content ul li.deleted-note .note-title > .note-title {
+.recent-changes-content ul li.deleted-note .note-title {
text-decoration: line-through;
}
diff --git a/apps/client/src/widgets/dialogs/recent_changes.ts b/apps/client/src/widgets/dialogs/recent_changes.ts
deleted file mode 100644
index 280d716d0..000000000
--- a/apps/client/src/widgets/dialogs/recent_changes.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import { formatDateTime } from "../../utils/formatters.js";
-import { t } from "../../services/i18n.js";
-import appContext, { type EventData } from "../../components/app_context.js";
-import BasicWidget from "../basic_widget.js";
-import dialogService, { openDialog } from "../../services/dialog.js";
-import froca from "../../services/froca.js";
-import hoistedNoteService from "../../services/hoisted_note.js";
-import linkService from "../../services/link.js";
-import server from "../../services/server.js";
-import toastService from "../../services/toast.js";
-import ws from "../../services/ws.js";
-import { Modal } from "bootstrap";
-
-const TPL = /*html*/`
-
`;
-
-// TODO: Deduplicate with server.
-interface RecentChangesRow {
- noteId: string;
- date: string;
-}
-
-export default class RecentChangesDialog extends BasicWidget {
-
- private ancestorNoteId?: string;
-
- private modal!: bootstrap.Modal;
- private $content!: JQuery;
- private $eraseDeletedNotesNow!: JQuery;
-
- doRender() {
- this.$widget = $(TPL);
- this.modal = Modal.getOrCreateInstance(this.$widget[0]);
-
- this.$content = this.$widget.find(".recent-changes-content");
- this.$eraseDeletedNotesNow = this.$widget.find(".erase-deleted-notes-now-button");
- this.$eraseDeletedNotesNow.on("click", () => {
- server.post("notes/erase-deleted-notes-now").then(() => {
- this.refresh();
-
- toastService.showMessage(t("recent_changes.deleted_notes_message"));
- });
- });
- }
-
- async showRecentChangesEvent({ ancestorNoteId }: EventData<"showRecentChanges">) {
- this.ancestorNoteId = ancestorNoteId;
-
- await this.refresh();
-
- openDialog(this.$widget);
- }
-
- async refresh() {
- if (!this.ancestorNoteId) {
- this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
- }
-
- const recentChangesRows = await server.get(`recent-changes/${this.ancestorNoteId}`);
-
- // preload all notes into cache
- await froca.getNotes(
- recentChangesRows.map((r) => r.noteId),
- true
- );
-
- this.$content.empty();
-
- if (recentChangesRows.length === 0) {
- this.$content.append(t("recent_changes.no_changes_message"));
- }
-
- const groupedByDate = this.groupByDate(recentChangesRows);
-
- for (const [dateDay, dayChanges] of groupedByDate) {
- const $changesList = $("");
-
- const formattedDate = formatDateTime(dateDay, "full", "none");
- const dayEl = $("").append($("
").text(formattedDate)).append($changesList);
-
- for (const change of dayChanges) {
- const formattedTime = formatDateTime(change.date, "none", "short");
-
- let $noteLink;
-
- if (change.current_isDeleted) {
- $noteLink = $("");
-
- $noteLink.append($("").addClass("note-title").text(change.current_title));
-
- if (change.canBeUndeleted) {
- const $undeleteLink = $(``)
- .text(t("recent_changes.undelete_link"))
- .on("click", async () => {
- const text = t("recent_changes.confirm_undelete");
-
- if (await dialogService.confirm(text)) {
- await server.put(`notes/${change.noteId}/undelete`);
-
- this.modal.hide();
-
- await ws.waitForMaxKnownEntityChangeId();
-
- const activeContext = appContext.tabManager.getActiveContext();
- if (activeContext) {
- activeContext.setNote(change.noteId);
- }
- }
- });
-
- $noteLink.append(" (").append($undeleteLink).append(")");
- }
- } else {
- const note = await froca.getNote(change.noteId);
- const notePath = note?.getBestNotePathString();
-
- if (notePath) {
- $noteLink = await linkService.createLink(notePath, {
- title: change.title,
- showNotePath: true
- });
- } else {
- $noteLink = $("").text(note?.title ?? "");
- }
- }
-
- $changesList.append(
- $("- ")
- .on("click", (e) => {
- // Skip clicks on the link or deleted notes
- if (e.target?.nodeName !== "A" && !change.current_isDeleted) {
- // Open the current note
- const activeContext = appContext.tabManager.getActiveContext();
- if (activeContext) {
- activeContext.setNote(change.noteId);
- }
- }
- })
- .toggleClass("deleted-note", !!change.current_isDeleted)
- .append($("").text(formattedTime).attr("title", change.date))
- .append($noteLink.addClass("note-title"))
- );
- }
-
- this.$content.append(dayEl);
- }
- }
-
- groupByDate(rows: RecentChangesRow[]) {
- const groupedByDate = new Map();
-
- 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;
- }
-}
diff --git a/apps/client/src/widgets/dialogs/recent_changes.tsx b/apps/client/src/widgets/dialogs/recent_changes.tsx
new file mode 100644
index 000000000..ec93da186
--- /dev/null
+++ b/apps/client/src/widgets/dialogs/recent_changes.tsx
@@ -0,0 +1,156 @@
+import { useEffect, useState } from "preact/hooks";
+import { EventData } from "../../components/app_context";
+import { openDialog } from "../../services/dialog";
+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";
+import { RecentChangesRow } from "@triliumnext/commons";
+import froca from "../../services/froca";
+import { formatDateTime } from "../../utils/formatters";
+import link from "../../services/link";
+import RawHtml from "../react/RawHtml";
+
+interface RecentChangesDialogProps {
+ ancestorNoteId?: string;
+}
+
+function RecentChangesDialogComponent({ ancestorNoteId }: RecentChangesDialogProps) {
+ const [ groupedByDate, setGroupedByDate ] = useState