diff --git a/syng-im/src/syng_im/android/core.cljs b/syng-im/src/syng_im/android/core.cljs index 9d84388fa2..04481430fb 100644 --- a/syng-im/src/syng_im/android/core.cljs +++ b/syng-im/src/syng_im/android/core.cljs @@ -15,7 +15,7 @@ (def logo-img (js/require "./images/cljs.png")) (defn alert [title] - (.alert (.-Alert js/React) title)) + (.alert (.-Alert js/React) title)) (defn app-root [] (let [greeting (subscribe [:get-greeting])] @@ -24,10 +24,11 @@ [text {:style {:font-size 30 :font-weight "100" :margin-bottom 20 :text-align "center"}} @greeting] [image {:source logo-img :style {:width 80 :height 80 :margin-bottom 30}}] - [touchable-highlight {:style {:background-color "#999" :padding 10 :border-radius 5} + [touchable-highlight {:style {:background-color "#999" :padding 10 :border-radius 5} :on-press #(alert "HELLO!")} [text {:style {:color "white" :text-align "center" :font-weight "bold"}} "press me"]]]))) (defn init [] - (dispatch-sync [:initialize-db]) - (.registerComponent app-registry "SyngIm" #(r/reactify-component app-root))) + (dispatch-sync [:initialize-db]) + (dispatch [:initialize-protocol]) + (.registerComponent app-registry "SyngIm" #(r/reactify-component app-root))) diff --git a/syng-im/src/syng_im/constants.cljs b/syng-im/src/syng_im/constants.cljs new file mode 100644 index 0000000000..d7d0c881cd --- /dev/null +++ b/syng-im/src/syng_im/constants.cljs @@ -0,0 +1,5 @@ +(ns syng-im.constants) + +(def ethereum-rpc-url "http://localhost:8545") + +(def text-content-type "text/plain") \ No newline at end of file diff --git a/syng-im/src/syng_im/db.cljs b/syng-im/src/syng_im/db.cljs index a210edb8a5..2859bd43cc 100644 --- a/syng-im/src/syng_im/db.cljs +++ b/syng-im/src/syng_im/db.cljs @@ -5,4 +5,13 @@ (def schema {:greeting s/Str}) ;; initial state of app-db -(def app-db {:greeting "Hello Clojure in iOS and Android!"}) +(def app-db {:greeting "Hello Clojure in iOS and Android!" + :identity-password "replace-me-with-user-entered-password"}) + + +(def protocol-initialized-path [:protocol-initialized]) +(def simple-store-path [:simple-store]) +(def identity-password-path [:identity-password]) +(def current-chat-id-path [:chat :current-chat-id]) +(defn arrived-message-path [chat-id] + [:chat chat-id :arrived-message-id]) \ No newline at end of file diff --git a/syng-im/src/syng_im/handlers.cljs b/syng-im/src/syng_im/handlers.cljs index adc5f20ad8..859aa85a44 100644 --- a/syng-im/src/syng_im/handlers.cljs +++ b/syng-im/src/syng_im/handlers.cljs @@ -2,7 +2,14 @@ (:require [re-frame.core :refer [register-handler after]] [schema.core :as s :include-macros true] - [syng-im.db :refer [app-db schema]])) + [syng-im.db :refer [app-db schema]] + [syng-im.protocol.api :refer [init-protocol]] + [syng-im.protocol.protocol-handler :refer [make-handler]] + [syng-im.models.protocol :refer [update-identity + set-initialized]] + [syng-im.models.messages :refer [save-message + new-message-arrived]] + [syng-im.utils.logging :as log])) ;; -- Middleware ------------------------------------------------------------ ;; @@ -11,22 +18,39 @@ (defn check-and-throw "throw an exception if db doesn't match the schema." [a-schema db] - (if-let [problems (s/check a-schema db)] - (throw (js/Error. (str "schema check failed: " problems))))) + (if-let [problems (s/check a-schema db)] + (throw (js/Error. (str "schema check failed: " problems))))) (def validate-schema-mw (after (partial check-and-throw schema))) ;; -- Handlers -------------------------------------------------------------- -(register-handler - :initialize-db - validate-schema-mw +(register-handler :initialize-db (fn [_ _] app-db)) -(register-handler - :set-greeting - validate-schema-mw +;; -- Protocol -------------------------------------------------------------- + +(register-handler :initialize-protocol + (fn [db [_]] + (init-protocol (make-handler db)) + db)) + +(register-handler :protocol-initialized + (fn [db [_ identity]] + (-> db + (update-identity identity) + (set-initialized true)))) + +(register-handler :received-msg + (fn [db [_ {chat-id :from + msg-id :msg-id :as msg}]] + (save-message chat-id msg) + (new-message-arrived db chat-id msg-id))) + +;; -- Something -------------------------------------------------------------- + +(register-handler :set-greeting (fn [db [_ value]] (assoc db :greeting value))) \ No newline at end of file diff --git a/syng-im/src/syng_im/models/chat.cljs b/syng-im/src/syng_im/models/chat.cljs new file mode 100644 index 0000000000..e312d3daf5 --- /dev/null +++ b/syng-im/src/syng_im/models/chat.cljs @@ -0,0 +1,8 @@ +(ns syng-im.models.chat + (:require [syng-im.db :as db])) + +(defn set-current-chat-id [db chat-id] + (assoc-in db db/current-chat-id-path chat-id)) + +(defn current-chat-id [db] + (get-in db db/current-chat-id-path)) diff --git a/syng-im/src/syng_im/models/messages.cljs b/syng-im/src/syng_im/models/messages.cljs new file mode 100644 index 0000000000..291d6ded70 --- /dev/null +++ b/syng-im/src/syng_im/models/messages.cljs @@ -0,0 +1,46 @@ +(ns syng-im.models.messages + (:require [syng-im.persistence.realm :as r] + [cljs.reader :refer [read-string]] + [syng-im.utils.random :refer [timestamp]] + [syng-im.db :as db])) + +(defn save-message [chat-id {:keys [from to msg-id content content-type outgoing] :or {outgoing false} :as msg}] + (when-not (r/exists? :msgs :msg-id msg-id) + (r/write + (fn [] + (r/create :msgs {:chat-id chat-id + :msg-id msg-id + :from from + :to to + :content content + :content-type content-type + :outgoing outgoing + :timestamp (timestamp)} true))))) + +(defn get-messages* [chat-id] + (-> (r/get-by-field :msgs :chat-id chat-id) + (r/sorted :timestamp :desc) + (r/page 0 10))) + +(defn get-messages [chat-id] + (-> (get-messages* chat-id) + (js->clj :keywordize-keys true))) + +(defn new-message-arrived [db chat-id msg-id] + (assoc-in db (db/arrived-message-path chat-id) msg-id)) + +(comment + + (save-message "0x040028c500ff086ecf1cfbb3c1a7240179cde5b86f9802e6799b9bbe9cdd7ad1b05ae8807fa1f9ed19cc8ce930fc2e878738c59f030a6a2f94b3522dc1378ff154" + {:msg-id "153" + :content "hello!" + :content-type "text/plain"}) + + (get-messages* "0x040028c500ff086ecf1cfbb3c1a7240179cde5b86f9802e6799b9bbe9cdd7ad1b05ae8807fa1f9ed19cc8ce930fc2e878738c59f030a6a2f94b3522dc1378ff154") + + (get-messages "0x043df89d36f6e3d8ade18e55ac3e2e39406ebde152f76f2f82d674681d59319ffd9880eebfb4f5f8d5c222ec485b44d6e30ba3a03c96b1c946144fdeba1caccd43") + + (doseq [msg (get-messages* "0x043df89d36f6e3d8ade18e55ac3e2e39406ebde152f76f2f82d674681d59319ffd9880eebfb4f5f8d5c222ec485b44d6e30ba3a03c96b1c946144fdeba1caccd43")] + (r/delete msg)) + + ) \ No newline at end of file diff --git a/syng-im/src/syng_im/models/protocol.cljs b/syng-im/src/syng_im/models/protocol.cljs new file mode 100644 index 0000000000..47f164e6a2 --- /dev/null +++ b/syng-im/src/syng_im/models/protocol.cljs @@ -0,0 +1,27 @@ +(ns syng-im.models.protocol + (:require [cljs.reader :refer [read-string]] + [syng-im.protocol.state.storage :as s] + [syng-im.utils.encryption :refer [password-encrypt + password-decrypt]] + [syng-im.utils.types :refer [to-edn-string]] + [re-frame.db :refer [app-db]] + [syng-im.db :as db] + [syng-im.persistence.simple-kv-store :as kv] + [syng-im.utils.logging :as log])) + +(defn set-initialized [db initialized?] + (assoc-in db db/protocol-initialized-path initialized?)) + +(defn update-identity [db identity] + (let [password (get-in db db/identity-password-path) + encrypted (->> (to-edn-string identity) + (password-encrypt password))] + (s/put kv/kv-store :identity encrypted) + (assoc db :user-identity identity))) + +(defn stored-identity [db] + (let [encrypted (s/get kv/kv-store :identity) + password (get-in db db/identity-password-path)] + (when encrypted + (-> (password-decrypt password encrypted) + (read-string))))) diff --git a/syng-im/src/syng_im/persistence/realm.cljs b/syng-im/src/syng_im/persistence/realm.cljs new file mode 100644 index 0000000000..1c7201f7ec --- /dev/null +++ b/syng-im/src/syng_im/persistence/realm.cljs @@ -0,0 +1,94 @@ +(ns syng-im.persistence.realm + (:require [cljs.reader :refer [read-string]] + [syng-im.utils.logging :as log] + [syng-im.utils.types :refer [to-string]]) + (:refer-clojure :exclude [exists?])) + +(set! js/Realm (js/require "realm")) + +(def opts {:schema [{:name "Contact" + :properties {:phone-number "string" + :whisper-identity "string" + :name "string" + :photo-path "string"}} + {:name :kv-store + :primaryKey :key + :properties {:key "string" + :value "string"}} + {:name :msgs + :primaryKey :msg-id + :properties {:msg-id "string" + :from "string" + :to "string" + :content "string" ;; TODO make it ArrayBuffer + :content-type "string" + :timestamp "int" + :chat-id "string" + :outgoing "bool"}}]}) + +(def realm (js/Realm. (clj->js opts))) + +(def schema-by-name (->> (:schema opts) + (mapv (fn [{:keys [name] :as schema}] + [name schema])) + (into {}))) + +(defn field-type [schema-name field] + (get-in schema-by-name [schema-name :properties field])) + +(defn write [f] + (.write realm f)) + +(defn create + ([schema-name obj] + (create schema-name obj false)) + ([schema-name obj update?] + (.create realm (to-string schema-name) (clj->js obj) update?))) + +(defmulti to-query (fn [schema-name operator field value] + operator)) + +(defmethod to-query :eq [schema-name operator field value] + (let [value (to-string value) + query (str (name field) "=" (if (= "string" (field-type schema-name field)) + (str "\"" value "\"") + value)) + ;_ (log/debug query) + ] + query)) + +(defn get-by-field [schema-name field value] + (-> (.objects realm (name schema-name)) + (.filtered (to-query schema-name :eq field value)))) + +(defn sorted [results field-name order] + (.sorted results (to-string field-name) (if (= order :asc) + false + true))) + +(defn page [results from to] + (js/Array.prototype.slice.call results from to)) + +(defn single [result] + (-> (aget result 0))) + +(defn single-cljs [result] + (some-> (aget result 0) + (js->clj :keywordize-keys true))) + +(defn decode-value [{:keys [key value]}] + (read-string value)) + +(defn delete [obj] + (write (fn [] + (.delete realm obj)))) + +(defn exists? [schema-name field value] + (> (.-length (get-by-field schema-name field value)) + 0)) + +(defn get-count [objs] + (.-length objs)) + +(defn get-list [schema-name] + (vals (js->clj (.objects realm schema-name) :keywordize-keys true))) diff --git a/syng-im/src/syng_im/persistence/simple_kv_store.cljs b/syng-im/src/syng_im/persistence/simple_kv_store.cljs new file mode 100644 index 0000000000..15ea802465 --- /dev/null +++ b/syng-im/src/syng_im/persistence/simple_kv_store.cljs @@ -0,0 +1,24 @@ +(ns syng-im.persistence.simple-kv-store + (:require [syng-im.protocol.state.storage :as st] + [syng-im.persistence.realm :as r] + [syng-im.utils.types :refer [to-edn-string]])) + +(defrecord SimpleKvStore [] + st/Storage + (put [_ key value] + (r/write + (fn [] + (r/create :kv-store {:key key + :value (to-edn-string value)} true)))) + (get [_ key] + (some-> (r/get-by-field :kv-store :key key) + (r/single-cljs) + (r/decode-value))) + (contains-key? [_ key] + (r/exists? :kv-store :key key)) + (delete [_ key] + (-> (r/get-by-field :kv-store :key key) + (r/single) + (r/delete)))) + +(def kv-store (->SimpleKvStore)) diff --git a/syng-im/src/syng_im/protocol/protocol_handler.cljs b/syng-im/src/syng_im/protocol/protocol_handler.cljs new file mode 100644 index 0000000000..2e240cf910 --- /dev/null +++ b/syng-im/src/syng_im/protocol/protocol_handler.cljs @@ -0,0 +1,45 @@ +(ns syng-im.protocol.protocol-handler + (:require [syng-im.utils.logging :as log] + [syng-im.constants :refer [ethereum-rpc-url]] + [re-frame.core :refer [dispatch]] + [syng-im.models.protocol :refer [stored-identity]] + [syng-im.persistence.simple-kv-store :as kv])) + + +(defn make-handler [db] + {:ethereum-rpc-url ethereum-rpc-url + :identity (stored-identity db) + :storage kv/kv-store + :handler (fn [{:keys [event-type] :as event}] + (log/info "Event:" (clj->js event)) + (case event-type + :initialized (let [{:keys [identity]} event] + (dispatch [:protocol-initialized identity])) + :new-msg (let [{:keys [from to payload]} event] + (dispatch [:received-msg (assoc payload :from from :to to)])) + ;:msg-acked (let [{:keys [msg-id]} event] + ; (add-to-chat "chat" ":" (str "Message " msg-id " was acked"))) + ;:delivery-failed (let [{:keys [msg-id]} event] + ; (add-to-chat "chat" ":" (str "Delivery of message " msg-id " failed"))) + ;:new-group-chat (let [{:keys [from group-id identities]} event] + ; (set-group-id! group-id) + ; (set-group-identities identities) + ; (add-to-chat "group-chat" ":" (str "Received group chat invitation from " from " for group-id: " group-id))) + ;:group-chat-invite-acked (let [{:keys [from group-id]} event] + ; (add-to-chat "group-chat" ":" (str "Received ACK for group chat invitation from " from " for group-id: " group-id))) + ;:new-group-msg (let [{from :from + ; {content :content} :payload} event] + ; (add-to-chat "group-chat" from content)) + ;:group-new-participant (let [{:keys [group-id identity from]} event] + ; (add-to-chat "group-chat" ":" (str (shorten from) " added " (shorten identity) " to group chat")) + ; (add-identity-to-group-list identity)) + ;:group-removed-participant (let [{:keys [group-id identity from]} event] + ; (add-to-chat "group-chat" ":" (str (shorten from) " removed " (shorten identity) " from group chat")) + ; (remove-identity-from-group-list identity)) + ;:removed-from-group (let [{:keys [group-id from]} event] + ; (add-to-chat "group-chat" ":" (str (shorten from) " removed you from group chat"))) + ;:participant-left-group (let [{:keys [group-id from]} event] + ; (add-to-chat "group-chat" ":" (str (shorten from) " left group chat"))) + ;(add-to-chat "chat" ":" (str "Don't know how to handle " event-type)) + (log/info "Don't know how to handle" event-type) + ))}) diff --git a/syng-im/src/syng_im/utils/event.cljs b/syng-im/src/syng_im/utils/event.cljs new file mode 100644 index 0000000000..bb47182867 --- /dev/null +++ b/syng-im/src/syng_im/utils/event.cljs @@ -0,0 +1,9 @@ +(ns syng-im.utils.event + (:require [cljs.core.async :refer [