<commit_before>(ns frontend.components.overlay
  (:require [cemerick.url :as url]
            [clojure.set :as set]
            [clojure.string :as str]
            [datascript :as d]
            [frontend.analytics :as analytics]
            [frontend.async :refer [put!]]
            [frontend.auth :as auth]
            [frontend.components.common :as common]
            [frontend.components.doc-viewer :as doc-viewer]
            [frontend.components.document-access :as document-access]
            [frontend.components.permissions :as permissions]
            [frontend.components.team :as team]
            [frontend.datascript :as ds]
            [frontend.models.doc :as doc-model]
            [frontend.state :as state]
            [frontend.urls :as urls]
            [frontend.utils :as utils :include-macros true]
            [frontend.utils.date :refer (date->bucket)]
            [goog.dom]
            [goog.dom.Range]
            [goog.dom.selection]
            [goog.labs.userAgent.browser :as ua]
            [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true])
  (:require-macros [sablono.core :refer (html)])
  (:import [goog.ui IdGenerator]))

(defn share-url-input [app owner]
  (reify
    om/IRender
    (render [_]
      (html
       [:div.make
        [:form.menu-invite-form.make
         [:input {:type "text"
                  :required "true"
                  :data-adaptive ""
                  :onMouseDown (fn [e]
                                 (.focus (.-target e))
                                 (goog.dom.selection/setStart (.-target e) 0)
                                 (goog.dom.selection/setEnd (.-target e) 10000)
                                 (utils/stop-event e))
                  :value (urls/absolute-doc-url (:document/id app))}]
         [:label {:data-placeholder "Copy the url to share"}]]]))))

(defn auth-link [app owner {:keys [source] :as opts}]
  (reify
    om/IDisplayName (display-name [_] "Overlay Auth Link")
    om/IRender
    (render [_]
      (let [cast! (om/get-shared owner :cast!)
            login-button-learned? (get-in app state/login-button-learned-path)]
        (html
         (if (:cust app)
           [:form.vein.make.stick
            {:method "post"
             :action "/logout"
             :ref "logout-form"
             :role "button"}
            [:input
             {:type "hidden"
              :name "__anti-forgery-token"
              :value (utils/csrf-token)}]
            [:input
             {:type "hidden"
              :name "redirect-to"
              :value (-> (.-location js/window)
                         (.-href)
                         (url/url)
                         :path)}]
            [:a.vein.make
             {:on-click         #(.submit (om/get-node owner "logout-form"))
              :on-touch-end #(do (.submit (om/get-node owner "logout-form")) (.preventDefault %))
              :role "button"}
             (common/icon :logout)
             [:span "Log out"]]]

           [:a.vein.make.stick
            {:href (auth/auth-url :source source)
             :role "button"}
            (common/icon :login)
            [:span "Log in"]]))))))

(defn start [app owner]
  (reify
    om/IDisplayName (display-name [_] "Overlay Start")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when-let [chat-datoms (seq (filter #(= :document/privacy (:a %))
                                                       (:tx-data tx-report)))]
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc (doc-model/find-by-id @db (:document/id app))]
        (html
          [:div.menu-view
           [:div.veins
            [:a.vein.make
             {:on-click         #(cast! :overlay-info-toggled)
              :on-touch-end #(do (cast! :overlay-info-toggled) (.preventDefault %))
              :role "button"}
             (common/icon :info)
             [:span "About"]]
            [:a.vein.make
             {:href "/new"
              :role "button"}
             (common/icon :newdoc)
             [:span "New Document"]]
            [:a.vein.make
             {:on-click         #(cast! :your-docs-opened)
              :on-touch-end #(do (cast! :your-docs-opened) (.preventDefault %))
              :role "button"}
             (common/icon :clock)
             [:span "Your Documents"]]
            ;; TODO: should this use the permissions model? Would have to send some
            ;;       info about the document
            (if (auth/has-document-access? app (:document/id app))
              [:a.vein.make
               {:on-click         #(cast! :sharing-menu-opened)
                :on-touch-end #(do (cast! :sharing-menu-opened) (.preventDefault %))
                :role "button"}
               (common/icon :share)
               [:span "Sharing"]]

              [:a.vein.make
               {:on-click         #(cast! :document-permissions-opened)
                :on-touch-end #(do (cast! :document-permissions-opened) (.preventDefault %))
                :role "button"}
               (common/icon :users)
               [:span "Request Access"]])
            [:a.vein.make
             {:on-click         #(cast! :shortcuts-menu-opened)
              :on-touch-end #(do (cast! :shortcuts-menu-opened) (.preventDefault %))
              :class "mobile-hidden"
              :role "button"}
             (common/icon :command)
             [:span "Shortcuts"]]
             (om/build auth-link app {:opts {:source "start-overlay"}})
            ]])))))

