diff --git a/apps/client/src/widgets/containers/launcher.tsx b/apps/client/src/widgets/containers/launcher.tsx index 2e9b6693f..93820296d 100644 --- a/apps/client/src/widgets/containers/launcher.tsx +++ b/apps/client/src/widgets/containers/launcher.tsx @@ -1,4 +1,3 @@ -import SyncStatusWidget from "../sync_status.js"; import BasicWidget, { wrapReactWidgets } from "../basic_widget.js"; import utils, { isMobile } from "../../services/utils.js"; import type FNote from "../../entities/fnote.js"; @@ -16,6 +15,7 @@ import { ParentComponent } from "../react/react_utils.jsx"; import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { LaunchBarActionButton, useLauncherIconAndTitle } from "../launch_bar/launch_bar_widgets.jsx"; import CalendarWidget from "../launch_bar/CalendarWidget.jsx"; +import SyncStatus from "../launch_bar/SyncStatus.jsx"; interface InnerWidget extends BasicWidget { settings?: { @@ -106,11 +106,11 @@ export default class LauncherWidget extends BasicWidget { return ; case "bookmarks": - return + return ; case "protectedSession": - return + return ; case "syncStatus": - return new SyncStatusWidget(); + return ; case "backInHistoryButton": return case "forwardInHistoryButton": diff --git a/apps/client/src/widgets/launch_bar/SyncStatus.css b/apps/client/src/widgets/launch_bar/SyncStatus.css new file mode 100644 index 000000000..dc9795e6a --- /dev/null +++ b/apps/client/src/widgets/launch_bar/SyncStatus.css @@ -0,0 +1,26 @@ +.sync-status { + box-sizing: border-box; +} + +.sync-status .sync-status-icon { + display: inline-block; + position: relative; + top: -5px; + font-size: 110%; +} + +.sync-status .sync-status-sub-icon { + font-size: 40%; + position: absolute; + inset-inline-start: 0; + top: 16px; +} + +.sync-status .sync-status-icon span { + border: none !important; +} + +.sync-status-icon:not(.sync-status-in-progress):hover { + background-color: var(--hover-item-background-color); + cursor: pointer; +} \ No newline at end of file diff --git a/apps/client/src/widgets/launch_bar/SyncStatus.tsx b/apps/client/src/widgets/launch_bar/SyncStatus.tsx new file mode 100644 index 000000000..b6f122d44 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/SyncStatus.tsx @@ -0,0 +1,118 @@ +import { useEffect, useRef, useState } from "preact/hooks"; +import "./SyncStatus.css"; +import { t } from "../../services/i18n"; +import clsx from "clsx"; +import { escapeQuotes } from "../../services/utils"; +import { useStaticTooltip, useTriliumOption } from "../react/hooks"; +import sync from "../../services/sync"; +import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws"; +import { WebSocketMessage } from "@triliumnext/commons"; + +type SyncState = "unknown" | "in-progress" + | "connected-with-changes" | "connected-no-changes" + | "disconnected-with-changes" | "disconnected-no-changes"; + +interface StateMapping { + title: string; + icon: string; + hasChanges?: boolean; +} + +const STATE_MAPPINGS: Record = { + unknown: { + title: t("sync_status.unknown"), + icon: "bx bx-time" + }, + "connected-with-changes": { + title: t("sync_status.connected_with_changes"), + icon: "bx bx-wifi", + hasChanges: true + }, + "connected-no-changes": { + title: t("sync_status.connected_no_changes"), + icon: "bx bx-wifi" + }, + "disconnected-with-changes": { + title: t("sync_status.disconnected_with_changes"), + icon: "bx bx-wifi-off", + hasChanges: true + }, + "disconnected-no-changes": { + title: t("sync_status.disconnected_no_changes"), + icon: "bx bx-wifi-off" + }, + "in-progress": { + title: t("sync_status.in_progress"), + icon: "bx bx-analyse bx-spin" + } +}; + +export default function SyncStatus() { + const syncState = useSyncStatus(); + const { title, icon, hasChanges } = STATE_MAPPINGS[syncState]; + const spanRef = useRef(null); + const [ syncServerHost ] = useTriliumOption("syncServerHost"); + useStaticTooltip(spanRef, { + html: true + // TODO: Placement + }); + + return (syncServerHost && +
+
+ { + if (syncState === "in-progress") return; + sync.syncNow(); + }} + > + {hasChanges && ( + + )} + +
+
+ ) +} + +function useSyncStatus() { + const [ syncState, setSyncState ] = useState("unknown"); + + useEffect(() => { + let lastSyncedPush: number; + + function onMessage(message: WebSocketMessage) { + // First, read last synced push. + if ("lastSyncedPush" in message) { + lastSyncedPush = message.lastSyncedPush; + } else if ("data" in message && message.data && "lastSyncedPush" in message.data && lastSyncedPush) { + lastSyncedPush = message.data.lastSyncedPush; + } + + // Determine if all changes were pushed. + const allChangesPushed = lastSyncedPush === ws.getMaxKnownEntityChangeSyncId(); + + let syncState: SyncState = "unknown"; + if (message.type === "sync-pull-in-progress") { + syncState = "in-progress"; + } else if (message.type === "sync-push-in-progress") { + syncState = "in-progress"; + } else if (message.type === "sync-finished") { + syncState = allChangesPushed ? "connected-no-changes" : "connected-with-changes"; + } else if (message.type === "sync-failed") { + syncState = allChangesPushed ? "disconnected-no-changes" : "disconnected-with-changes"; + } else if (message.type === "frontend-update") { + lastSyncedPush = message.data.lastSyncedPush; + } + setSyncState(syncState); + } + + subscribeToMessages(onMessage); + return () => unsubscribeToMessage(onMessage); + }, []); + + return syncState; +} diff --git a/apps/client/src/widgets/sync_status.ts b/apps/client/src/widgets/sync_status.ts deleted file mode 100644 index 3ce671034..000000000 --- a/apps/client/src/widgets/sync_status.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { t } from "../services/i18n.js"; -import BasicWidget from "./basic_widget.js"; -import ws from "../services/ws.js"; -import options from "../services/options.js"; -import syncService from "../services/sync.js"; -import { escapeQuotes, handleRightToLeftPlacement } from "../services/utils.js"; -import { Tooltip } from "bootstrap"; -import { WebSocketMessage } from "@triliumnext/commons"; - -const TPL = /*html*/` -
- - -
- - - - - - - - - - - - - - -
-
-`; - -export default class SyncStatusWidget extends BasicWidget { - - syncState: "unknown" | "in-progress" | "connected" | "disconnected"; - allChangesPushed: boolean; - lastSyncedPush!: number; - settings: { - // TriliumNextTODO: narrow types and use TitlePlacement Type - titlePlacement: string; - }; - - constructor() { - super(); - - this.syncState = "unknown"; - this.allChangesPushed = false; - this.settings = { - titlePlacement: "right" - }; - } - - doRender() { - this.$widget = $(TPL); - this.$widget.hide(); - - this.$widget.find(".sync-status-icon:not(.sync-status-in-progress)").on("click", () => syncService.syncNow()); - - ws.subscribeToMessages((message) => this.processMessage(message)); - } - - showIcon(className: string) { - if (!options.get("syncServerHost")) { - this.toggleInt(false); - return; - } - - Tooltip.getOrCreateInstance(this.$widget.find(`.sync-status-${className}`)[0], { - html: true, - placement: handleRightToLeftPlacement(this.settings.titlePlacement), - fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ] - }); - - this.$widget.show(); - this.$widget.find(".sync-status-icon").hide(); - this.$widget.find(`.sync-status-${className}`).show(); - } - - processMessage(message: WebSocketMessage) { - if (message.type === "sync-pull-in-progress") { - this.syncState = "in-progress"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "sync-push-in-progress") { - this.syncState = "in-progress"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "sync-finished") { - this.syncState = "connected"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "sync-failed") { - this.syncState = "disconnected"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "frontend-update") { - this.lastSyncedPush = message.data.lastSyncedPush; - } - - this.allChangesPushed = this.lastSyncedPush === ws.getMaxKnownEntityChangeSyncId(); - - if (["unknown", "in-progress"].includes(this.syncState)) { - this.showIcon(this.syncState); - } else { - this.showIcon(`${this.syncState}-${this.allChangesPushed ? "no-changes" : "with-changes"}`); - } - } -}