From 0ea444c545b053932ef912cead0cedfe8efc1076 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Mon, 28 Nov 2022 10:06:28 +0800 Subject: [PATCH 01/16] fix: hovered or selected shape should have higher zindex --- tldraw/packages/react/src/components/Canvas/Canvas.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tldraw/packages/react/src/components/Canvas/Canvas.tsx b/tldraw/packages/react/src/components/Canvas/Canvas.tsx index c4edb062fd..6d3f3ca49d 100644 --- a/tldraw/packages/react/src/components/Canvas/Canvas.tsx +++ b/tldraw/packages/react/src/components/Canvas/Canvas.tsx @@ -138,7 +138,7 @@ export const Canvas = observer(function Renderer({ isSelected={selectedShapesSet.has(shape)} isErasing={erasingShapesSet.has(shape)} meta={meta} - zIndex={1000 + i} + zIndex={selectedOrHooveredShape === shape ? 10000 : 1000 + i} onEditingEnd={onEditingEnd} /> ))} From fe9e7e9b6061e3eba79477e6db5b9836da984c22 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Mon, 28 Nov 2022 14:35:35 +0800 Subject: [PATCH 02/16] fix: a merge issue --- tldraw/packages/react/src/components/Canvas/Canvas.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tldraw/packages/react/src/components/Canvas/Canvas.tsx b/tldraw/packages/react/src/components/Canvas/Canvas.tsx index 6d3f3ca49d..d6b7f32a89 100644 --- a/tldraw/packages/react/src/components/Canvas/Canvas.tsx +++ b/tldraw/packages/react/src/components/Canvas/Canvas.tsx @@ -110,6 +110,7 @@ export const Canvas = observer(function Renderer({ const selectedShapesSet = React.useMemo(() => new Set(selectedShapes || []), [selectedShapes]) const erasingShapesSet = React.useMemo(() => new Set(erasingShapes || []), [erasingShapes]) const singleSelectedShape = selectedShapes?.length === 1 ? selectedShapes[0] : undefined + const selectedOrHooveredShape = hoveredShape || singleSelectedShape return (
From 0a36a1ca9a2fbd25dda6117c12024d114c8bcf4c Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Mon, 28 Nov 2022 14:40:24 +0800 Subject: [PATCH 03/16] fix: typo --- tldraw/packages/react/src/components/Canvas/Canvas.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tldraw/packages/react/src/components/Canvas/Canvas.tsx b/tldraw/packages/react/src/components/Canvas/Canvas.tsx index d6b7f32a89..352aee4681 100644 --- a/tldraw/packages/react/src/components/Canvas/Canvas.tsx +++ b/tldraw/packages/react/src/components/Canvas/Canvas.tsx @@ -110,7 +110,7 @@ export const Canvas = observer(function Renderer({ const selectedShapesSet = React.useMemo(() => new Set(selectedShapes || []), [selectedShapes]) const erasingShapesSet = React.useMemo(() => new Set(erasingShapes || []), [erasingShapes]) const singleSelectedShape = selectedShapes?.length === 1 ? selectedShapes[0] : undefined - const selectedOrHooveredShape = hoveredShape || singleSelectedShape + const selectedOrHoveredShape = hoveredShape || singleSelectedShape return (
@@ -139,7 +139,7 @@ export const Canvas = observer(function Renderer({ isSelected={selectedShapesSet.has(shape)} isErasing={erasingShapesSet.has(shape)} meta={meta} - zIndex={selectedOrHooveredShape === shape ? 10000 : 1000 + i} + zIndex={selectedOrHoveredShape === shape ? 10000 : 1000 + i} onEditingEnd={onEditingEnd} /> ))} From a694db5bd3625ae997f4de0f34ab340536303bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20C=C3=89ARD?= Date: Thu, 24 Nov 2022 00:38:04 +0100 Subject: [PATCH 04/16] French typos --- src/main/frontend/dicts.cljc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index 00157621c0..fe57e0b396 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -1407,8 +1407,8 @@ :settings-page/enable-tooltip "Astuces" :settings-page/enable-whiteboards "Tableaux blancs" :settings-page/export-theme "Exporter le theme" - :settings-page/filename-format "Format de nm de fichier" - :settings-page/git-commit-delay "Délai (secondes) des commit Git automatiques" + :settings-page/filename-format "Format de nom de fichier" + :settings-page/git-commit-delay "Délai (secondes) des commits Git automatiques" :settings-page/git-confirm "Vous devez redémarrer l'application après avoir mis à jour le dossier Git" :settings-page/git-desc "est utilisé pour gérer les versions de pages, vous pouvez cliquer sur..." :settings-page/git-switcher-label "Activer les commits Git automatiques" @@ -1421,7 +1421,7 @@ :settings-page/show-brackets "Montrer les parenthèses, crochets et accolades" :settings-page/spell-checker "Vérification autographique" :settings-page/sync "Synchronisation" - :settings-page/tab-advanced "Advancé" + :settings-page/tab-advanced "Avancé" :settings-page/tab-assets "Pièces-jointes" :settings-page/tab-editor "Éditeur" :settings-page/tab-features "Fonctionnalités" From f914fc12394f7652e1adfac5aa56bdd3a6606a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20C=C3=89ARD?= Date: Thu, 24 Nov 2022 00:40:23 +0100 Subject: [PATCH 05/16] French typos --- src/main/frontend/dicts.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index fe57e0b396..b64c070f56 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -1419,7 +1419,7 @@ :settings-page/preferred-outdenting "Mise en retrait logique" :settings-page/shortcut-settings "Personnaliser les raccourcis" :settings-page/show-brackets "Montrer les parenthèses, crochets et accolades" - :settings-page/spell-checker "Vérification autographique" + :settings-page/spell-checker "Vérification orthographique" :settings-page/sync "Synchronisation" :settings-page/tab-advanced "Avancé" :settings-page/tab-assets "Pièces-jointes" From 0d520265efaf7dc74381c646fe319442c39af0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20C=C3=89ARD?= Date: Thu, 24 Nov 2022 00:10:16 +0100 Subject: [PATCH 06/16] French typo on first warning after opening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "à moins que vous n'ouvriez" au subjonctif présent. --- src/main/frontend/dicts.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index b64c070f56..b478adf8ed 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -1299,7 +1299,7 @@ :left-side-bar/new-whiteboard "Nouveau tableau blanc" :linked-references/filter-search "Rechercher dans les pages liées" :on-boarding/add-graph "Ajouter un graphe" - :on-boarding/demo-graph "Il s'agit d'un graphe de démo, les changements ne seront pas enregistrés à moins que vous n'ouvrir un dossier local." + :on-boarding/demo-graph "Il s'agit d'un graphe de démo, les changements ne seront pas enregistrés à moins que vous n'ouvriez un dossier local." :on-boarding/new-graph-desc-1 "Logseq supporte à la fois le Markdown et l'Org-mode. Vous pouvez ouvrir un dossier existant ou en créer un nouveau sur cet appareil. Vos données seront enregistrées uniquement sur cet appareil." :on-boarding/new-graph-desc-2 "Après avoir ouvert votre dossier, cela créera 3 sous-dossiers :" :on-boarding/new-graph-desc-3 "/journals - contient vos pages du journal" From 9f66c252409f1b546023b53f4648392bf902c9b2 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Mon, 28 Nov 2022 15:04:27 +0800 Subject: [PATCH 07/16] fix: decrease Object Action Bar distance when below portal --- .../apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx | 2 +- tldraw/packages/react/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx b/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx index 7326bebb07..e75b1855f0 100644 --- a/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx @@ -29,7 +29,7 @@ const _ContextBar: TLContextBarComponent = ({ shapes, offsets, hidden }) const elm = rContextBar.current if (!elm) return const size = rSize.current ?? [0, 0] - const [x, y] = getContextBarTranslation(size, { ...offsets, bottom: offsets.bottom - 32 }) + const [x, y] = getContextBarTranslation(size, offsets) elm.style.setProperty('transform', `translateX(${x}px) translateY(${y}px)`) }, [offsets]) diff --git a/tldraw/packages/react/src/index.ts b/tldraw/packages/react/src/index.ts index 4095c63638..e29ea4b01e 100644 --- a/tldraw/packages/react/src/index.ts +++ b/tldraw/packages/react/src/index.ts @@ -14,7 +14,7 @@ export function getContextBarTranslation(barSize: number[], offset: TLOffset) { let y = 0 if (offset.top < 116) { // Show on bottom - y = offset.height / 2 + 72 + y = offset.height / 2 + 40 // Too far down, move up if (offset.bottom < 140) { y += offset.bottom - 140 From 91eced93d0ceb3ea48322ca06e1472f2fa5dc51d Mon Sep 17 00:00:00 2001 From: Andelf Date: Mon, 28 Nov 2022 14:46:38 +0800 Subject: [PATCH 08/16] fix(test): e2e accessibility test fail --- src/main/frontend/ui.cljs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/frontend/ui.cljs b/src/main/frontend/ui.cljs index 55a0834785..07d80a4581 100644 --- a/src/main/frontend/ui.cljs +++ b/src/main/frontend/ui.cljs @@ -250,10 +250,11 @@ content]] [:div.ml-4.flex-shrink-0.flex [:button.inline-flex.text-gray-400.focus:outline-none.focus:text-gray-500.transition.ease-in-out.duration-150.notification-close-button - {:on-click (fn [] + {:aria-label "Close" + :on-click (fn [] (notification/clear! uid))} - (icon "x" {:fill "currentColor"})]]]]]]]))) + (icon "x" {:fill "currentColor"})]]]]]]]))) (declare button) From b84d42da8993aa0ba18b91c68966cb23945ff4cb Mon Sep 17 00:00:00 2001 From: Andelf Date: Mon, 28 Nov 2022 15:24:17 +0800 Subject: [PATCH 09/16] fixup! fix(test): e2e accessibility test fail --- src/main/frontend/handler/events.cljs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index e7cf1d0375..487b469b07 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -769,13 +769,13 @@ "We suggest you upgrade now to avoid potential bugs."] (when (seq paths) [:p - "For example, the files below have reserved characters that can't be synced on some platforms."])] - ] + "For example, the files below have reserved characters that can't be synced on some platforms."])]] (ui/button - "Update filename format" - :on-click (fn [] - (notification/clear-all!) - (state/set-modal! + "Update filename format" + :aria-label "Update filename format" + :on-click (fn [] + (notification/clear-all!) + (state/set-modal! (fn [_] (conversion-component/files-breaking-changed)) {:id :filename-format-panel :center? true}))) (when (seq paths) From f71305310e34c94c6648bcad197e322b24909c07 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 28 Nov 2022 16:14:49 +0800 Subject: [PATCH 10/16] fix: e2e tests --- src/main/frontend/handler/events.cljs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index 487b469b07..0fecc41867 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -353,7 +353,8 @@ (state/set-modal! #(git-component/file-specific-version path hash content))) ;; Hook on a graph is ready to be shown to the user. -;; It's different from :graph/resotred, as :graph/restored is for window reloaded +;; It's different from :graph/restored, as :graph/restored is for window reloaded +;; FIXME: config may not be loaded when the graph is ready. (defmethod handle :graph/ready [[_ repo]] (when (config/local-db? repo) @@ -363,14 +364,15 @@ (state/pub-event! [:graph/dir-gone dir])))) ;; FIXME: an ugly implementation for redirecting to page on new window is restored (repo-handler/graph-ready! repo) - (js/setTimeout - (fn [] - (let [filename-format (state/get-filename-format repo)] - (when (and (util/electron?) - (not (config/demo-graph?)) - (not= filename-format :triple-lowbar)) - (state/pub-event! [:ui/notify-outdated-filename-format []])))) - 3000)) + (when-not (config/test?) + (js/setTimeout + (fn [] + (let [filename-format (state/get-filename-format repo)] + (when (and (util/electron?) + (not (config/demo-graph?)) + (not= filename-format :triple-lowbar)) + (state/pub-event! [:ui/notify-outdated-filename-format []])))) + 3000))) (defmethod handle :notification/show [[_ {:keys [content status clear?]}]] (notification/show! content status clear?)) From e3c839be9fc858fbb2bf434bdf3671a2b075c8a7 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 28 Nov 2022 16:30:51 +0800 Subject: [PATCH 11/16] fix: enable CI environment variable --- shadow-cljs.edn | 3 ++- src/main/frontend/config.cljs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 5d191e6cea..b93911f5e4 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -35,7 +35,8 @@ :redef false}} :closure-defines {goog.debug.LOGGING_ENABLED true frontend.config/ENABLE-PLUGINS #shadow/env ["ENABLE_PLUGINS" :as :bool :default true] - frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]} + frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true] + frontend.config/TEST #shadow/env ["CI" :as :bool :default false]} ;; NOTE: electron, browser/mobile-app use different asset-paths. ;; For browser/mobile-app devs, assets are located in /static/js(via HTTP root). diff --git a/src/main/frontend/config.cljs b/src/main/frontend/config.cljs index b8828f50e8..ed94c5ff07 100644 --- a/src/main/frontend/config.cljs +++ b/src/main/frontend/config.cljs @@ -18,7 +18,8 @@ (reset! state/publishing? publishing?) -(def test? false) +(goog-define TEST false) +(def test? TEST) (goog-define ENABLE-FILE-SYNC-PRODUCTION false) From 12c97195133417cc14bf6e0a3f4f2149aadcefcf Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 28 Nov 2022 16:31:58 +0800 Subject: [PATCH 12/16] fix: not a fn --- src/main/frontend/handler/events.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index 0fecc41867..a65a472c41 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -364,7 +364,7 @@ (state/pub-event! [:graph/dir-gone dir])))) ;; FIXME: an ugly implementation for redirecting to page on new window is restored (repo-handler/graph-ready! repo) - (when-not (config/test?) + (when-not config/test? (js/setTimeout (fn [] (let [filename-format (state/get-filename-format repo)] From a46b7226add1a5c8d253cd559d3d4370907f76a2 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 28 Nov 2022 17:45:30 +0800 Subject: [PATCH 13/16] fix: refresh tokens when starting the app --- src/main/frontend/components/file_sync.cljs | 27 ++++++++++----------- src/main/frontend/components/file_sync.css | 4 --- src/main/frontend/handler/user.cljs | 21 +++++----------- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/main/frontend/components/file_sync.cljs b/src/main/frontend/components/file_sync.cljs index be0dc3535b..71fd8c94fd 100644 --- a/src/main/frontend/components/file_sync.cljs +++ b/src/main/frontend/components/file_sync.cljs @@ -409,7 +409,6 @@ (str "status-of-" (and (keyword? status) (name status)))])} (when (and (not config/publishing?) (user-handler/logged-in?)) - (ui/dropdown-with-links ;; trigger (fn [{:keys [toggle-fn]}] @@ -428,21 +427,21 @@ (ui/icon "cloud-off" {:size ui/icon-size})])) ;; links - (cond-> [] + (cond-> (vec + (when-not (and no-active-files? idle?) + (cond + need-password? + [{:title [:div.file-item.flex.items-center.leading-none.pt-3 + {:style {:margin-left -8}} + (ui/icon "lock" {:size 20}) [:span.pl-1.font-semibold "Password is required"]] + :options {:on-click fs-sync/sync-need-password!}}] + + ;; head of upcoming sync + (not no-active-files?) + [{:title [:div.file-item.is-first ""] + :options {:class "is-first-placeholder"}}]))) synced-file-graph? (concat - (when-not (and no-active-files? idle?) - (cond - need-password? - [{:title [:div.file-item.flex.items-center.leading-none.pt-3 - (ui/icon "lock" {:size 20}) [:span.pl-1.font-semibold "Password is required"]] - :options {:on-click fs-sync/sync-need-password!}}] - - ;; head of upcoming sync - (not no-active-files?) - [{:title [:div.file-item.is-first ""] - :options {:class "is-first-placeholder"}}])) - (map (fn [f] {:title [:div.file-item {:key (str "downloading-" f)} (gp-util/safe-decode-uri-component f)] diff --git a/src/main/frontend/components/file_sync.css b/src/main/frontend/components/file_sync.css index 965b44cbf8..82376190b0 100644 --- a/src/main/frontend/components/file_sync.css +++ b/src/main/frontend/components/file_sync.css @@ -159,10 +159,6 @@ .title-wrap { flex: 1; } - - .menu-link { - @apply px-2; - } } &.is-enabled-progress-pane { diff --git a/src/main/frontend/handler/user.cljs b/src/main/frontend/handler/user.cljs index 2c8cdccdc3..8eb674fa7c 100644 --- a/src/main/frontend/handler/user.cljs +++ b/src/main/frontend/handler/user.cljs @@ -133,24 +133,15 @@ (set-tokens! (:id_token (:body resp)) (:access_token (:body resp))))))))) (defn restore-tokens-from-localstorage - "Restore id-token, access-token, refresh-token from localstorage, - and refresh id-token&access-token if necessary. - return nil when tokens are not available." + "Refresh id-token&access-token, pull latest repos, returns nil when tokens are not available." [] (println "restore-tokens-from-localstorage") - (let [id-token (js/localStorage.getItem "id-token") - access-token (js/localStorage.getItem "access-token") - refresh-token (js/localStorage.getItem "refresh-token")] + (let [refresh-token (js/localStorage.getItem "refresh-token")] (when refresh-token - (set-tokens! id-token access-token refresh-token) - (when (or (nil? id-token) (nil? access-token) - (-> id-token parse-jwt almost-expired?) - (-> access-token parse-jwt almost-expired?)) - (go - ;; id-token or access-token expired - ( Date: Mon, 28 Nov 2022 17:50:06 +0800 Subject: [PATCH 14/16] chore: rename :user/login to :user/fetch-info-and-graphs --- src/main/frontend/handler/events.cljs | 2 +- src/main/frontend/handler/user.cljs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index a65a472c41..68e0998b02 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -76,7 +76,7 @@ (async/go (async/c (persist-var/load-vars))) (async/ resp :body (as-> $ (set-tokens! (:id_token $) (:access_token $) (:refresh_token $))) - (#(state/pub-event! [:user/login]))) + (#(state/pub-event! [:user/fetch-info-and-graphs]))) (debug/pprint "login-callback" resp))))) (defn logout [] From 6d9a1486fe9d1cd97bba674edbe033bb875b74d3 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 28 Nov 2022 17:54:47 +0800 Subject: [PATCH 15/16] enhance: don't show "successfully processed" when waiting --- src/main/frontend/components/file_sync.cljs | 52 +++++++++++---------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main/frontend/components/file_sync.cljs b/src/main/frontend/components/file_sync.cljs index 71fd8c94fd..1e96da99de 100644 --- a/src/main/frontend/components/file_sync.cljs +++ b/src/main/frontend/components/file_sync.cljs @@ -264,10 +264,13 @@ (storage/set :ui/file-sync-active-file-list? list-active?))) [list-active?]) - [:div.cp__file-sync-indicator-progress-pane - {:ref *el-ref - :class (when (and syncing? progressing?) "is-progress-active")} - (let [idle-&-no-active? (and idle? no-active-files?)] + (let [idle-&-no-active? (and idle? no-active-files?) + waiting? (not (or (not online?) + idle-&-no-active? + syncing?))] + [:div.cp__file-sync-indicator-progress-pane + {:ref *el-ref + :class (when (and syncing? progressing?) "is-progress-active")} [:div.a [:div.al [:strong @@ -285,30 +288,31 @@ :else "Waiting..." )]] [:div.ar - (when queuing? (sync-now))]]) + (when queuing? (sync-now))]] - [:div.b.dark:text-gray-200 - [:div.bl - [:span.flex.items-center - (if no-active-files? - [:span.opacity-100.pr-1 "Successfully processed"] - [:span.opacity-60.pr-1 "Processed"])] + (when-not waiting? + [:div.b.dark:text-gray-200 + [:div.bl + [:span.flex.items-center + (if no-active-files? + [:span.opacity-100.pr-1 "Successfully processed"] + [:span.opacity-60.pr-1 "Processed"])] - (first tip-b&p)] + (first tip-b&p)] - [:div.br - [:small.opacity-50 - (when syncing? - (calc-time-left))]]] + [:div.br + [:small.opacity-50 + (when syncing? + (calc-time-left))]]]) - [:div.c - (second tip-b&p) - (when (or history-files? (not no-active-files?)) - [:span.inline-flex.ml-1.active:opacity-50 - {:on-click #(set-list-active? (not list-active?))} - (if list-active? - (ui/icon "chevron-up" {:style {:font-size 24}}) - (ui/icon "chevron-left" {:style {:font-size 24}}))])]])) + [:div.c + (second tip-b&p) + (when (or history-files? (not no-active-files?)) + [:span.inline-flex.ml-1.active:opacity-50 + {:on-click #(set-list-active? (not list-active?))} + (if list-active? + (ui/icon "chevron-up" {:style {:font-size 24}}) + (ui/icon "chevron-left" {:style {:font-size 24}}))])]]))) (defn- sort-files [files] From d342fdb1ae1a7b238561338c6c9dd456c77e8f0f Mon Sep 17 00:00:00 2001 From: Andelf Date: Tue, 29 Nov 2022 05:01:49 +0800 Subject: [PATCH 16/16] refactor(android): rewrite fs watcher - Polling based fs watcher - Avoid notifying too soon after file op Close #7072 Close #7106 Close #6740 --- .../main/java/com/logseq/app/FsWatcher.java | 272 ++++++++++-------- 1 file changed, 152 insertions(+), 120 deletions(-) diff --git a/android/app/src/main/java/com/logseq/app/FsWatcher.java b/android/app/src/main/java/com/logseq/app/FsWatcher.java index 2fc8872da1..839e613883 100644 --- a/android/app/src/main/java/com/logseq/app/FsWatcher.java +++ b/android/app/src/main/java/com/logseq/app/FsWatcher.java @@ -1,7 +1,5 @@ package com.logseq.app; -import android.annotation.SuppressLint; -import android.os.Build; import android.system.ErrnoException; import android.system.Os; import android.system.StructStat; @@ -12,8 +10,9 @@ import android.net.Uri; import java.io.*; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; import java.util.regex.Pattern; import java.io.File; @@ -26,9 +25,9 @@ import com.getcapacitor.PluginCall; @CapacitorPlugin(name = "FsWatcher") public class FsWatcher extends Plugin { - - List observers; private String mPath; + private PollingFsWatcher mWatcher; + private Thread mThread; @Override public void load() { @@ -37,14 +36,11 @@ public class FsWatcher extends Plugin { @PluginMethod() public void watch(PluginCall call) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - call.reject("Android version not supported"); - return; - } String pathParam = call.getString("path"); // check file:// or no scheme uris Uri u = Uri.parse(pathParam); Log.i("FsWatcher", "watching " + u); + // TODO: handle context:// uri if (u.getScheme() == null || u.getScheme().equals("file")) { File pathObj; try { @@ -56,32 +52,15 @@ public class FsWatcher extends Plugin { mPath = pathObj.getAbsolutePath(); - int mask = FileObserver.CLOSE_WRITE | - FileObserver.MOVE_SELF | FileObserver.MOVED_FROM | FileObserver.MOVED_TO | - FileObserver.DELETE | FileObserver.DELETE_SELF | FileObserver.CREATE; - - if (observers != null) { + if (mWatcher != null) { call.reject("already watching"); return; } - observers = new ArrayList<>(); - observers.add(new SingleFileObserver(pathObj, mask)); - // NOTE: only watch first level of directory - File[] files = pathObj.listFiles(); - if (files != null) { - for (File file : files) { - String filename = file.getName(); - if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) { - observers.add(new SingleFileObserver(file, mask)); - } - } - } + mWatcher = new PollingFsWatcher(mPath); + mThread = new Thread(mWatcher); + mThread.start(); - this.initialNotify(pathObj); - - for (int i = 0; i < observers.size(); i++) - observers.get(i).startWatching(); call.resolve(); } else { call.reject(u.getScheme() + " scheme not supported"); @@ -92,77 +71,69 @@ public class FsWatcher extends Plugin { public void unwatch(PluginCall call) { Log.i("FsWatcher", "unwatch all..."); - if (observers != null) { - for (int i = 0; i < observers.size(); ++i) - observers.get(i).stopWatching(); - observers.clear(); - observers = null; + if (mWatcher != null) { + mThread.interrupt(); + mWatcher = null; } call.resolve(); } - public void initialNotify(File pathObj) { - this.initialNotify(pathObj, 2); - } - - public void initialNotify(File pathObj, int maxDepth) { - if (maxDepth == 0) { - return; - } - File[] files = pathObj.listFiles(); - if (files != null) { - for (File file : files) { - String filename = file.getName(); - if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) { - this.initialNotify(file, maxDepth - 1); - } else if (file.isFile() - && Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", - file.getName())) { - this.onObserverEvent(FileObserver.CREATE, file.getAbsolutePath()); - } - } - } - } - // add, change, unlink events - public void onObserverEvent(int event, String path) { + public void onObserverEvent(int event, String path, SimpleFileMetadata metadata) { JSObject obj = new JSObject(); String content = null; File f = new File(path); + + boolean shouldRead = false; + if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown|excalidraw)$", f.getName())) { + shouldRead = true; + } + obj.put("path", Uri.fromFile(f)); obj.put("dir", Uri.fromFile(new File(mPath))); + JSObject stat; switch (event) { - case FileObserver.CLOSE_WRITE: + case FileObserver.MODIFY: obj.put("event", "change"); - try { - obj.put("stat", getFileStat(path)); - content = getFileContents(f); - } catch (IOException | ErrnoException e) { - e.printStackTrace(); + stat = new JSObject(); + stat.put("mtime", metadata.mtime); + stat.put("ctime", metadata.ctime); + stat.put("size", metadata.size); + obj.put("stat", stat); + if (shouldRead) { + try { + content = getFileContents(f); + } catch (IOException e) { + Log.e("FsWatcher", "error reading modified file"); + e.printStackTrace(); + } } + Log.i("FsWatcher", "prepare event " + obj); obj.put("content", content); break; - case FileObserver.MOVED_TO: case FileObserver.CREATE: obj.put("event", "add"); - try { - obj.put("stat", getFileStat(path)); - content = getFileContents(f); - } catch (IOException | ErrnoException e) { - e.printStackTrace(); + stat = new JSObject(); + stat.put("mtime", metadata.mtime); + stat.put("ctime", metadata.ctime); + stat.put("size", metadata.size); + obj.put("stat", stat); + if (shouldRead) { + try { + content = getFileContents(f); + } catch (IOException e) { + Log.e("FsWatcher", "error reading new file"); + e.printStackTrace(); + } } - Log.i("FsWatcher", "prepare event " + obj); obj.put("content", content); break; - case FileObserver.MOVE_SELF: - case FileObserver.MOVED_FROM: case FileObserver.DELETE: - case FileObserver.DELETE_SELF: if (f.exists()) { - Log.i("FsWatcher", "abandon notification due to file exists"); + Log.i("FsWatcher", "abandon delete notification due to file exists"); return; } else { obj.put("event", "unlink"); @@ -193,59 +164,120 @@ public class FsWatcher extends Plugin { return outputStream.toString("utf-8"); } - public static JSObject getFileStat(final String path) throws ErrnoException { - File file = new File(path); - StructStat stat = Os.stat(path); - JSObject obj = new JSObject(); - obj.put("atime", stat.st_atime); - obj.put("mtime", stat.st_mtime); - obj.put("ctime", stat.st_ctime); - obj.put("size", file.length()); - return obj; - } + public class SimpleFileMetadata { + public long mtime; + public long ctime; + public long size; + public long ino; - private class SingleFileObserver extends FileObserver { - private final String mPath; - - public SingleFileObserver(String path, int mask) { - super(path, mask); - mPath = path; + public SimpleFileMetadata(File file) throws ErrnoException { + StructStat stat = Os.stat(file.getPath()); + mtime = stat.st_mtime; + ctime = stat.st_ctime; + size = stat.st_size; + ino = stat.st_ino; } - @SuppressLint("NewApi") - public SingleFileObserver(File path, int mask) { - super(path, mask); - mPath = path.getAbsolutePath(); + public boolean equals(SimpleFileMetadata other) { + return mtime == other.mtime && ctime == other.ctime && size == other.size && ino == other.ino; + } + } + + + public class PollingFsWatcher implements Runnable { + private String mPath; + private Map metaDb; + + public PollingFsWatcher(String path) { + metaDb = new HashMap(); + + File dir = new File(path); + try { + mPath = dir.getCanonicalPath(); + } catch (IOException e) { + e.printStackTrace(); + } } @Override - public void onEvent(int event, String path) { - if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) { - Log.d("FsWatcher", "got path=" + mPath + "/" + path + " event=" + event); - // TODO: handle newly created directory - if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) { - String fullPath = mPath + "/" + path; - if (event == FileObserver.MOVE_SELF || event == FileObserver.MOVED_FROM || - event == FileObserver.DELETE || event == FileObserver.DELETE_SELF) { - Log.d("FsWatcher", "defer delete notification for " + path); - Thread timer = new Thread() { - @Override - public void run() { - try { - // delay 500ms then send, enough for most syncing net disks - Thread.sleep(500); - FsWatcher.this.onObserverEvent(event, fullPath); - } catch (InterruptedException e) { - e.printStackTrace(); - } + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + this.tick(); + Thread.sleep(2000); // The same as iOS fswatcher, 2s interval + } catch (InterruptedException e) { + // e.printStackTrace(); + Log.i("FsWatcher", "interrupted, unwatch"); + break; + } + } + + } + + private void tick() { + Map newMetaDb = new HashMap(); + + Stack paths = new Stack(); + paths.push(mPath); + while (!paths.isEmpty()) { + String dir = paths.pop(); + File curr = new File(dir); + + File[] files = curr.listFiles(); + if (files != null) { + for (File file : files) { + String filename = file.getName(); + if (file.isDirectory()) { + if (!filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) { + paths.push(file.getAbsolutePath()); } - }; - timer.start(); - } else { - FsWatcher.this.onObserverEvent(event, fullPath); + } else if (file.isFile() && !filename.equals("graphs-txid.edn") && !filename.equals("broken-config.edn")) { + try { + SimpleFileMetadata metadata = new SimpleFileMetadata(file); + newMetaDb.put(file.getAbsolutePath(), metadata); + } catch (ErrnoException e) { + } + } } } } + this.updateMetaDb(newMetaDb); + } + + private void updateMetaDb(Map newMetaDb) { + for (Map.Entry entry : newMetaDb.entrySet()) { + String path = entry.getKey(); + SimpleFileMetadata newMeta = entry.getValue(); + SimpleFileMetadata oldMeta = metaDb.remove(path); + if (oldMeta == null) { + // new file + onObserverEvent(FileObserver.CREATE, path, newMeta); + Log.d("FsWatcher", "create " + path); + } else if (!oldMeta.equals(newMeta)) { + // file changed + onObserverEvent(FileObserver.MODIFY, path, newMeta); + Log.d("FsWatcher", "changed " + path); + } + } + for (String path : metaDb.keySet()) { + // file deleted + Thread timer = new Thread() { + @Override + public void run() { + try { + // delay 500ms then send, enough for most syncing net disks + Thread.sleep(500); + onObserverEvent(FileObserver.DELETE, path, null); + Log.d("FsWatcher", "deleted " + path); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + timer.start(); + } + + this.metaDb = newMetaDb; } } }