(defn team-start [app owner]
  (reify
    om/IDisplayName (display-name [_] "Overlay Team Start")
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc (doc-model/find-by-id @db (:document/id app))]
        (html
          [:div.menu-view
           [:div.veins
            [:a.vein.make
             {:on-click #(cast! :team-settings-opened)
              :role "button"}
             (common/icon :share)
             [:span "Permissions"]]
            [:a.vein.make
             {:on-click #(cast! :team-docs-opened)
              :role "button"}
             (common/icon :clock)
             [:span "Team Documents"]]
            ]])))))

(defn private-sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Private Sharing")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :document/privacy (:a %))
                                           (= :permission/document (:a %))
                                           (= :access-grant/document (:a %))
                                           (= :access-request/document (:a %))
                                           (= :access-request/status (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc-id (:document/id app)
            doc (doc-model/find-by-id @db doc-id)
            permission-grant-email (get-in app state/permission-grant-email-path)
            permissions (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :permission/document ?doc-id]] @db doc-id)
            access-grants (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-grant/document ?doc-id]] @db doc-id)
            access-requests (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-request/document ?doc-id]] @db doc-id)]
        (html
          [:div.content
           [:h2.make
            "This document is private."]
           [:p.make
            "It's only visible to users with access."
            " Add your teammate's email to grant them access."]
           [:form.menu-invite-form.make
            {:on-submit #(do (cast! :permission-grant-submitted)
                           false)
             :on-key-down #(when (= "Enter" (.-key %))
                             (cast! :permission-grant-submitted)
                             false)}
            [:input
             {:type "text"
              :required "true"
              :data-adaptive ""
              :value (or permission-grant-email "")
              :on-change #(cast! :permission-grant-email-changed {:value (.. % -target -value)})}]
            [:label
             {:data-placeholder "Teammate's email"
              :data-placeholder-nil "What's your teammate's email?"
              :data-placeholder-forgot "Don't forget to submit!"}]]
           (for [access-entity (sort-by (comp - :db/id) (concat permissions access-grants access-requests))]
             (permissions/render-access-entity access-entity cast!))])))))

(defn read-only-sharing-for-admin [app owner]
  (reify
    om/IDisplayName (display-name [_] "Read-Only Sharing (admin)")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :document/privacy (:a %))
                                           (= :permission/document (:a %))
                                           (= :access-grant/document (:a %))
                                           (= :access-request/document (:a %))
                                           (= :access-request/status (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc-id (:document/id app)
            doc (doc-model/find-by-id @db doc-id)
            permission-grant-email (get-in app state/permission-grant-email-path)
            permissions (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :permission/document ?doc-id]] @db doc-id)
            access-grants (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-grant/document ?doc-id]] @db doc-id)
            access-requests (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-request/document ?doc-id]] @db doc-id)]
        (html
         [:div.content
          [:h2.make
           "This document is read-only."]

          [:p.make
           "Anyone with the url can see the doc and chat, but can't edit the canvas. "
           "Share the url to show off your work."]
          (om/build share-url-input app)

          [:p.make
           "Add your teammate's email to grant them full access."]
          [:form.menu-invite-form.make
           {:on-submit #(do (cast! :permission-grant-submitted)
                            false)
            :on-key-down #(when (= "Enter" (.-key %))
                            (cast! :permission-grant-submitted)
                            false)}
           [:input
            {:type "text"
             :required "true"
             :data-adaptive ""
             :value (or permission-grant-email "")
             :on-change #(cast! :permission-grant-email-changed {:value (.. % -target -value)})}]
           [:label
            {:data-placeholder "Teammate's email"
             :data-placeholder-nil "What's your teammate's email?"
             :data-placeholder-forgot "Don't forget to submit!"}]]
          (for [access-entity (sort-by (comp - :db/id) (concat permissions access-grants access-requests))]
            (permissions/render-access-entity access-entity cast!))])))))

