events.cljs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. (ns frontend.handler.events
  2. "System-component-like ns that defines named events and listens on a
  3. core.async channel to handle them. Any part of the system can dispatch
  4. one of these events using state/pub-event!"
  5. (:refer-clojure :exclude [run!])
  6. (:require ["@sentry/react" :as Sentry]
  7. [cljs-bean.core :as bean]
  8. [clojure.core.async :as async]
  9. [clojure.core.async.interop :refer [p->c]]
  10. [clojure.string :as string]
  11. [frontend.commands :as commands]
  12. [frontend.components.rtc.indicator :as indicator]
  13. [frontend.config :as config]
  14. [frontend.date :as date]
  15. [frontend.db :as db]
  16. [frontend.db.async :as db-async]
  17. [frontend.db.model :as db-model]
  18. [frontend.db.react :as react]
  19. [frontend.db.transact :as db-transact]
  20. [frontend.extensions.fsrs :as fsrs]
  21. [frontend.fs :as fs]
  22. [frontend.fs.sync :as sync]
  23. [frontend.fs.watcher-handler :as fs-watcher]
  24. [frontend.handler.assets :as assets-handler]
  25. [frontend.handler.code :as code-handler]
  26. [frontend.handler.common.page :as page-common-handler]
  27. [frontend.handler.db-based.property :as db-property-handler]
  28. [frontend.handler.db-based.rtc :as rtc-handler]
  29. [frontend.handler.db-based.rtc-flows :as rtc-flows]
  30. [frontend.handler.editor :as editor-handler]
  31. [frontend.handler.export :as export]
  32. [frontend.handler.graph :as graph-handler]
  33. [frontend.handler.notification :as notification]
  34. [frontend.handler.page :as page-handler]
  35. [frontend.handler.plugin :as plugin-handler]
  36. [frontend.handler.repo :as repo-handler]
  37. [frontend.handler.repo-config :as repo-config-handler]
  38. [frontend.handler.route :as route-handler]
  39. [frontend.handler.search :as search-handler]
  40. [frontend.handler.shell :as shell-handler]
  41. [frontend.handler.ui :as ui-handler]
  42. [frontend.mobile.util :as mobile-util]
  43. [frontend.modules.instrumentation.posthog :as posthog]
  44. [frontend.modules.outliner.pipeline :as pipeline]
  45. [frontend.modules.outliner.ui :as ui-outliner-tx]
  46. [frontend.modules.shortcut.core :as st]
  47. [frontend.persist-db :as persist-db]
  48. [frontend.quick-capture :as quick-capture]
  49. [frontend.state :as state]
  50. [frontend.util :as util]
  51. [frontend.util.persist-var :as persist-var]
  52. [goog.dom :as gdom]
  53. [lambdaisland.glogi :as log]
  54. [logseq.db.frontend.schema :as db-schema]
  55. [logseq.shui.ui :as shui]
  56. [promesa.core :as p]))
  57. ;; TODO: should we move all events here?
  58. (defmulti handle first)
  59. (defn file-sync-restart! []
  60. (async/go (async/<! (p->c (persist-var/load-vars)))
  61. (async/<! (sync/<sync-stop))
  62. (some-> (sync/<sync-start) async/<!)))
  63. (defn file-sync-stop! []
  64. (async/go (async/<! (p->c (persist-var/load-vars)))
  65. (async/<! (sync/<sync-stop))))
  66. (defmethod handle :graph/added [[_ repo {:keys [empty-graph?]}]]
  67. (search-handler/rebuild-indices!)
  68. (plugin-handler/hook-plugin-app :graph-after-indexed {:repo repo :empty-graph? empty-graph?})
  69. (route-handler/redirect-to-home!)
  70. (when-let [dir-name (and (not (config/db-based-graph? repo)) (config/get-repo-dir repo))]
  71. (fs/watch-dir! dir-name))
  72. (file-sync-restart!))
  73. (defmethod handle :init/commands [_]
  74. (page-handler/init-commands!))
  75. (defmethod handle :graph/unlinked [repo current-repo]
  76. (when (= (:url repo) current-repo)
  77. (file-sync-restart!)))
  78. (defn- graph-switch
  79. [graph]
  80. (react/clear-query-state!)
  81. (let [db-based? (config/db-based-graph? graph)]
  82. (state/set-current-repo! graph)
  83. (page-handler/init-commands!)
  84. ;; load config
  85. (repo-config-handler/restore-repo-config! graph)
  86. (when-not (= :draw (state/get-current-route))
  87. (route-handler/redirect-to-home!))
  88. (when-not db-based?
  89. ;; graph-switch will trigger a rtc-start automatically
  90. ;; (rtc-handler/<rtc-start! graph)
  91. (file-sync-restart!))
  92. (when-let [dir-name (and (not db-based?) (config/get-repo-dir graph))]
  93. (fs/watch-dir! dir-name))
  94. (graph-handler/settle-metadata-to-local! {:last-seen-at (js/Date.now)})))
  95. ;; Parameters for the `persist-db` function, to show the notification messages
  96. (defn- graph-switch-on-persisted
  97. "graph: the target graph to switch to"
  98. [graph opts]
  99. (p/do!
  100. (repo-handler/restore-and-setup-repo! graph)
  101. (graph-switch graph)
  102. (state/set-state! :sync-graph/init? false)
  103. (when (:rtc-download? opts)
  104. (and (search-handler/rebuild-indices!) true)
  105. (repo-handler/refresh-repos!))))
  106. (defmethod handle :graph/switch [[_ graph opts]]
  107. (export/cancel-db-backup!)
  108. (persist-db/export-current-graph!)
  109. (state/set-state! :db/async-queries {})
  110. (st/refresh!)
  111. (p/let [writes-finished? (state/<invoke-db-worker :thread-api/file-writes-finished? (state/get-current-repo))
  112. request-finished? (db-transact/request-finished?)]
  113. (if (not writes-finished?) ; TODO: test (:sync-graph/init? @state/state)
  114. (do
  115. (log/info :graph/switch (cond->
  116. {:request-finished? request-finished?
  117. :file-writes-finished? writes-finished?}
  118. (false? request-finished?)
  119. (assoc :unfinished-requests? @db-transact/*unfinished-request-ids)))
  120. (notification/show!
  121. "Please wait seconds until all changes are saved for the current graph."
  122. :warning))
  123. (graph-switch-on-persisted graph opts))))
  124. (defmethod handle :graph/open-new-window [[_ev target-repo]]
  125. (ui-handler/open-new-window-or-tab! target-repo))
  126. (defmethod handle :graph/migrated [[_ _repo]]
  127. (js/alert "Graph migrated."))
  128. (defmethod handle :page/create [[_ page-name opts]]
  129. (if (= page-name (date/today))
  130. (page-handler/create-today-journal!)
  131. (page-handler/<create! page-name opts)))
  132. (defmethod handle :page/deleted [[_ repo page-name file-path tx-meta]]
  133. (page-common-handler/after-page-deleted! repo page-name file-path tx-meta))
  134. (defmethod handle :page/renamed [[_ repo data]]
  135. (page-common-handler/after-page-renamed! repo data))
  136. (defmethod handle :page/create-today-journal [[_ _repo]]
  137. (p/let [_ (page-handler/create-today-journal!)]
  138. (ui-handler/re-render-root!)))
  139. (defmethod handle :graph/sync-context []
  140. (let [context {:dev? config/dev?
  141. :node-test? util/node-test?
  142. :validate-db-options (:dev/validate-db-options (state/get-config))
  143. :importing? (:graph/importing @state/state)
  144. :date-formatter (state/get-date-formatter)
  145. :journal-file-name-format (or (state/get-journal-file-name-format)
  146. date/default-journal-filename-formatter)
  147. :export-bullet-indentation (state/get-export-bullet-indentation)
  148. :preferred-format (state/get-preferred-format)
  149. :journals-directory (config/get-journals-directory)
  150. :whiteboards-directory (config/get-whiteboards-directory)
  151. :pages-directory (config/get-pages-directory)}]
  152. (state/<invoke-db-worker :thread-api/set-context context)))
  153. ;; Hook on a graph is ready to be shown to the user.
  154. ;; It's different from :graph/restored, as :graph/restored is for window reloaded
  155. ;; FIXME: config may not be loaded when the graph is ready.
  156. (defmethod handle :graph/ready
  157. [[_ repo]]
  158. (when (config/local-file-based-graph? repo)
  159. (p/let [dir (config/get-repo-dir repo)
  160. dir-exists? (fs/dir-exists? dir)]
  161. (when (and (not dir-exists?)
  162. (not util/nfs?))
  163. (state/pub-event! [:graph/dir-gone dir]))))
  164. (let [db-based? (config/db-based-graph? repo)]
  165. (p/do!
  166. (state/pub-event! [:graph/sync-context])
  167. ;; re-render-root is async and delegated to rum, so we need to wait for main ui to refresh
  168. (when (util/mobile?)
  169. (state/pub-event! [:mobile/post-init]))
  170. ;; FIXME: an ugly implementation for redirecting to page on new window is restored
  171. (repo-handler/graph-ready! repo)
  172. (when-not config/publishing?
  173. (if db-based?
  174. (export/auto-db-backup! repo {:backup-now? true})
  175. (fs-watcher/load-graph-files! repo))))))
  176. (defmethod handle :instrument [[_ {:keys [type payload] :as opts}]]
  177. (when-not (empty? (dissoc opts :type :payload))
  178. (js/console.error "instrument data-map should only contains [:type :payload]"))
  179. (posthog/capture type payload))
  180. (defmethod handle :capture-error [[_ {:keys [error payload]}]]
  181. (let [[user-uuid graph-uuid tx-id] @sync/graphs-txid
  182. payload (merge
  183. {:schema-version (str db-schema/version)
  184. :db-schema-version (when-let [db (frontend.db/get-db)]
  185. (str (:kv/value (frontend.db/entity db :logseq.kv/schema-version))))
  186. :user-id user-uuid
  187. :graph-id graph-uuid
  188. :tx-id tx-id
  189. :db-based (config/db-based-graph? (state/get-current-repo))}
  190. payload)]
  191. (Sentry/captureException error
  192. (bean/->js {:tags payload}))))
  193. (defmethod handle :exec-plugin-cmd [[_ {:keys [pid cmd action]}]]
  194. (commands/exec-plugin-simple-command! pid cmd action))
  195. (defmethod handle :shortcut-handler-refreshed [[_]]
  196. (when-not @st/*pending-inited?
  197. (reset! st/*pending-inited? true)
  198. (st/consume-pending-shortcuts!)))
  199. (defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]]
  200. (let [main-node (util/app-scroll-container-node)]
  201. (state/set-state! :mobile/show-action-bar? false)
  202. (when (= (state/sub :editor/record-status) "RECORDING")
  203. (state/set-state! :mobile/show-recording-bar? true))
  204. (when-let [^js html (js/document.querySelector ":root")]
  205. (.setProperty (.-style html) "--ls-native-kb-height" (str keyboard-height "px"))
  206. (.add (.-classList html) "has-mobile-keyboard")
  207. (.setProperty (.-style html) "--ls-native-toolbar-opacity" 1))
  208. (when (mobile-util/native-ios?)
  209. (reset! util/keyboard-height keyboard-height)
  210. (set! (.. main-node -style -marginBottom) (str keyboard-height "px")))))
  211. (defmethod handle :mobile/keyboard-will-hide [[_]]
  212. (let [main-node (util/app-scroll-container-node)]
  213. (when (= (state/sub :editor/record-status) "RECORDING")
  214. (state/set-state! :mobile/show-recording-bar? false))
  215. (when-let [^js html (js/document.querySelector ":root")]
  216. (.removeProperty (.-style html) "--ls-native-kb-height")
  217. (.setProperty (.-style html) "--ls-native-toolbar-opacity" 0)
  218. (.remove (.-classList html) "has-mobile-keyboard"))
  219. (when (mobile-util/native-ios?)
  220. (when-let [card-preview-el (js/document.querySelector ".cards-review")]
  221. (set! (.. card-preview-el -style -marginBottom) "0px"))
  222. (set! (.. main-node -style -marginBottom) "0px")
  223. (when-let [left-sidebar-node (gdom/getElement "left-sidebar")]
  224. (set! (.. left-sidebar-node -style -bottom) "0px"))
  225. (when-let [right-sidebar-node (gdom/getElementByClass "sidebar-item-list")]
  226. (set! (.. right-sidebar-node -style -paddingBottom) "150px")))))
  227. (defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data] :as payload}]]
  228. (when-let [payload (and (seq blocks)
  229. (merge payload {:tx-data (map #(into [] %) tx-data)}))]
  230. (plugin-handler/hook-plugin-db :changed payload)
  231. (plugin-handler/hook-plugin-block-changes payload)))
  232. (defmethod handle :rebuild-slash-commands-list [[_]]
  233. (page-handler/rebuild-slash-commands-list!))
  234. (defmethod handle :shortcut/refresh [[_]]
  235. (st/refresh!))
  236. (defmethod handle :editor/set-heading [[_ block heading]]
  237. (when-let [id (:block/uuid block)]
  238. (editor-handler/set-heading! id heading)))
  239. (defmethod handle :graph/restored [[_ graph]]
  240. (when graph (assets-handler/ensure-assets-dir! graph))
  241. (rtc-flows/trigger-rtc-start graph)
  242. (fsrs/update-due-cards-count)
  243. (when-not (mobile-util/native-ios?)
  244. (state/pub-event! [:graph/ready graph])))
  245. (defmethod handle :whiteboard-link [[_ shapes]]
  246. (route-handler/go-to-search! :whiteboard/link)
  247. (state/set-state! :whiteboard/linked-shapes shapes))
  248. (defmethod handle :whiteboard-go-to-link [[_ link]]
  249. (route-handler/redirect! {:to :page
  250. :path-params {:name link}}))
  251. (defmethod handle :graph/save-db-to-disk [[_ _opts]]
  252. (persist-db/export-current-graph! {:succ-notification? true :force-save? true}))
  253. (defmethod handle :ui/re-render-root [[_]]
  254. (ui-handler/re-render-root!))
  255. (defmethod handle :run/cli-command [[_ command content]]
  256. (when (and command (not (string/blank? content)))
  257. (shell-handler/run-cli-command-wrapper! command content)))
  258. (defmethod handle :editor/quick-capture [[_ args]]
  259. (quick-capture/quick-capture args))
  260. (defmethod handle :modal/keymap [[_]]
  261. (state/open-settings! :keymap))
  262. (defmethod handle :editor/toggle-own-number-list [[_ blocks]]
  263. (let [batch? (sequential? blocks)
  264. blocks (cond->> blocks
  265. batch?
  266. (map #(cond-> % (or (uuid? %) (string? %)) (db-model/get-block-by-uuid))))]
  267. (if (and batch? (> (count blocks) 1))
  268. (editor-handler/toggle-blocks-as-own-order-list! blocks)
  269. (when-let [block (cond-> blocks batch? (first))]
  270. (if (editor-handler/own-order-number-list? block)
  271. (editor-handler/remove-block-own-order-list-type! block)
  272. (editor-handler/make-block-as-own-order-list! block))))))
  273. (defmethod handle :editor/remove-own-number-list [[_ block]]
  274. (when (some-> block (editor-handler/own-order-number-list?))
  275. (editor-handler/remove-block-own-order-list-type! block)))
  276. (defmethod handle :editor/save-current-block [_]
  277. (editor-handler/save-current-block!))
  278. (defmethod handle :editor/save-code-editor [_]
  279. (code-handler/save-code-editor!))
  280. (defmethod handle :editor/focus-code-editor [[_ editing-block container]]
  281. (when-let [^js cm (util/get-cm-instance container)]
  282. (when-not (.hasFocus cm)
  283. (let [cursor-pos (some-> (:editor/cursor-range @state/state) (deref) (count))
  284. direction (:block.editing/direction editing-block)
  285. pos (:block.editing/pos editing-block)
  286. to-line (case direction
  287. :up (.lastLine cm)
  288. (case pos
  289. :max (.lastLine cm)
  290. 0))]
  291. ;; move to friendly cursor
  292. (doto cm
  293. (.focus)
  294. (.setCursor to-line (or cursor-pos 0)))))))
  295. (defmethod handle :editor/toggle-children-number-list [[_ block]]
  296. (when-let [blocks (and block (db-model/get-block-immediate-children (state/get-current-repo) (:block/uuid block)))]
  297. (editor-handler/toggle-blocks-as-own-order-list! blocks)))
  298. (defmethod handle :editor/upsert-type-block [[_ {:keys [block type lang update-current-block?]}]]
  299. (p/do!
  300. (when-not update-current-block?
  301. (editor-handler/save-current-block!))
  302. (when-not update-current-block?
  303. (p/delay 16))
  304. (let [block (db/entity (:db/id block))
  305. block-type (:logseq.property.node/display-type block)
  306. block-title (:block/title block)
  307. latest-code-lang (or lang
  308. (:kv/value (db/entity :logseq.kv/latest-code-lang)))
  309. turn-type! #(if (and (= (keyword type) :code) latest-code-lang)
  310. (db-property-handler/set-block-properties!
  311. (:block/uuid %)
  312. {:logseq.property.node/display-type (keyword type)
  313. :logseq.property.code/lang latest-code-lang})
  314. (db-property-handler/set-block-property!
  315. (:block/uuid %) :logseq.property.node/display-type (keyword type)))]
  316. (p/let [block (if (or (not (nil? block-type))
  317. (and (not update-current-block?) (not (string/blank? block-title))))
  318. (p/let [result (ui-outliner-tx/transact!
  319. {:outliner-op :insert-blocks}
  320. ;; insert a new block
  321. (let [[_p _ block'] (editor-handler/insert-new-block-aux! {} block "")]
  322. (turn-type! block')))]
  323. (when-let [id (:block/uuid (first (:blocks result)))]
  324. (db/entity [:block/uuid id])))
  325. (p/do!
  326. (turn-type! block)
  327. (db/entity [:block/uuid (:block/uuid block)])))]
  328. (js/setTimeout #(editor-handler/edit-block! block :max) 100)))))
  329. (defmethod handle :vector-search/sync-state [[_ state]]
  330. (state/set-state! :vector-search/state state))
  331. (defmethod handle :rtc/sync-state [[_ state]]
  332. (state/update-state! :rtc/state (fn [old] (merge old state))))
  333. (defmethod handle :rtc/log [[_ data]]
  334. (state/set-state! :rtc/log data))
  335. (defmethod handle :rtc/remote-graph-gone [_]
  336. (p/do!
  337. (notification/show! "This graph has been removed from Logseq Sync." :warning false)
  338. (rtc-handler/<get-remote-graphs)))
  339. (defmethod handle :rtc/download-remote-graph [[_ graph-name graph-uuid graph-schema-version]]
  340. (assert (= (:major (db-schema/parse-schema-version db-schema/version))
  341. (:major (db-schema/parse-schema-version graph-schema-version)))
  342. {:app db-schema/version
  343. :remote-graph graph-schema-version})
  344. (->
  345. (p/do!
  346. (when (util/mobile?)
  347. (shui/popup-show!
  348. nil
  349. (fn []
  350. [:div.flex.flex-col.items-center.justify-center.mt-8.gap-4
  351. [:div (str "Downloading " graph-name " ...")]
  352. (indicator/downloading-logs)])
  353. {:id :download-rtc-graph}))
  354. (rtc-handler/<rtc-download-graph! graph-name graph-uuid graph-schema-version 60000)
  355. (rtc-handler/<get-remote-graphs)
  356. (when (util/mobile?)
  357. (shui/popup-hide! :download-rtc-graph)))
  358. (p/catch (fn [e]
  359. (println "RTC download graph failed, error:")
  360. (js/console.error e)
  361. (when (util/mobile?)
  362. (shui/popup-hide! :download-rtc-graph)
  363. ;; TODO: notify error
  364. )))))
  365. ;; db-worker -> UI
  366. (defmethod handle :db/sync-changes [[_ data]]
  367. (let [retract-datoms (filter (fn [d] (and (= :block/uuid (:a d)) (false? (:added d)))) (:tx-data data))
  368. retracted-tx-data (map (fn [d] [:db/retractEntity (:e d)]) retract-datoms)
  369. tx-data (concat (:tx-data data) retracted-tx-data)]
  370. (pipeline/invoke-hooks (assoc data :tx-data tx-data))
  371. nil))
  372. (defmethod handle :db/export-sqlite [_]
  373. (export/export-repo-as-sqlite-db! (state/get-current-repo))
  374. nil)
  375. (defmethod handle :editor/run-query-command [_]
  376. (editor-handler/run-query-command!))
  377. (defmethod handle :editor/load-blocks [[_ ids]]
  378. (when (seq ids)
  379. ;; not using `<get-blocks` here becuase because we want to
  380. ;; load all nested children here for copy/export
  381. (p/all (map (fn [id]
  382. (db-async/<get-block (state/get-current-repo) id
  383. {:skip-refresh? false})) ids))))
  384. (defmethod handle :vector-search/load-model-progress [[_ data]]
  385. (state/set-state! :vector-search/load-model-progress data))
  386. (defn run!
  387. []
  388. (let [chan (state/get-events-chan)]
  389. (async/go-loop []
  390. (let [[payload d] (async/<! chan)]
  391. (->
  392. (try
  393. (p/resolved (handle payload))
  394. (catch :default error
  395. (p/rejected error)))
  396. (p/then (fn [result]
  397. (p/resolve! d result)))
  398. (p/catch (fn [error]
  399. (log/error :event-error error :event (first payload))
  400. (let [type :handle-system-events/failed]
  401. (state/pub-event! [:capture-error {:error error
  402. :payload {:type type
  403. :payload payload}}])
  404. (p/reject! d error))))))
  405. (recur))
  406. chan))
  407. (comment
  408. (let [{:keys [deprecated-app-id current-app-id]} {:deprecated-app-id "AFDADF9A-7466-4ED8-B74F-AAAA0D4565B9", :current-app-id "7563518E-0EFD-4AD2-8577-10CFFD6E4596"}]
  409. (def deprecated-app-id deprecated-app-id)
  410. (def current-app-id current-app-id))
  411. (def deprecated-repo (state/get-current-repo))
  412. (def new-repo (string/replace deprecated-repo deprecated-app-id current-app-id))
  413. (update-file-path deprecated-repo new-repo deprecated-app-id current-app-id))