whiteboard.cljs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. (ns frontend.handler.whiteboard
  2. "Whiteboard related handlers"
  3. (:require [datascript.core :as d]
  4. [dommy.core :as dom]
  5. [frontend.db :as db]
  6. [frontend.db.model :as model]
  7. [frontend.handler.editor :as editor-handler]
  8. [frontend.handler.route :as route-handler]
  9. [frontend.handler.property.util :as pu]
  10. [frontend.state :as state]
  11. [frontend.config :as config]
  12. [frontend.storage :as storage]
  13. [frontend.util :as util]
  14. [logseq.common.util :as common-util]
  15. [logseq.graph-parser.whiteboard :as gp-whiteboard]
  16. [promesa.core :as p]
  17. [goog.object :as gobj]
  18. [clojure.set :as set]
  19. [clojure.string :as string]
  20. [cljs-bean.core :as bean]
  21. [logseq.db.sqlite.util :as sqlite-util]))
  22. ;; FIXME: no need to store :logseq.property/ls-type since it's stored already in `:block/type`
  23. (defn js->clj-keywordize
  24. [obj]
  25. (js->clj obj :keywordize-keys true))
  26. (defn shape->block [shape page-id]
  27. (let [repo (state/get-current-repo)]
  28. (gp-whiteboard/shape->block repo shape page-id)))
  29. (defn- build-shapes
  30. [page-block blocks]
  31. (let [page-metadata (pu/get-block-property-value page-block :logseq.property.tldraw/page)
  32. shapes-index (:shapes-index page-metadata)
  33. shape-id->index (zipmap shapes-index (range 0 (count shapes-index)))]
  34. (->> blocks
  35. (map (fn [block]
  36. (assoc block :index (get shape-id->index (str (:block/uuid block)) 0))))
  37. (filter pu/shape-block?)
  38. (map pu/block->shape)
  39. (sort-by :index))))
  40. (defn- whiteboard-clj->tldr [page-block blocks]
  41. (let [id (str (:block/uuid page-block))
  42. shapes (build-shapes page-block blocks)
  43. tldr-page (pu/page-block->tldr-page page-block)
  44. assets (:assets tldr-page)
  45. tldr-page (dissoc tldr-page :assets)]
  46. (clj->js {:currentPageId id
  47. :assets (or assets #js[])
  48. :selectedIds #js[]
  49. :pages [(merge tldr-page
  50. {:id id
  51. :name (:block/name page-block)
  52. :shapes shapes})]})))
  53. (defn db-build-page-block
  54. [page-entity page-name tldraw-page assets shapes-index]
  55. (let [get-k #(gobj/get tldraw-page %)]
  56. {:block/original-name page-name
  57. :block/name (util/page-name-sanity-lc page-name)
  58. :block/type "whiteboard"
  59. :logseq.property/ls-type :whiteboard-page
  60. :logseq.property.tldraw/page {:id (get-k "id")
  61. :name (get-k "name")
  62. :bindings (js->clj-keywordize (get-k "bindings"))
  63. :nonce (get-k "nonce")
  64. :assets (js->clj-keywordize assets)
  65. :shapes-index shapes-index}
  66. :block/updated-at (util/time-ms)
  67. :block/created-at (or (:block/created-at page-entity)
  68. (util/time-ms))}))
  69. (defn file-build-page-block
  70. [page-entity page-name tldraw-page assets shapes-index]
  71. (let [get-k #(gobj/get tldraw-page %)]
  72. {:block/original-name page-name
  73. :block/name (util/page-name-sanity-lc page-name)
  74. :block/type "whiteboard"
  75. :block/properties {(pu/get-pid :logseq.property/ls-type)
  76. :whiteboard-page
  77. (pu/get-pid :logseq.property.tldraw/page)
  78. {:id (get-k "id")
  79. :name (get-k "name")
  80. :bindings (js->clj-keywordize (get-k "bindings"))
  81. :nonce (get-k "nonce")
  82. :assets (js->clj-keywordize assets)
  83. :shapes-index shapes-index}}
  84. :block/updated-at (util/time-ms)
  85. :block/created-at (or (:block/created-at page-entity)
  86. (util/time-ms))}))
  87. (defn build-page-block
  88. [page-entity page-name tldraw-page assets shapes-index]
  89. (let [f (if (config/db-based-graph? (state/get-current-repo))
  90. db-build-page-block
  91. file-build-page-block)]
  92. (f page-entity page-name tldraw-page assets shapes-index)))
  93. (defn- compute-tx
  94. [^js app ^js tl-page new-id-nonces db-id-nonces page-uuid replace?]
  95. (let [page-entity (db/get-page page-uuid)
  96. assets (js->clj-keywordize (.getCleanUpAssets app))
  97. new-shapes (.-shapes tl-page)
  98. shapes-index (map #(gobj/get % "id") new-shapes)
  99. shape-id->index (zipmap shapes-index (range (.-length new-shapes)))
  100. upsert-shapes (->> (set/difference new-id-nonces db-id-nonces)
  101. (map (fn [{:keys [id]}]
  102. (-> (.-serialized ^js (.getShapeById tl-page id))
  103. js->clj-keywordize
  104. (assoc :index (get shape-id->index id)))))
  105. (set))
  106. old-ids (set (map :id db-id-nonces))
  107. new-ids (set (map :id new-id-nonces))
  108. created-ids (->> (set/difference new-ids old-ids)
  109. (remove string/blank?)
  110. (set))
  111. created-shapes (set (filter #(created-ids (:id %)) upsert-shapes))
  112. deleted-ids (->> (set/difference old-ids new-ids)
  113. (remove string/blank?))
  114. repo (state/get-current-repo)
  115. deleted-shapes (when (seq deleted-ids)
  116. (->> (db/pull-many repo '[*] (mapv (fn [id] [:block/uuid (uuid id)]) deleted-ids))
  117. (mapv (fn [b] (pu/get-block-property-value b :logseq.property.tldraw/shape)))
  118. (remove nil?)))
  119. deleted-shapes-tx (mapv (fn [id] [:db/retractEntity [:block/uuid (uuid id)]]) deleted-ids)
  120. upserted-blocks (->> (map #(shape->block % (:db/id page-entity)) upsert-shapes)
  121. (remove (fn [b]
  122. (= (:nonce
  123. (pu/get-block-property-value
  124. (db/entity [:block/uuid (:block/uuid b)])
  125. :logseq.property.tldraw/shape))
  126. (:nonce
  127. (pu/get-block-property-value
  128. b
  129. :logseq.property.tldraw/shape))))))
  130. page-name (or (:block/original-name page-entity) (str page-uuid))
  131. page-block (build-page-block page-entity page-name tl-page assets shapes-index)]
  132. (when (or (seq upserted-blocks)
  133. (seq deleted-shapes-tx)
  134. (not= (:block/properties page-block)
  135. (:block/properties page-entity)))
  136. {:page-block page-block
  137. :upserted-blocks (map sqlite-util/block-with-timestamps upserted-blocks)
  138. :delete-blocks deleted-shapes-tx
  139. :deleted-shapes deleted-shapes
  140. :new-shapes created-shapes
  141. :metadata {:whiteboard/transact? true
  142. :replace? replace?}})))
  143. (defonce *last-shapes-nonce (atom {}))
  144. ;; FIXME: it seems that nonce for the page block will not be updated with new updates for the whiteboard
  145. (defn <transact-tldr-delta!
  146. [page-uuid ^js app replace?]
  147. (let [tl-page ^js (second (first (.-pages app)))
  148. shapes (.-shapes ^js tl-page)
  149. page-block (model/get-page page-uuid)
  150. prev-page-metadata (pu/get-block-property-value page-block :logseq.property.tldraw/page)
  151. prev-shapes-index (:shapes-index prev-page-metadata)
  152. shape-id->prev-index (zipmap prev-shapes-index (range (count prev-shapes-index)))
  153. new-id-nonces (set (map-indexed (fn [idx shape]
  154. (let [id (.-id shape)]
  155. {:id id
  156. :nonce (if (= idx (get shape-id->prev-index id))
  157. (.-nonce shape)
  158. (js/Date.now))})) shapes))
  159. repo (state/get-current-repo)
  160. db-id-nonces (or
  161. (get-in @*last-shapes-nonce [repo page-uuid])
  162. (set (->> (model/get-whiteboard-id-nonces repo (:db/id page-block))
  163. (map #(update % :id str)))))
  164. {:keys [page-block new-shapes deleted-shapes upserted-blocks delete-blocks metadata] :as result}
  165. (compute-tx app tl-page new-id-nonces db-id-nonces page-uuid replace?)]
  166. (when (seq result)
  167. (let [tx-data (concat delete-blocks [page-block] upserted-blocks)
  168. metadata' (cond
  169. ;; group
  170. (some #(= "group" (:type %)) new-shapes)
  171. (assoc metadata :whiteboard/op :group)
  172. ;; ungroup
  173. (and (not-empty deleted-shapes) (every? #(= "group" (:type %)) deleted-shapes))
  174. (assoc metadata :whiteboard/op :un-group)
  175. ;; arrow
  176. (some #(and (= "line" (:type %))
  177. (= "arrow " (:end (:decorations %)))) new-shapes)
  178. (assoc metadata :whiteboard/op :new-arrow)
  179. :else
  180. metadata)]
  181. (swap! *last-shapes-nonce assoc-in [repo page-uuid] new-id-nonces)
  182. (if (contains? #{:new-arrow} (:whiteboard/op metadata'))
  183. (state/set-state! :whiteboard/pending-tx-data
  184. {:tx-data tx-data
  185. :metadata metadata'})
  186. (let [pending-tx-data (:whiteboard/pending-tx-data @state/state)
  187. tx-data' (concat (:tx-data pending-tx-data) tx-data)
  188. metadata'' (merge metadata' (:metadata pending-tx-data))]
  189. (state/set-state! :whiteboard/pending-tx-data {})
  190. (db/transact! repo tx-data' metadata'')))))))
  191. (defn get-default-new-whiteboard-tx
  192. [page-name id]
  193. (let [properties {(pu/get-pid :logseq.property/ls-type) :whiteboard-page,
  194. (pu/get-pid :logseq.property.tldraw/page)
  195. {:id (str id),
  196. :name page-name,
  197. :ls-type :whiteboard-page,
  198. :bindings {},
  199. :nonce 1,
  200. :assets []}}
  201. m #:block{:uuid id
  202. :name (util/page-name-sanity-lc page-name),
  203. :original-name page-name
  204. :type "whiteboard",
  205. :journal? false
  206. :format :markdown
  207. :updated-at (util/time-ms),
  208. :created-at (util/time-ms)}]
  209. (if (config/db-based-graph? (state/get-current-repo))
  210. [(merge m properties)]
  211. [(assoc m :block/properties properties)])))
  212. (defn <create-new-whiteboard-page!
  213. ([]
  214. (<create-new-whiteboard-page! nil))
  215. ([name]
  216. (p/let [uuid (or (and name (parse-uuid name)) (d/squuid))
  217. name (or name (str uuid))
  218. _ (db/transact! (get-default-new-whiteboard-tx name uuid))]
  219. uuid)))
  220. (defn <create-new-whiteboard-and-redirect!
  221. ([]
  222. (<create-new-whiteboard-and-redirect! (str (d/squuid))))
  223. ([name]
  224. (when-not config/publishing?
  225. (p/let [id (<create-new-whiteboard-page! name)]
  226. (route-handler/redirect-to-page! id {:new-whiteboard? true})))))
  227. (defn ->logseq-portal-shape
  228. [block-id point]
  229. {:blockType (if (parse-uuid (str block-id)) "B" "P")
  230. :id (str (d/squuid))
  231. :compact false
  232. :pageId (str block-id)
  233. :point point
  234. :size [400, 0]
  235. :type "logseq-portal"})
  236. (defn add-new-block-portal-shape!
  237. "Given the block uuid, add a new shape to the referenced block.
  238. By default it will be placed next to the given shape id"
  239. [block-uuid source-shape & {:keys [link? bottom?]}]
  240. (when-let [app (state/active-tldraw-app)]
  241. (let [^js api (.-api app)
  242. point (-> (.getShapeById app source-shape)
  243. (.-bounds)
  244. ((fn [bounds] (if bottom?
  245. [(.-minX ^js bounds) (+ 64 (.-maxY ^js bounds))]
  246. [(+ 64 (.-maxX ^js bounds)) (.-minY ^js bounds)]))))
  247. shape (->logseq-portal-shape block-uuid point)]
  248. (when (uuid? block-uuid) (editor-handler/set-blocks-id! [block-uuid]))
  249. (.createShapes api (clj->js shape))
  250. (when link?
  251. (.createNewLineBinding api source-shape (:id shape))))))
  252. (defn get-page-tldr
  253. [page-uuid]
  254. (let [page (model/get-page page-uuid)
  255. react-page (db/sub-block (:db/id page))
  256. blocks (:block/_page react-page)]
  257. (whiteboard-clj->tldr react-page blocks)))
  258. (defn <add-new-block!
  259. [page-uuid content]
  260. (p/let [repo (state/get-current-repo)
  261. new-block-id (db/new-block-id)
  262. page-entity (model/get-page page-uuid)
  263. tx (sqlite-util/block-with-timestamps
  264. {:block/uuid new-block-id
  265. :block/content (or content "")
  266. :block/format :markdown
  267. :block/page (:db/id page-entity)
  268. :block/parent (:db/id page-entity)})
  269. _ (db/transact! repo [tx] {:whiteboard/transact? true})]
  270. new-block-id))
  271. (defn inside-portal?
  272. [target]
  273. (some? (dom/closest target ".tl-logseq-cp-container")))
  274. (defn closest-shape
  275. [target]
  276. (when-let [shape-el (dom/closest target "[data-shape-id]")]
  277. (.getAttribute shape-el "data-shape-id")))
  278. (defn get-onboard-whiteboard-edn
  279. []
  280. (p/let [^js res (js/fetch "./whiteboard/onboarding.edn") ;; do we need to cache it?
  281. text (.text res)
  282. edn (common-util/safe-read-string text)]
  283. edn))
  284. (defn clone-whiteboard-from-edn
  285. "Given a tldr, clone the whiteboard page into current active whiteboard"
  286. ([edn]
  287. (when-let [app (state/active-tldraw-app)]
  288. (clone-whiteboard-from-edn edn (.-api app))))
  289. ([{:keys [pages blocks]} api]
  290. (let [page-block (first pages)
  291. ;; FIXME: should also clone normal blocks
  292. shapes (build-shapes page-block blocks)
  293. tldr-page (pu/page-block->tldr-page page-block)
  294. assets (:assets tldr-page)
  295. bindings (:bindings tldr-page)]
  296. (.cloneShapesIntoCurrentPage ^js api (clj->js {:shapes shapes
  297. :assets assets
  298. :bindings bindings})))))
  299. (defn should-populate-onboarding-whiteboard?
  300. "When there is no whiteboard, or there is only one whiteboard that has the given page name, we should populate the onboarding shapes"
  301. [page-uuid]
  302. (let [whiteboards (model/get-all-whiteboards (state/get-current-repo))]
  303. (and (or (empty? whiteboards)
  304. (and
  305. (= 1 (count whiteboards))
  306. (= (str page-uuid) (str (:block/uuid (first whiteboards))))))
  307. (not (state/get-onboarding-whiteboard?)))))
  308. (defn update-shapes!
  309. [shapes]
  310. (when-let [app (state/active-tldraw-app)]
  311. (let [^js api (.-api app)]
  312. (apply (.-updateShapes api) (bean/->js shapes)))))
  313. (defn update-shapes-index!
  314. [page-uuid]
  315. (when-let [app (state/active-tldraw-app)]
  316. (let [tl-page ^js (second (first (.-pages app)))]
  317. (when tl-page
  318. (when-let [page (db/get-page page-uuid)]
  319. (let [page-metadata (pu/get-block-property-value page :logseq.property.tldraw/page)
  320. shapes-index (:shapes-index page-metadata)]
  321. (when (seq shapes-index)
  322. (.updateShapesIndex tl-page (bean/->js shapes-index)))))))))
  323. (defn populate-onboarding-whiteboard
  324. [api]
  325. (when (some? api)
  326. (-> (p/let [edn (get-onboard-whiteboard-edn)]
  327. (clone-whiteboard-from-edn edn api)
  328. (state/set-onboarding-whiteboard! true))
  329. (p/catch
  330. (fn [e] (js/console.warn "Failed to populate onboarding whiteboard" e))))))
  331. (defn cleanup!
  332. [^js tl-page]
  333. (let [shapes (.-shapes tl-page)]
  334. (.cleanup tl-page (map #(.-id %) shapes))))
  335. (defn onboarding-show
  336. []
  337. (when (not (or (state/sub :whiteboard/onboarding-tour?)
  338. (config/demo-graph?)
  339. (util/mobile?)))
  340. (state/pub-event! [:whiteboard/onboarding])
  341. (state/set-state! [:whiteboard/onboarding-tour?] true)
  342. (storage/set :whiteboard-onboarding-tour? true)))