(defn read-only-sharing-for-viewer [app owner]
  (reify
    om/IDisplayName (display-name [_] "Read-Only sharing (viewer)")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :access-request/document (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [db cast!]} (om/get-shared owner)
            doc-id (:document/id app)
            document (d/entity @db doc-id)
            access-requests (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-request/document ?doc-id]] @db doc-id)]
        (html
         [:div.content
          [:h2.make
           "This document is read-only."]
          (if (:cust app)
            (list
             [:p.make
              "You can view this doc and chat, but anything you prototype here will only be visible to you."]
             (if (empty? access-requests)
               (list
                [:p.make
                 "You can try to request full access or even "
                 [:a {:href "/new"} "create your own"]
                 " document."]
                [:a.make.menu-cta
                 {:on-click #(cast! :permission-requested {:doc-id doc-id})
                  :role "button"}
                 "Request Access"])
               [:p.make
                [:span
                 "Okay, we notified the owner of this document about your request. "
                 "While you wait for a response, try prototyping in "]
                [:a {:href "/new"} "your own document"]
                [:span "."]]))
            (list
             [:p.make
              "You can view this doc and chat, but anything you prototype here won't save. "
              "If you sign in with Google you can request full access from the owner of this document."]
             [:div.calls-to-action.make
              (om/build common/google-login {:source "Permission Denied Menu"})]))])))))

(defn read-only-sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Read-Only Sharing")
    om/IRender
    (render [_]
      (if (auth/contains-scope? auth/scope-heirarchy (:max-document-scope app) :admin)
        (om/build read-only-sharing-for-admin app)
        (om/build read-only-sharing-for-viewer app)))))

