diff --git a/src/cljs/athens/events/remote.cljs b/src/cljs/athens/events/remote.cljs index 1c6fac5185..4e65e78317 100644 --- a/src/cljs/athens/events/remote.cljs +++ b/src/cljs/athens/events/remote.cljs @@ -307,3 +307,11 @@ ;; Remove the server event after everything is done. true (into [[:remote/clear-server-event event]]))]]}))) + +;; Subs + +(rf/reg-sub + :remote/event-sync-memory-log + (fn [db _] + (when-let [event-sync (:remote/event-sync db)] + (event-sync/stage-log event-sync :memory)))) diff --git a/src/cljs/athens/router.cljs b/src/cljs/athens/router.cljs index ebc2a84d9d..3943195fe3 100644 --- a/src/cljs/athens/router.cljs +++ b/src/cljs/athens/router.cljs @@ -156,6 +156,7 @@ (def routes ["/" ["" {:name :home}] + ["quick-capture" {:name :quickcapture}] ["settings" {:name :settings}] ["pages" {:name :pages}] ["page-t/:title" {:name :page-by-title}] diff --git a/src/cljs/athens/views.cljs b/src/cljs/athens/views.cljs index 7b7652a74f..8c1a546b9d 100644 --- a/src/cljs/athens/views.cljs +++ b/src/cljs/athens/views.cljs @@ -13,8 +13,10 @@ [athens.views.app-toolbar :as app-toolbar] [athens.views.athena :refer [athena-component]] [athens.views.help :refer [help-popup]] + [athens.views.hoc.perf-mon :as perf-mon] [athens.views.left-sidebar :as left-sidebar] [athens.views.pages.core :as pages] + [athens.views.pages.quick-capture :as quick-capture] [athens.views.pages.settings :as settings] [athens.views.right-sidebar.core :as right-sidebar] [re-frame.core :as rf])) @@ -34,6 +36,7 @@ [] (let [loading (rf/subscribe [:loading?]) modal (rf/subscribe [:modal]) + route-name (rf/subscribe [:current-route/name]) right-sidebar-open? (rf/subscribe [:right-sidebar/open]) right-sidebar-width (rf/subscribe [:right-sidebar/width]) settings-open? (rf/subscribe [:settings/open?])] @@ -42,52 +45,57 @@ (zoom)) [:> ChakraProvider {:theme theme, :bg "background.basement"} - [:> ContextMenuProvider - [:> LayoutProvider - [help-popup] - [alert] - [athena-component] - (cond - (and @loading @modal) [db-modal/window] + [:> LayoutProvider + [:> ContextMenuProvider + (if + (= @route-name :quickcapture) + [perf-mon/hoc-perfmon-no-new-tx {:span-name "quick-capture"} + [:f> quick-capture/quick-capture]] + [:<> + [help-popup] + [alert] + [athena-component] + (cond + (and @loading @modal) [db-modal/window] - @loading - [:> Center {:height "100vh"} - [:> Flex {:width 28 - :flexDirection "column" - :gap 2 - :color "foreground.secondary" - :borderRadius "lg" - :placeItems "center" - :placeContent "center" - :height 28} - [:> Spinner {:size "xl"}]]] + @loading + [:> Center {:height "100vh"} + [:> Flex {:width 28 + :flexDirection "column" + :gap 2 + :color "foreground.secondary" + :borderRadius "lg" + :placeItems "center" + :placeContent "center" + :height 28} + [:> Spinner {:size "xl"}]]] - :else [:<> - (when @modal - [db-modal/window]) - (when @settings-open? - [settings/page]) - [:> VStack {:overscrollBehavior "contain" - :id "main-layout" - :spacing 0 - :overflowY "auto" - :height "100vh" - :bg "background.floor" - :transitionDuration "fast" - :transitionProperty "background" - :transitionTimingFunction "ease-in-out" - :align "stretch" - :position "relative"} - [app-toolbar/app-toolbar] - [:> HStack {:overscrollBehavior "contain" - :align "stretch" - :spacing 0 - :flex 1} - [left-sidebar/left-sidebar] - [:> MainContent {:rightSidebarWidth @right-sidebar-width - :isRightSidebarOpen @right-sidebar-open?} - [pages/view]] - [:> RightSidebarResizeControl {:rightSidebarWidth @right-sidebar-width - :isRightSidebarOpen @right-sidebar-open? - :onResizeSidebar #(rf/dispatch [:right-sidebar/set-width %])}] - [right-sidebar/right-sidebar]]]])]]]]))) + :else [:<> + (when @modal + [db-modal/window]) + (when @settings-open? + [settings/page]) + [:> VStack {:overscrollBehavior "contain" + :id "main-layout" + :spacing 0 + :overflowY "auto" + :height "100vh" + :bg "background.floor" + :transitionDuration "fast" + :transitionProperty "background" + :transitionTimingFunction "ease-in-out" + :align "stretch" + :position "relative"} + [app-toolbar/app-toolbar] + [:> HStack {:overscrollBehavior "contain" + :align "stretch" + :spacing 0 + :flex 1} + [left-sidebar/left-sidebar] + [:> MainContent {:rightSidebarWidth @right-sidebar-width + :isRightSidebarOpen @right-sidebar-open?} + [pages/view]] + [:> RightSidebarResizeControl {:rightSidebarWidth @right-sidebar-width + :isRightSidebarOpen @right-sidebar-open? + :onResizeSidebar #(rf/dispatch [:right-sidebar/set-width %])}] + [right-sidebar/right-sidebar]]]])])]]]]))) diff --git a/src/cljs/athens/views/pages/core.cljs b/src/cljs/athens/views/pages/core.cljs index 8c39b43c09..a0ba996607 100644 --- a/src/cljs/athens/views/pages/core.cljs +++ b/src/cljs/athens/views/pages/core.cljs @@ -6,6 +6,7 @@ [athens.views.pages.daily-notes :as daily-notes] [athens.views.pages.graph :as graph] [athens.views.pages.page :as page] + [athens.views.pages.quick-capture :as quick-capture] [re-frame.core :as rf])) @@ -20,6 +21,8 @@ :title "Reconnecting to server..."}))) [:<> (case @route-name + :quickcapture [perf-mon/hoc-perfmon-no-new-tx {:span-name "quick-capture"} + [quick-capture/quick-capture]] :pages [perf-mon/hoc-perfmon-no-new-tx {:span-name "pages/all-pages"} [all-pages/page]] :page [perf-mon/hoc-perfmon {:span-name "pages/page"} diff --git a/src/cljs/athens/views/pages/quick_capture.cljs b/src/cljs/athens/views/pages/quick_capture.cljs new file mode 100644 index 0000000000..2029b1e214 --- /dev/null +++ b/src/cljs/athens/views/pages/quick_capture.cljs @@ -0,0 +1,71 @@ +(ns athens.views.pages.quick-capture + (:require + [athens.common-events.graph.ops :as graph-ops] + [athens.common.utils :as utils] + [athens.dates :as dates] + [athens.electron.db-menu.core :refer [db-menu]] + [clojure.data :as data] + ["@chakra-ui/react" :refer [Button]] + ["/components/Quick/QuickCapture" :refer [QuickCapture]] + ["react" :as react] + [reagent.core :as r] + [re-frame.core :as rf])) + + +(defn quick-capture + [] + (let [[notes setNotes] (react/useState []) + [lastSyncTime, setLastSyncTime] (react/useState nil) + memory-log @(rf/subscribe [:remote/event-sync-memory-log]) + unsynced-uids (->> memory-log + (map #(-> % second :event/op :op/consequences first :op/args :block/uid)) + (filter some?) + set) + unsynced-uids-notes (->> notes + (filter #(-> % :isSaved false?)) + (map :uid) + set) + mock-sync (fn [notes] + (prn notes) + (setNotes []) + (setLastSyncTime (js/Date.now))) + mock-add-item (fn [string] + ;; Send via reframe. + (rf/dispatch [:properties/update-in [:node/title (:title (dates/get-day))] + ["last-qc-message"] + ;; TODO: need to support first/last, and deep path positions + #_["Quick Capture" current-username :last] + (fn [db uid] + (let [new-note (merge {:string string :timestamp (js/Date.now) :uid uid} + (when-not memory-log {:isSaved true}))] + ;; Save on local comp state. + (setNotes (conj notes new-note)) + ;; Save on graph. + [(graph-ops/build-block-save-op db uid string)]))]))] + + (when memory-log + (let [[a b] (data/diff unsynced-uids unsynced-uids-notes)] + (when (or a b) + (println notes a b) + (setNotes (map (fn [{:keys [uid] :as x}] + (cond + (and a (a uid)) (assoc x :isSaved false) + (and b (b uid)) (assoc x :isSaved true) + :else x)) + notes))))) + + [:<> [:> QuickCapture {:dbMenu (r/as-element [db-menu]) + :notes notes + :lastSyncTime lastSyncTime + :onAddItem mock-add-item + :newEventId utils/gen-event-id}] + [:> Button {:position "fixed" + :variant "text" + :size "xs" + :left "50%" + :top 5 + :transform "translateX(-50%)" + :onClick mock-sync} "mock update"]])) + +;; state +;; unsaved changes \ No newline at end of file diff --git a/src/js/components/Quick/QuickCapture.tsx b/src/js/components/Quick/QuickCapture.tsx new file mode 100644 index 0000000000..5d60114214 --- /dev/null +++ b/src/js/components/Quick/QuickCapture.tsx @@ -0,0 +1,221 @@ +import React from 'react' +import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Box, Button, FormControl, HStack, Text, Textarea, VStack } from "@chakra-ui/react" +import { CheckmarkCircleFillIcon } from '@/Icons/Icons' +import { AnimatePresence, motion } from 'framer-motion' + +const FloatingInput = (props) => { + const { onSubmit } = props + const [string, setString] = React.useState("") + const inputRef = React.useRef(null) + + const handleSubmit = (e) => { + if (string.length) { + e.preventDefault() + onSubmit(string) + setString("") + inputRef.current.focus() + } + } + + React.useEffect(() => { + inputRef.current.focus() + }, []) + + return + +