header.cljs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. (ns frontend.components.header
  2. (:require [cljs-bean.core :as bean]
  3. [cljs-time.coerce :as tc]
  4. [cljs-time.core :as t]
  5. [clojure.string :as string]
  6. [dommy.core :as d]
  7. [frontend.common.missionary :as c.m]
  8. [frontend.components.export :as export]
  9. [frontend.components.file-sync :as fs-sync]
  10. [frontend.components.page-menu :as page-menu]
  11. [frontend.components.plugins :as plugins]
  12. [frontend.components.right-sidebar :as sidebar]
  13. [frontend.components.rtc.indicator :as rtc-indicator]
  14. [frontend.components.server :as server]
  15. [frontend.components.settings :as settings]
  16. [frontend.components.svg :as svg]
  17. [frontend.config :as config]
  18. [frontend.context.i18n :refer [t]]
  19. [frontend.db :as db]
  20. [frontend.handler :as handler]
  21. [frontend.handler.db-based.rtc-flows :as rtc-flows]
  22. [frontend.handler.page :as page-handler]
  23. [frontend.handler.plugin :as plugin-handler]
  24. [frontend.handler.route :as route-handler]
  25. [frontend.handler.user :as user-handler]
  26. [frontend.hooks :as hooks]
  27. [frontend.mobile.util :as mobile-util]
  28. [frontend.state :as state]
  29. [frontend.storage :as storage]
  30. [frontend.ui :as ui]
  31. [frontend.util :as util]
  32. [frontend.version :refer [version]]
  33. [logseq.db :as ldb]
  34. [logseq.shui.ui :as shui]
  35. [logseq.shui.util :as shui-util]
  36. [missionary.core :as m]
  37. [reitit.frontend.easy :as rfe]
  38. [rum.core :as rum]))
  39. (rum/defc home-button
  40. < {:key-fn #(identity "home-button")}
  41. []
  42. (shui/button-ghost-icon :home
  43. {:title (t :home)
  44. :on-click #(do
  45. (when (mobile-util/native-iphone?)
  46. (state/set-left-sidebar-open! false))
  47. (route-handler/redirect-to-home!))}))
  48. (rum/defcs rtc-collaborators <
  49. rum/reactive
  50. (rum/local nil ::online-users)
  51. (rum/local nil ::online-users-canceler)
  52. {:will-mount (fn [state]
  53. (reset!
  54. (::online-users-canceler state)
  55. (c.m/run-task :fetch-online-users
  56. (m/reduce (fn [_ v] (reset! (::online-users state) v)) rtc-flows/rtc-online-users-flow)
  57. :succ (constantly nil)))
  58. state)
  59. :will-unmount (fn [state]
  60. (when @(::online-users-canceler state) (@(::online-users-canceler state)))
  61. (reset! (::online-users state) nil)
  62. state)}
  63. [state]
  64. (let [rtc-graph-id (ldb/get-graph-rtc-uuid (db/get-db))
  65. online-users @(::online-users state)]
  66. (when rtc-graph-id
  67. [:div.rtc-collaborators.flex.gap-1.text-sm.py-2.bg-gray-01.items-center
  68. (shui/button-ghost-icon :user-plus
  69. {:on-click #(shui/dialog-open!
  70. (fn []
  71. [:div.p-2.-mb-8
  72. [:h1.text-3xl.-mt-2.-ml-2 "Collaborators:"]
  73. (settings/settings-collaboration)]))})
  74. (when (seq online-users)
  75. (for [{user-email :user/email
  76. user-name :user/name
  77. user-uuid :user/uuid} online-users
  78. :let [color (shui-util/uuid-color user-uuid)]]
  79. (when user-name
  80. (shui/avatar
  81. {:class "w-5 h-5"
  82. :style {:app-region "no-drag"}
  83. :title user-email}
  84. (shui/avatar-fallback
  85. {:style {:background-color (str color "50")
  86. :font-size 11}}
  87. (some-> (subs user-name 0 2) (string/upper-case)))))))])))
  88. (rum/defc left-menu-button < rum/reactive
  89. < {:key-fn #(identity "left-menu-toggle-button")}
  90. [{:keys [on-click]}]
  91. (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
  92. [:button.#left-menu.cp__header-left-menu.button.icon
  93. {:title (t :header/toggle-left-sidebar)
  94. :on-click on-click}
  95. (ui/icon "menu-2" {:size ui/icon-size})]))
  96. (defn bug-report-url []
  97. (let [ua (.-userAgent js/navigator)
  98. safe-ua (string/replace ua #"[^_/a-zA-Z0-9\.\(\)]+" " ")
  99. platform (str "App Version: " version "\n"
  100. "Git Revision: " config/REVISION "\n"
  101. "Platform: " safe-ua "\n"
  102. "Language: " (.-language js/navigator) "\n"
  103. "Plugins: " (string/join ", " (map (fn [[k v]]
  104. (str (name k) " (" (:version v) ")"))
  105. (:plugin/installed-plugins @state/state))))]
  106. (str "https://github.com/logseq/logseq/issues/new?"
  107. "title=&"
  108. "template=bug_report.yaml&"
  109. "labels=from:in-app&"
  110. "platform="
  111. (js/encodeURIComponent platform))))
  112. (rum/defc ^:large-vars/cleanup-todo toolbar-dots-menu < rum/reactive
  113. [{:keys [current-repo t]}]
  114. (let [page (some-> (sidebar/get-current-page) db/get-page)
  115. page-menu (if (ldb/page? page)
  116. (page-menu/page-menu page)
  117. (when-not config/publishing?
  118. (when (config/db-based-graph?)
  119. (let [block-id-str (str (:block/uuid page))
  120. favorited? (page-handler/favorited? block-id-str)]
  121. [{:title (if favorited?
  122. (t :page/unfavorite)
  123. (t :page/add-to-favorites))
  124. :options {:on-click
  125. (fn []
  126. (if favorited?
  127. (page-handler/<unfavorite-page! block-id-str)
  128. (page-handler/<favorite-page! block-id-str)))}}]))))
  129. page-menu-and-hr (concat page-menu [{:hr true}])
  130. login? (and (state/sub :auth/id-token) (user-handler/logged-in?))
  131. items (fn []
  132. (->>
  133. [(when (state/enable-editing?)
  134. {:title (t :settings)
  135. :options {:on-click state/open-settings!}
  136. :icon (ui/icon "settings")})
  137. (when config/lsp-enabled?
  138. {:title (t :plugins)
  139. :options {:on-click #(plugin-handler/goto-plugins-dashboard!)}
  140. :icon (ui/icon "apps")})
  141. {:title (t :appearance)
  142. :options {:on-click #(state/pub-event! [:ui/toggle-appearance])}
  143. :icon (ui/icon "color-swatch")}
  144. (when current-repo
  145. {:title (t :export-graph)
  146. :options {:on-click #(shui/dialog-open! export/export)}
  147. :icon (ui/icon "database-export")})
  148. (when (and current-repo (state/enable-editing?))
  149. {:title (t :import)
  150. :options {:href (rfe/href :import)}
  151. :icon (ui/icon "file-upload")})
  152. (when config/publishing?
  153. {:title (t :toggle-theme)
  154. :options {:on-click #(state/toggle-theme!)}
  155. :icon (ui/icon "bulb")})
  156. ;; Disable login on Web until RTC is ready
  157. (when (and (not login?)
  158. (or
  159. (storage/get :login-enabled)
  160. (not util/web-platform?)))
  161. {:title (t :login)
  162. :options {:on-click #(state/pub-event! [:user/login])}
  163. :icon (ui/icon "user")})
  164. (when login? {:hr true})
  165. (when login?
  166. {:item [:span.flex.flex-col.relative.group.pt-1.w-full
  167. [:b.leading-none (user-handler/username)]
  168. [:small.opacity-70 (user-handler/email)]
  169. [:i.absolute.opacity-0.group-hover:opacity-100.text-red-rx-09
  170. {:class "right-1 top-3" :title (t :logout)}
  171. (ui/icon "logout")]]
  172. :options {:on-click #(user-handler/logout)
  173. :class "w-full"}})]
  174. (concat page-menu-and-hr)
  175. (remove nil?)))]
  176. (shui/button-ghost-icon :dots
  177. {:title (t :header/more)
  178. :class "toolbar-dots-btn"
  179. :on-pointer-down (fn [^js e]
  180. (shui/popup-show! (.-target e)
  181. (fn [{:keys [id]}]
  182. (for [{:keys [hr item title options icon]} (items)]
  183. (let [on-click' (:on-click options)
  184. href (:href options)]
  185. (if hr
  186. (shui/dropdown-menu-separator)
  187. (shui/dropdown-menu-item
  188. (assoc options
  189. :on-click (fn [^js e]
  190. (when on-click'
  191. (when-not (false? (on-click' e))
  192. (shui/popup-hide! id)))))
  193. (or item
  194. (if href
  195. [:a.flex.items-center.w-full
  196. {:href href :on-click #(shui/popup-hide! id)
  197. :style {:color "inherit"}}
  198. [:span.flex.items-center.gap-1.w-full
  199. icon [:div title]]]
  200. [:span.flex.items-center.gap-1.w-full
  201. icon [:div title]])))))))
  202. {:align "end"
  203. :as-dropdown? true
  204. :content-props {:class "w-64"
  205. :align-offset -32}}))})))
  206. (rum/defc back-and-forward
  207. < {:key-fn #(identity "nav-history-buttons")}
  208. []
  209. [:div.flex.flex-row
  210. (ui/with-shortcut :go/backward "bottom"
  211. (shui/button-ghost-icon :arrow-left
  212. {:title (t :header/go-back) :on-click #(js/window.history.back)
  213. :class "it navigation nav-left"}))
  214. (ui/with-shortcut :go/forward "bottom"
  215. (shui/button-ghost-icon :arrow-right
  216. {:title (t :header/go-forward) :on-click #(js/window.history.forward)
  217. :class "it navigation nav-right"}))])
  218. (rum/defc updater-tips-new-version
  219. [t]
  220. (let [[downloaded, set-downloaded] (rum/use-state nil)
  221. _ (hooks/use-effect!
  222. (fn []
  223. (when-let [channel (and (util/electron?) "auto-updater-downloaded")]
  224. (let [callback (fn [_ args]
  225. (js/console.debug "[new-version downloaded] args:" args)
  226. (let [args (bean/->clj args)]
  227. (set-downloaded args)
  228. (state/set-state! :electron/auto-updater-downloaded args))
  229. nil)]
  230. (js/apis.addListener channel callback)
  231. #(js/apis.removeListener channel callback))))
  232. [])]
  233. (when downloaded
  234. [:div.cp__header-tips
  235. [:p (t :updater/new-version-install)
  236. [:a.restart.ml-2
  237. {:on-click #(handler/quit-and-install-new-version!)}
  238. (svg/reload 16) [:strong (t :updater/quit-and-install)]]]])))
  239. (defn- clear-recent-highlight!
  240. []
  241. (let [nodes (d/by-class "recent-block")]
  242. (when (seq nodes)
  243. (doseq [node nodes]
  244. (d/remove-class! node "recent-block")))))
  245. (rum/defc recent-slider-inner
  246. []
  247. (let [[recent-days set-recent-days!] (rum/use-state (state/get-highlight-recent-days))
  248. [thumb-ref set-thumb-ref!] (rum/use-state nil)]
  249. (hooks/use-effect!
  250. (fn []
  251. (when thumb-ref
  252. (.focus ^js thumb-ref)))
  253. [thumb-ref])
  254. (hooks/use-effect!
  255. (fn []
  256. (let [all-nodes (d/by-class "ls-block")
  257. recent-node (fn [node]
  258. (let [id (some-> (d/attr node "blockid") uuid)
  259. block (db/entity [:block/uuid id])]
  260. (when block
  261. (t/after?
  262. (tc/from-long (:block/updated-at block))
  263. (t/ago (t/days recent-days))))))
  264. recent-nodes (filter recent-node all-nodes)
  265. old-nodes (remove recent-node all-nodes)]
  266. (when (seq recent-nodes)
  267. (doseq [node recent-nodes]
  268. (d/add-class! node "recent-block")))
  269. (when (seq old-nodes)
  270. (doseq [node old-nodes]
  271. (d/remove-class! node "recent-block")))))
  272. [recent-days])
  273. [:div.recent-slider.flex.flex-row.gap-1.items-center
  274. {:class "w-[32%]"}
  275. (shui/slider
  276. {:class "relative flex w-full touch-none select-none items-center "
  277. :default-value #js [3 100]
  278. :on-value-change (fn [result]
  279. (set-recent-days! (first result))
  280. (state/set-highlight-recent-days! (first result)))
  281. :minStepsBetweenThumbs 1}
  282. (shui/slider-track
  283. {:class "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"})
  284. (shui/tooltip-provider
  285. (shui/tooltip
  286. (shui/tooltip-trigger
  287. {:as-child true
  288. :on-click (fn [e] (.preventDefault e))}
  289. (shui/slider-thumb
  290. {:ref set-thumb-ref!
  291. :class "block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none"}))
  292. (shui/tooltip-content
  293. {:onPointerDownOutside (fn [e] (.preventDefault e))}
  294. (str "Highlight recent blocks"
  295. (when (not= recent-days 0)
  296. (str ": " recent-days " days ago")))))))
  297. (shui/button
  298. {:variant :ghost
  299. :size :sm
  300. :title "Quit highlight recent blocks"
  301. :class "opacity-50 hover:opacity-100"
  302. :on-click (fn [] (state/toggle-highlight-recent-blocks!))}
  303. (ui/icon "x" {:size 16}))]))
  304. (rum/defc recent-slider < rum/reactive
  305. {:will-update (fn [state]
  306. (when-not @(:ui/toggle-highlight-recent-blocks? @state/state)
  307. (clear-recent-highlight!))
  308. state)}
  309. []
  310. (when (state/sub :ui/toggle-highlight-recent-blocks?)
  311. (recent-slider-inner)))
  312. (rum/defc ^:large-vars/cleanup-todo header < rum/reactive
  313. [{:keys [current-repo default-home new-block-mode]}]
  314. (let [_ (state/sub [:user/info :UserGroups])
  315. electron-mac? (and util/mac? (util/electron?))
  316. left-menu (left-menu-button {:on-click (fn []
  317. (state/set-left-sidebar-open!
  318. (not (:ui/left-sidebar-open? @state/state))))})
  319. custom-home-page? (and (state/custom-home-page?)
  320. (= (state/sub-default-home-page) (state/get-current-page)))]
  321. [:div.cp__header.drag-region#head
  322. {:class (util/classnames [{:electron-mac electron-mac?
  323. :native-ios (mobile-util/native-ios?)
  324. :native-android (mobile-util/native-android?)}])
  325. :on-double-click (fn [^js e]
  326. (when-let [target (.-target e)]
  327. (cond
  328. (and (util/electron?)
  329. (.. target -classList (contains "drag-region")))
  330. (js/window.apis.toggleMaxOrMinActiveWindow)
  331. (mobile-util/native-platform?)
  332. (util/scroll-to-top true))))
  333. :style {:fontSize 50}}
  334. [:div.l.flex.items-center.drag-region
  335. [left-menu
  336. (if (mobile-util/native-platform?)
  337. ;; back button for mobile
  338. (when-not (or (state/home?) custom-home-page? (state/whiteboard-dashboard?))
  339. (ui/with-shortcut :go/backward "bottom"
  340. [:button.it.navigation.nav-left.button.icon.opacity-70
  341. {:title (t :header/go-back) :on-click #(js/window.history.back)}
  342. (ui/icon "chevron-left" {:size 26})]))
  343. ;; search button for non-mobile
  344. (when current-repo
  345. (ui/with-shortcut :go/search "right"
  346. [:button.button.icon#search-button
  347. {:title (t :header/search)
  348. :on-click #(do (when (or (mobile-util/native-android?)
  349. (mobile-util/native-iphone?))
  350. (state/set-left-sidebar-open! false))
  351. (state/pub-event! [:go/search]))}
  352. (ui/icon "search" {:size ui/icon-size})])))]]
  353. [:div.r.flex.drag-region
  354. (when (and current-repo
  355. (ldb/get-graph-rtc-uuid (db/get-db))
  356. (user-handler/logged-in?)
  357. (config/db-based-graph? current-repo)
  358. (user-handler/team-member?))
  359. [:<>
  360. (recent-slider)
  361. (rum/with-key (rtc-collaborators)
  362. (str "collab-" current-repo))
  363. (rtc-indicator/indicator)])
  364. (when (and current-repo
  365. (not (config/demo-graph? current-repo))
  366. (not (config/db-based-graph? current-repo))
  367. (user-handler/alpha-or-beta-user?))
  368. (fs-sync/indicator))
  369. (when (and (not= (state/get-current-route) :home)
  370. (not custom-home-page?))
  371. (home-button))
  372. (when config/lsp-enabled?
  373. [:<>
  374. (plugins/hook-ui-items :toolbar)
  375. (plugins/updates-notifications)])
  376. (when (state/feature-http-server-enabled?)
  377. (server/server-indicator (state/sub :electron/server)))
  378. (when (util/electron?)
  379. (back-and-forward))
  380. (when-not (mobile-util/native-platform?)
  381. (new-block-mode))
  382. (when config/publishing?
  383. [:a.text-sm.font-medium.button {:href (rfe/href :graph)}
  384. (t :graph)])
  385. (toolbar-dots-menu {:t t
  386. :current-repo current-repo
  387. :default-home default-home})
  388. (sidebar/toggle)
  389. (updater-tips-new-version t)]]))