(defn public-sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Public Sharing")
    om/IRender
    (render [_]
      (let [cast! (om/get-shared owner :cast!)
            invite-to (or (get-in app state/invite-to-path) "")]
        (html
          [:div.content
           [:h2.make
            "This document is public."]
           (if-not (:cust app)
             (list
               [:p.make
                "It's visible to anyone with the url.
                Sign in with your Google account to send an invite or give someone your url."]
               [:div.calls-to-action.make
                (om/build common/google-login {:source "Public Sharing Menu"})])

             (list
               [:p.make
                "Anyone with the url can view and edit."]

               (om/build share-url-input app)

               [:p.make
                "Email or text a friend to invite them to collaborate:"]
               [:form.menu-invite-form.make
                {:on-submit #(do (cast! :invite-submitted)
                               false)
                 :on-key-down #(when (= "Enter" (.-key %))
                                 (cast! :invite-submitted)
                                 false)}
                [:input
                 {:type "text"
                  :required "true"
                  :data-adaptive ""
                  :value (or invite-to "")
                  :on-change #(cast! :invite-to-changed {:value (.. % -target -value)})}]
                [:label
                 {:data-placeholder (cond (pos? (.indexOf invite-to "@"))
                                          "Email address"
                                          (= 10 (count (str/replace invite-to #"[^0-9]" "")))
                                          "Phone number"
                                          :else "Email address or phone number")
                  :data-placeholder-nil "Email address or phone number?"
                  :data-placeholder-forgot "Don't forget to submit!"}]]))
           (when-let [response (first (get-in app (state/invite-responses-path (:document/id app))))]
             [:div response])
           ;; TODO: keep track of invites
           ])))))

(defn sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Overlay Sharing")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :document/privacy (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc-id (:document/id app)
            doc (doc-model/find-by-id @db doc-id)
            cant-edit-reason (cond (:team app)
                                   nil

                                   (not (contains? (get-in app [:cust :flags]) :flags/private-docs))
                                   :no-private-docs-flag

                                   (not (auth/owner? @db doc (get-in app [:cust])))
                                   :not-creator)]
        (html
         [:div.menu-view
          (case (:document/privacy doc)
            :document.privacy/public (om/build public-sharing app)
            :document.privacy/private (om/build private-sharing app)
            ;;read-only sharing needs to know the user's permission for the doc
            :document.privacy/read-only (om/build read-only-sharing app)
            nil)

          (case cant-edit-reason
            :no-private-docs-flag
            [:a.vein.make.stick.external {:href "/pricing"}
             [:span "Need private docs? Start a free trial."]
             (common/icon :arrow-right)]


            [:form.privacy-select.vein.make.stick
             [:input.privacy-radio {:type "radio"
                                    :hidden "true"
                                    :id "privacy-public"
                                    :name "privacy"
                                    :checked (keyword-identical? :document.privacy/public (:document/privacy doc))
                                    :disabled (boolean cant-edit-reason)
                                    :onChange #(if cant-edit-reason
                                                 (utils/stop-event %)
                                                 (cast! :document-privacy-changed
                                                        {:doc-id doc-id
                                                         :setting :document.privacy/public}))}]
             [:label.privacy-label {:class (when cant-edit-reason "disabled")
                                    :title (when (= :not-creator cant-edit-reason) "You must be the owner of this doc to change its privacy.")
                                    :for "privacy-public"
                                    :role "button"}
              (common/icon :public)
              [:span "Public"]
              (when (= :not-creator cant-edit-reason)
                [:small "(privacy change requires owner)"])]
             [:input.privacy-radio {:type "radio"
                                    :hidden "true"
                                    :id "privacy-read-only"<commit_msg>don't let replay break the sharing menu<commit_after>(ns frontend.components.overlay
  (:require [cemerick.url :as url]
            [clojure.set :as set]
            [clojure.string :as str]
            [datascript :as d]
            [frontend.analytics :as analytics]
            [frontend.async :refer [put!]]
            [frontend.auth :as auth]
            [frontend.components.common :as common]
            [frontend.components.doc-viewer :as doc-viewer]
            [frontend.components.document-access :as document-access]
            [frontend.components.permissions :as permissions]
            [frontend.components.team :as team]
            [frontend.datascript :as ds]
            [frontend.models.doc :as doc-model]
            [frontend.state :as state]
            [frontend.urls :as urls]
            [frontend.utils :as utils :include-macros true]
            [frontend.utils.date :refer (date->bucket)]
            [goog.dom]
            [goog.dom.Range]
            [goog.dom.selection]
            [goog.labs.userAgent.browser :as ua]
            [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true])
  (:require-macros [sablono.core :refer (html)])
  (:import [goog.ui IdGenerator]))

(defn share-url-input [app owner]
  (reify
    om/IRender
    (render [_]
      (html
       [:form.menu-invite-form.make
        [:input {:type "text"
                 :required "true"
                 :data-adaptive ""
                 :onMouseDown (fn [e]
                                (.focus (.-target e))
                                (goog.dom.selection/setStart (.-target e) 0)
                                (goog.dom.selection/setEnd (.-target e) 10000)
                                (utils/stop-event e))
                 :value (urls/absolute-doc-url (:document/id app))}]
        [:label {:data-placeholder "Copy the url to share"}]]))))

(defn auth-link [app owner {:keys [source] :as opts}]
  (reify
    om/IDisplayName (display-name [_] "Overlay Auth Link")
    om/IRender
    (render [_]
      (let [cast! (om/get-shared owner :cast!)
            login-button-learned? (get-in app state/login-button-learned-path)]
        (html
         (if (:cust app)
           [:form.vein.make.stick
            {:method "post"
             :action "/logout"
             :ref "logout-form"
             :role "button"}
            [:input
             {:type "hidden"
              :name "__anti-forgery-token"
              :value (utils/csrf-token)}]
            [:input
             {:type "hidden"
              :name "redirect-to"
              :value (-> (.-location js/window)
                         (.-href)
                         (url/url)
                         :path)}]
            [:a.vein.make
             {:on-click         #(.submit (om/get-node owner "logout-form"))
              :on-touch-end #(do (.submit (om/get-node owner "logout-form")) (.preventDefault %))
              :role "button"}
             (common/icon :logout)
             [:span "Log out"]]]

           [:a.vein.make.stick
            {:href (auth/auth-url :source source)
             :role "button"}
            (common/icon :login)
            [:span "Log in"]]))))))

(defn start [app owner]
  (reify
    om/IDisplayName (display-name [_] "Overlay Start")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when-let [chat-datoms (seq (filter #(= :document/privacy (:a %))
                                                       (:tx-data tx-report)))]
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc (doc-model/find-by-id @db (:document/id app))]
        (html
          [:div.menu-view
           [:div.veins
            [:a.vein.make
             {:on-click         #(cast! :overlay-info-toggled)
              :on-touch-end #(do (cast! :overlay-info-toggled) (.preventDefault %))
              :role "button"}
             (common/icon :info)
             [:span "About"]]
            [:a.vein.make
             {:href "/new"
              :role "button"}
             (common/icon :newdoc)
             [:span "New Document"]]
            [:a.vein.make
             {:on-click         #(cast! :your-docs-opened)
              :on-touch-end #(do (cast! :your-docs-opened) (.preventDefault %))
              :role "button"}
             (common/icon :clock)
             [:span "Your Documents"]]
            ;; TODO: should this use the permissions model? Would have to send some
            ;;       info about the document
            (if (auth/has-document-access? app (:document/id app))
              [:a.vein.make
               {:on-click         #(cast! :sharing-menu-opened)
                :on-touch-end #(do (cast! :sharing-menu-opened) (.preventDefault %))
                :role "button"}
               (common/icon :share)
               [:span "Sharing"]]

              [:a.vein.make
               {:on-click         #(cast! :document-permissions-opened)
                :on-touch-end #(do (cast! :document-permissions-opened) (.preventDefault %))
                :role "button"}
               (common/icon :users)
               [:span "Request Access"]])
            [:a.vein.make
             {:on-click         #(cast! :shortcuts-menu-opened)
              :on-touch-end #(do (cast! :shortcuts-menu-opened) (.preventDefault %))
              :class "mobile-hidden"
              :role "button"}
             (common/icon :command)
             [:span "Shortcuts"]]
             (om/build auth-link app {:opts {:source "start-overlay"}})
            ]])))))

(defn team-start [app owner]
  (reify
    om/IDisplayName (display-name [_] "Overlay Team Start")
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc (doc-model/find-by-id @db (:document/id app))]
        (html
          [:div.menu-view
           [:div.veins
            [:a.vein.make
             {:on-click #(cast! :team-settings-opened)
              :role "button"}
             (common/icon :share)
             [:span "Permissions"]]
            [:a.vein.make
             {:on-click #(cast! :team-docs-opened)
              :role "button"}
             (common/icon :clock)
             [:span "Team Documents"]]
            ]])))))

(defn private-sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Private Sharing")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :document/privacy (:a %))
                                           (= :permission/document (:a %))
                                           (= :access-grant/document (:a %))
                                           (= :access-request/document (:a %))
                                           (= :access-request/status (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc-id (:document/id app)
            doc (doc-model/find-by-id @db doc-id)
            permission-grant-email (get-in app state/permission-grant-email-path)
            permissions (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :permission/document ?doc-id]] @db doc-id)
            access-grants (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-grant/document ?doc-id]] @db doc-id)
            access-requests (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-request/document ?doc-id]] @db doc-id)]
        (html
          [:div.content
           [:h2.make
            "This document is private."]
           [:p.make
            "It's only visible to users with access."
            " Add your teammate's email to grant them access."]
           [:form.menu-invite-form.make
            {:on-submit #(do (cast! :permission-grant-submitted)
                           false)
             :on-key-down #(when (= "Enter" (.-key %))
                             (cast! :permission-grant-submitted)
                             false)}
            [:input
             {:type "text"
              :required "true"
              :data-adaptive ""
              :value (or permission-grant-email "")
              :on-change #(cast! :permission-grant-email-changed {:value (.. % -target -value)})}]
            [:label
             {:data-placeholder "Teammate's email"
              :data-placeholder-nil "What's your teammate's email?"
              :data-placeholder-forgot "Don't forget to submit!"}]]
           (for [access-entity (sort-by (comp - :db/id) (concat permissions access-grants access-requests))]
             (permissions/render-access-entity access-entity cast!))])))))

(defn read-only-sharing-for-admin [app owner]
  (reify
    om/IDisplayName (display-name [_] "Read-Only Sharing (admin)")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :document/privacy (:a %))
                                           (= :permission/document (:a %))
                                           (= :access-grant/document (:a %))
                                           (= :access-request/document (:a %))
                                           (= :access-request/status (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc-id (:document/id app)
            doc (doc-model/find-by-id @db doc-id)
            permission-grant-email (get-in app state/permission-grant-email-path)
            permissions (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :permission/document ?doc-id]] @db doc-id)
            access-grants (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-grant/document ?doc-id]] @db doc-id)
            access-requests (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-request/document ?doc-id]] @db doc-id)]
        (html
         [:div.content
          [:h2.make
           "This document is read-only."]

          [:p.make
           "Anyone with the url can see the doc and chat, but can't edit the canvas. "
           "Share the url to show off your work."]
          (om/build share-url-input app)

          [:p.make
           "Add your teammate's email to grant them full access."]
          [:form.menu-invite-form.make
           {:on-submit #(do (cast! :permission-grant-submitted)
                            false)
            :on-key-down #(when (= "Enter" (.-key %))
                            (cast! :permission-grant-submitted)
                            false)}
           [:input
            {:type "text"
             :required "true"
             :data-adaptive ""
             :value (or permission-grant-email "")
             :on-change #(cast! :permission-grant-email-changed {:value (.. % -target -value)})}]
           [:label
            {:data-placeholder "Teammate's email"
             :data-placeholder-nil "What's your teammate's email?"
             :data-placeholder-forgot "Don't forget to submit!"}]]
          (for [access-entity (sort-by (comp - :db/id) (concat permissions access-grants access-requests))]
            (permissions/render-access-entity access-entity cast!))])))))

(defn read-only-sharing-for-viewer [app owner]
  (reify
    om/IDisplayName (display-name [_] "Read-Only sharing (viewer)")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :access-request/document (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [db cast!]} (om/get-shared owner)
            doc-id (:document/id app)
            document (d/entity @db doc-id)
            access-requests (ds/touch-all '[:find ?t :in $ ?doc-id :where [?t :access-request/document ?doc-id]] @db doc-id)]
        (html
         [:div.content
          [:h2.make
           "This document is read-only."]
          (if (:cust app)
            (list
             [:p.make
              "You can view this doc and chat, but anything you prototype here will only be visible to you."]
             (if (empty? access-requests)
               (list
                [:p.make
                 "You can try to request full access or even "
                 [:a {:href "/new"} "create your own"]
                 " document."]
                [:a.make.menu-cta
                 {:on-click #(cast! :permission-requested {:doc-id doc-id})
                  :role "button"}
                 "Request Access"])
               [:p.make
                [:span
                 "Okay, we notified the owner of this document about your request. "
                 "While you wait for a response, try prototyping in "]
                [:a {:href "/new"} "your own document"]
                [:span "."]]))
            (list
             [:p.make
              "You can view this doc and chat, but anything you prototype here won't save. "
              "If you sign in with Google you can request full access from the owner of this document."]
             [:div.calls-to-action.make
              (om/build common/google-login {:source "Permission Denied Menu"})]))])))))

(defn read-only-sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Read-Only Sharing")
    om/IRender
    (render [_]
      (if (auth/contains-scope? auth/scope-heirarchy (:max-document-scope app) :admin)
        (om/build read-only-sharing-for-admin app)
        (om/build read-only-sharing-for-viewer app)))))

(defn public-sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Public Sharing")
    om/IRender
    (render [_]
      (let [cast! (om/get-shared owner :cast!)
            invite-to (or (get-in app state/invite-to-path) "")]
        (html
          [:div.content
           [:h2.make
            "This document is public."]
           (if-not (:cust app)
             (list
               [:p.make
                "It's visible to anyone with the url.
                Sign in with your Google account to send an invite or give someone your url."]
               [:div.calls-to-action.make
                (om/build common/google-login {:source "Public Sharing Menu"})])

             (list
               [:p.make
                "Anyone with the url can view and edit."]

               (om/build share-url-input app)

               [:p.make
                "Email or text a friend to invite them to collaborate:"]
               [:form.menu-invite-form.make
                {:on-submit #(do (cast! :invite-submitted)
                               false)
                 :on-key-down #(when (= "Enter" (.-key %))
                                 (cast! :invite-submitted)
                                 false)}
                [:input
                 {:type "text"
                  :required "true"
                  :data-adaptive ""
                  :value (or invite-to "")
                  :on-change #(cast! :invite-to-changed {:value (.. % -target -value)})}]
                [:label
                 {:data-placeholder (cond (pos? (.indexOf invite-to "@"))
                                          "Email address"
                                          (= 10 (count (str/replace invite-to #"[^0-9]" "")))
                                          "Phone number"
                                          :else "Email address or phone number")
                  :data-placeholder-nil "Email address or phone number?"
                  :data-placeholder-forgot "Don't forget to submit!"}]]))
           (when-let [response (first (get-in app (state/invite-responses-path (:document/id app))))]
             [:div response])
           ;; TODO: keep track of invites
           ])))))

(defn unknown-sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Unknown Sharing")
    om/IRender
    (render [_]
      (let [cast! (om/get-shared owner :cast!)
            invite-to (or (get-in app state/invite-to-path) "")])
      (html
       [:div.content
        [:h2.make "Share this document"]
        (if-not (:cust app)
          (list
           [:p.make
            "Sign in with your Google account to send an invite or give someone your url."]
           [:div.calls-to-action.make
            (om/build common/google-login {:source "Public Sharing Menu"})])

          (om/build share-url-input app))]))))

(defn sharing [app owner]
  (reify
    om/IDisplayName (display-name [_] "Overlay Sharing")
    om/IInitState (init-state [_] {:listener-key (.getNextUniqueId (.getInstance IdGenerator))})
    om/IDidMount
    (did-mount [_]
      (d/listen! (om/get-shared owner :db)
                 (om/get-state owner :listener-key)
                 (fn [tx-report]
                   ;; TODO: better way to check if state changed
                   (when (seq (filter #(or (= :document/privacy (:a %)))
                                      (:tx-data tx-report)))
                     (om/refresh! owner)))))
    om/IWillUnmount
    (will-unmount [_]
      (d/unlisten! (om/get-shared owner :db) (om/get-state owner :listener-key)))
    om/IRender
    (render [_]
      (let [{:keys [cast! db]} (om/get-shared owner)
            doc-id (:document/id app)
            doc (doc-model/find-by-id @db doc-id)
            cant-edit-reason (cond (:team app)
                                   nil

                                   (not (contains? (get-in app [:cust :flags]) :flags/private-docs))
                                   :no-private-docs-flag

                                   (not (auth/owner? @db doc (get-in app [:cust])))
                                   :not-creator)]
        (html
         [:div.menu-view
          (case (:document/privacy doc)
            :document.privacy/public (om/build public-sharing app)
            :document.privacy/private (om/build private-sharing app)
            ;;read-only sharing needs to know the user's permission for the doc
            :document.privacy/read-only (om/build read-only-sharing app)
            (om/build unknown-sharing app))

          (case cant-edit-reason
            :no-private-docs-flag
            [:a.vein.make.stick.external {:href "/pricing"}
             [:span "Need private docs? Start a free trial."]
             (common/icon :arrow-right)]


            [:form.privacy-select.vein.make.stick
             [:input.privacy-radio {:type "radio"
                                    :hidden "true"
                                    :id "privacy-public"
                                    :name "privacy"
                                    :checked (keyword-identical? :document.privacy/public (:document/privacy doc))
                                    :disabled (boolean cant-edit-reason)
                                    :onChange #(if cant-edit-reason
                                                 (utils/stop-event %)
                                                 (cast! :document-privacy-changed
                                                        {:doc-id doc-id
                                                         :setting :document.privacy/public}))}]
             [:label.privacy-label {:class (when cant-edit-reason "disabled")
                                    :title (when (= :not-creator cant-edit-reason) "You must be the owner of this doc to change its privacy.")
                                    :for "privacy-public"
                                    :role "button"}
              (common/icon :public)
              [:span "Public"]
              (when (= :not-creator cant-edit-reason)
                [:small "(privacy change requires owner)"])]
             [:input.privacy-radio {:type "radio"
                                    :hidden "true"
                                    :id "privacy-read-only"
{'fraction_non_alphanumeric': 0.13708293164664112, 'fraction_numerical': 0.0006818876531308048, 'mean_word_length': 1.4071768168440117, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 118, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2754035', 'n_tokens_mistral': 12490, 'n_tokens_neox': 11763, 'n_words': 2971}