diff --git a/syng-im/images/att.png b/syng-im/images/att.png new file mode 100644 index 0000000000..bafc26837d Binary files /dev/null and b/syng-im/images/att.png differ diff --git a/syng-im/images/delivered.png b/syng-im/images/delivered.png new file mode 100644 index 0000000000..0d7a9fbc8a Binary files /dev/null and b/syng-im/images/delivered.png differ diff --git a/syng-im/images/logo.png b/syng-im/images/logo.png new file mode 100644 index 0000000000..02e05bf0a9 Binary files /dev/null and b/syng-im/images/logo.png differ diff --git a/syng-im/images/mic.png b/syng-im/images/mic.png new file mode 100644 index 0000000000..9f5e8fc0a7 Binary files /dev/null and b/syng-im/images/mic.png differ diff --git a/syng-im/images/nav-back.png b/syng-im/images/nav-back.png new file mode 100644 index 0000000000..6e7cdb7339 Binary files /dev/null and b/syng-im/images/nav-back.png differ diff --git a/syng-im/images/no-photo.png b/syng-im/images/no-photo.png new file mode 100755 index 0000000000..2889cf4420 Binary files /dev/null and b/syng-im/images/no-photo.png differ diff --git a/syng-im/images/online.png b/syng-im/images/online.png new file mode 100644 index 0000000000..1d5193e578 Binary files /dev/null and b/syng-im/images/online.png differ diff --git a/syng-im/images/play.png b/syng-im/images/play.png new file mode 100644 index 0000000000..078b1e29c7 Binary files /dev/null and b/syng-im/images/play.png differ diff --git a/syng-im/images/seen.png b/syng-im/images/seen.png new file mode 100644 index 0000000000..17ec227355 Binary files /dev/null and b/syng-im/images/seen.png differ diff --git a/syng-im/images/smile.png b/syng-im/images/smile.png new file mode 100644 index 0000000000..e64022a14f Binary files /dev/null and b/syng-im/images/smile.png differ diff --git a/syng-im/project.clj b/syng-im/project.clj index 5a5e03db46..37ed02bf72 100644 --- a/syng-im/project.clj +++ b/syng-im/project.clj @@ -17,6 +17,7 @@ ["do" "clean" ["with-profile" "prod" "cljsbuild" "once" "ios"] ["with-profile" "prod" "cljsbuild" "once" "android"]]} + :figwheel {:nrepl-port 7888} :profiles {:dev {:dependencies [[figwheel-sidecar "0.5.0-2"] [com.cemerick/piggieback "0.2.1"]] :source-paths ["src" "env/dev"] diff --git a/syng-im/src/syng_im/android/core.cljs b/syng-im/src/syng_im/android/core.cljs index e474a9f24b..27dc1bf820 100644 --- a/syng-im/src/syng_im/android/core.cljs +++ b/syng-im/src/syng_im/android/core.cljs @@ -8,13 +8,10 @@ [syng-im.subs] [syng-im.components.react :refer [navigator app-registry]] [syng-im.components.chat :refer [chat]] - [syng-im.utils.logging :as log])) + [syng-im.utils.logging :as log] + [syng-im.navigation :refer [*nav-render*]])) -(def ^{:dynamic true :private true} *nav-render* - "Flag to suppress navigator re-renders from outside om when pushing/popping." - true) - (def back-button-handler (cljs/atom {:nav nil :handler nil})) diff --git a/syng-im/src/syng_im/components/chat.cljs b/syng-im/src/syng_im/components/chat.cljs index f1f6074a4f..dc3e69111b 100644 --- a/syng-im/src/syng_im/components/chat.cljs +++ b/syng-im/src/syng_im/components/chat.cljs @@ -1,21 +1,48 @@ (ns syng-im.components.chat (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] - [syng-im.components.react :refer [view text image touchable-highlight navigator]] - [syng-im.utils.logging :as log])) - -(def logo-img (js/require "./images/cljs.png")) - -(defn alert [title] - (.alert (.-Alert js/React) title)) + [syng-im.components.react :refer [android? + view + text + image + touchable-highlight + navigator + toolbar-android + list-view]] + [syng-im.utils.logging :as log] + [syng-im.navigation :refer [nav-pop]] + [syng-im.resources :as res] + [syng-im.utils.listview :refer [to-datasource]] + [syng-im.components.invertible-scroll-view :refer [invertible-scroll-view]] + [reagent.core :as r] + [syng-im.components.chat-message :refer [chat-message]] + [syng-im.components.chat-message-new :refer [chat-message-new]])) (defn chat [{:keys [navigator]}] - (let [greeting (subscribe [:get-greeting])] + (let [messages (subscribe [:get-chat-messages])] (fn [] - [view {:style {:flex-direction "column" :margin 40 :align-items "center"}} - [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} - :on-press #(alert "HELLO!")} - [text {:style {:color "white" :text-align "center" :font-weight "bold"}} "press me"]]]))) + (let [msgs @messages + _ (log/debug "messages=" msgs) + datasource (to-datasource msgs)] + [view {:style {:flex 1 + :backgroundColor "white"}} + (when android? + ;; TODO add IOS version + [toolbar-android {:logo res/logo-icon + :title "Chat name" + :titleColor "#4A5258" + :subtitle "Last seen just now" + :subtitleColor "#AAB2B2" + :navIcon res/nav-back-icon + :style {:backgroundColor "white" + :height 56 + :elevation 2} + :onIconClicked (fn [] + (nav-pop navigator))}]) + [list-view {:dataSource datasource + :renderScrollComponent (fn [props] + (invertible-scroll-view nil)) + :renderRow (fn [row section-id row-id] + (r/as-element [chat-message (js->clj row :keywordize-keys true)])) + :style {:backgroundColor "black"}}] + [chat-message-new]])))) diff --git a/syng-im/src/syng_im/components/chat_message.cljs b/syng-im/src/syng_im/components/chat_message.cljs new file mode 100644 index 0000000000..02f449f8bb --- /dev/null +++ b/syng-im/src/syng_im/components/chat_message.cljs @@ -0,0 +1,121 @@ +(ns syng-im.components.chat-message + (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [syng-im.components.react :refer [android? + view + text + image + touchable-highlight + navigator + toolbar-android]] + [syng-im.utils.logging :as log] + [syng-im.navigation :refer [nav-pop]] + [syng-im.resources :as res] + [syng-im.constants :refer [text-content-type]])) + + +(defn message-date [{:keys [date]}] + [text {:style {:marginVertical 10 + :fontFamily "Avenir-Roman" + :fontSize 11 + :color "#AAB2B2" + :letterSpacing 1 + :lineHeight 15 + :textAlign "center" + :opacity 0.8}} + date]) + +(defn message-content-audio [{:keys [content-type content-type]}] + [view {:style {:flexDirection "row" + :alignItems "center"}} + [view {:style {:width 33 + :height 33 + :borderRadius 50 + :elevation 1}} + [image {:source res/play + :style {:width 33 + :height 33}}]] + [view {:style {:marginTop 10 + :marginLeft 10 + :width 120 + :height 26 + :elevation 1}} + [view {:style {:position "absolute" + :top 4 + :width 120 + :height 2 + :backgroundColor "#EC7262"}}] + [view {:style {:position "absolute" + :left 0 + :top 0 + :width 2 + :height 10 + :backgroundColor "#4A5258"}}] + [text {:style {:position "absolute" + :left 1 + :top 11 + :fontFamily "Avenir-Roman" + :fontSize 11 + :color "#4A5258" + :letterSpacing 1 + :lineHeight 15}} + "03:39"]]]) + +(defn message-content [{:keys [content-type content outgoing]}] + [view {:style (merge {:borderRadius 6} + (if (= content-type text-content-type) + {:paddingVertical 12 + :paddingHorizontal 16} + {:paddingVertical 14 + :paddingHorizontal 10}) + (if outgoing + {:backgroundColor "#D3EEEF"} + {:backgroundColor "#FBF6E3"}))} + (if (= content-type text-content-type) + [text {:style {:fontSize 14 + :fontFamily "Avenir-Roman" + :color "#4A5258"}} + content] + [message-content-audio {:content content + :content-type content-type}])]) + +(defn message-delivery-status [{:keys [delivery-status]}] + [view {:style {:flexDirection "row" + :marginTop 2}} + [image {:source (if (= (keyword delivery-status) :seen) + res/seen-icon + res/delivered-icon) + :style {:marginTop 6 + :opacity 0.6}}] + [text {:style {:fontFamily "Avenir-Roman" + :fontSize 11 + :color "#AAB2B2" + :opacity 0.8 + :marginLeft 5}} + (if (= (keyword delivery-status) :seen) + "Seen" + "Delivered")]]) + +(defn message-body [{:keys [msg-id content content-type outgoing delivery-status]}] + [view {:style (merge {:flexDirection "column" + :width 260 + :marginVertical 5} + (if outgoing + {:alignSelf "flex-end" + :alignItems "flex-end"} + {:alignSelf "flex-start" + :alignItems "flex-start"}))} + [message-content {:content-type content-type + :content content + :outgoing outgoing}] + (when (and outgoing delivery-status) + [message-delivery-status {:delivery-status delivery-status}])]) + +(defn chat-message [{:keys [msg-id content content-type outgoing delivery-status date new-day] :as msg}] + [view {:paddingHorizontal 15} + (when new-day + [message-date {:date date}]) + [message-body {:msg-id msg-id + :content content + :content-type content-type + :outgoing outgoing + :delivery-status delivery-status}]]) diff --git a/syng-im/src/syng_im/components/chat_message_new.cljs b/syng-im/src/syng_im/components/chat_message_new.cljs new file mode 100644 index 0000000000..9b5f37a404 --- /dev/null +++ b/syng-im/src/syng_im/components/chat_message_new.cljs @@ -0,0 +1,51 @@ +(ns syng-im.components.chat-message-new + (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [syng-im.components.react :refer [android? + view + image + text-input]] + [syng-im.utils.logging :as log] + [syng-im.resources :as res] + [reagent.core :as r])) + + +(defn chat-message-new [] + (let [text (r/atom nil) + chat-id (subscribe [:get-current-chat-id])] + (fn [] + [view {:style {:flexDirection "row" + :margin 10 + :height 40 + :backgroundColor "#E5F5F6" + :borderRadius 5}} + [image {:source res/mic + :style {:marginTop 11 + :marginLeft 14 + :width 13 + :height 20}}] + [text-input {:underlineColorAndroid "#9CBFC0" + :style {:flex 1 + :marginLeft 18 + :lineHeight 42 + :fontSize 14 + :fontFamily "Avenir-Roman" + :color "#9CBFC0"} + :autoFocus true + :placeholder "Enter your message here" + :value @text + :onChangeText (fn [new-text] + (reset! text new-text) + (r/flush)) + :onSubmitEditing (fn [e] + (dispatch [:send-chat-msg @chat-id @text]) + (reset! text nil))}] + [image {:source res/smile + :style {:marginTop 11 + :marginRight 12 + :width 18 + :height 18}}] + [image {:source res/att + :style {:marginTop 14 + :marginRight 16 + :width 17 + :height 14}}]]))) diff --git a/syng-im/src/syng_im/components/invertible_scroll_view.cljs b/syng-im/src/syng_im/components/invertible_scroll_view.cljs new file mode 100644 index 0000000000..8611247c2d --- /dev/null +++ b/syng-im/src/syng_im/components/invertible_scroll_view.cljs @@ -0,0 +1,8 @@ +(ns syng-im.components.invertible-scroll-view) + +(set! js/InvertibleScrollView (js/require "react-native-invertible-scroll-view")) + +(defn invertible-scroll-view [props] + (js/React.createElement js/InvertibleScrollView + (clj->js (merge {:inverted true} props)))) + diff --git a/syng-im/src/syng_im/components/react.cljs b/syng-im/src/syng_im/components/react.cljs index ac65dc7a4c..0c524c4e80 100644 --- a/syng-im/src/syng_im/components/react.cljs +++ b/syng-im/src/syng_im/components/react.cljs @@ -8,4 +8,11 @@ (def text (r/adapt-react-class (.-Text js/React))) (def view (r/adapt-react-class (.-View js/React))) (def image (r/adapt-react-class (.-Image js/React))) -(def touchable-highlight (r/adapt-react-class (.-TouchableHighlight js/React))) \ No newline at end of file +(def touchable-highlight (r/adapt-react-class (.-TouchableHighlight js/React))) +(def toolbar-android (r/adapt-react-class (.-ToolbarAndroid js/React))) +(def list-view (r/adapt-react-class (.-ListView js/React))) +(def text-input (r/adapt-react-class (.-TextInput js/React))) + +(def platform (.. js/React -Platform -OS)) + +(def android? (= platform "android")) \ 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 2859bd43cc..8e2ee1a97a 100644 --- a/syng-im/src/syng_im/db.cljs +++ b/syng-im/src/syng_im/db.cljs @@ -5,13 +5,14 @@ (def schema {:greeting s/Str}) ;; initial state of app-db -(def app-db {:greeting "Hello Clojure in iOS and Android!" - :identity-password "replace-me-with-user-entered-password"}) +(def app-db {:greeting "Hello Clojure in iOS and Android!" + :identity-password "replace-me-with-user-entered-password" + :chat {:current-chat-id "0x040028c500ff086ecf1cfbb3c1a7240179cde5b86f9802e6799b9bbe9cdd7ad1b05ae8807fa1f9ed19cc8ce930fc2e878738c59f030a6a2f94b3522dc1378ff154"} + :chats {}}) (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 +(defn latest-msg-id-path [chat-id] + [:chats 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 859aa85a44..fa774b136f 100644 --- a/syng-im/src/syng_im/handlers.cljs +++ b/syng-im/src/syng_im/handlers.cljs @@ -7,9 +7,11 @@ [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])) + [syng-im.models.messages :refer [save-message]] + [syng-im.models.chat :refer [set-latest-msg-id]] + [syng-im.utils.logging :as log] + [syng-im.protocol.api :as api] + [syng-im.constants :refer [text-content-type]])) ;; -- Middleware ------------------------------------------------------------ ;; @@ -47,7 +49,23 @@ (fn [db [_ {chat-id :from msg-id :msg-id :as msg}]] (save-message chat-id msg) - (new-message-arrived db chat-id msg-id))) + (set-latest-msg-id db chat-id msg-id))) + +(register-handler :send-chat-msg + (fn [db [_ chat-id text]] + (log/debug "chat-id" chat-id "text" text) + (let [{msg-id :msg-id + {from :from + to :to} :msg} (api/send-user-msg {:to chat-id + :content text}) + msg {:msg-id msg-id + :from from + :to to + :content text + :content-type text-content-type + :outgoing true}] + (save-message chat-id msg) + (set-latest-msg-id db chat-id msg-id)))) ;; -- Something -------------------------------------------------------------- diff --git a/syng-im/src/syng_im/models/chat.cljs b/syng-im/src/syng_im/models/chat.cljs index e312d3daf5..23a03e79db 100644 --- a/syng-im/src/syng_im/models/chat.cljs +++ b/syng-im/src/syng_im/models/chat.cljs @@ -6,3 +6,10 @@ (defn current-chat-id [db] (get-in db db/current-chat-id-path)) + +(defn set-latest-msg-id [db chat-id msg-id] + (assoc-in db (db/latest-msg-id-path chat-id) msg-id)) + +(defn latest-msg-id [db chat-id] + (->> (db/latest-msg-id-path chat-id) + (get-in db))) \ No newline at end of file diff --git a/syng-im/src/syng_im/models/messages.cljs b/syng-im/src/syng_im/models/messages.cljs index 291d6ded70..93b0bac8bf 100644 --- a/syng-im/src/syng_im/models/messages.cljs +++ b/syng-im/src/syng_im/models/messages.cljs @@ -26,9 +26,6 @@ (-> (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" @@ -38,7 +35,7 @@ (get-messages* "0x040028c500ff086ecf1cfbb3c1a7240179cde5b86f9802e6799b9bbe9cdd7ad1b05ae8807fa1f9ed19cc8ce930fc2e878738c59f030a6a2f94b3522dc1378ff154") - (get-messages "0x043df89d36f6e3d8ade18e55ac3e2e39406ebde152f76f2f82d674681d59319ffd9880eebfb4f5f8d5c222ec485b44d6e30ba3a03c96b1c946144fdeba1caccd43") + (get-messages nil) (doseq [msg (get-messages* "0x043df89d36f6e3d8ade18e55ac3e2e39406ebde152f76f2f82d674681d59319ffd9880eebfb4f5f8d5c222ec485b44d6e30ba3a03c96b1c946144fdeba1caccd43")] (r/delete msg)) diff --git a/syng-im/src/syng_im/navigation.cljs b/syng-im/src/syng_im/navigation.cljs new file mode 100644 index 0000000000..6405c53589 --- /dev/null +++ b/syng-im/src/syng_im/navigation.cljs @@ -0,0 +1,9 @@ +(ns syng-im.navigation) + +(def ^{:dynamic true :private true} *nav-render* + "Flag to suppress navigator re-renders from outside om when pushing/popping." + true) + +(defn nav-pop [nav] + (binding [*nav-render* false] + (.pop nav))) \ No newline at end of file diff --git a/syng-im/src/syng_im/resources.cljs b/syng-im/src/syng_im/resources.cljs new file mode 100644 index 0000000000..d7e11f0aea --- /dev/null +++ b/syng-im/src/syng_im/resources.cljs @@ -0,0 +1,13 @@ +(ns syng-im.resources) + +(def logo-icon (js/require "./images/logo.png")) +(def nav-back-icon (js/require "./images/nav-back.png")) +(def user-no-photo (js/require "./images/no-photo.png")) +(def online-icon (js/require "./images/online.png")) +(def seen-icon (js/require "./images/seen.png")) +(def delivered-icon (js/require "./images/delivered.png")) +(def play (js/require "./images/play.png")) +(def mic (js/require "./images/mic.png")) +(def smile (js/require "./images/smile.png")) +(def att (js/require "./images/att.png")) + diff --git a/syng-im/src/syng_im/subs.cljs b/syng-im/src/syng_im/subs.cljs index 9bacfd9e3a..6b7b9bd30e 100644 --- a/syng-im/src/syng_im/subs.cljs +++ b/syng-im/src/syng_im/subs.cljs @@ -1,9 +1,25 @@ (ns syng-im.subs (:require-macros [reagent.ratom :refer [reaction]]) - (:require [re-frame.core :refer [register-sub]])) + (:require [re-frame.core :refer [register-sub]] + [syng-im.models.chat :refer [current-chat-id + latest-msg-id]] + [syng-im.models.messages :refer [get-messages]])) -(register-sub - :get-greeting - (fn [db _] - (reaction - (get @db :greeting)))) \ No newline at end of file +(register-sub :get-greeting (fn [db _] + (reaction + (get @db :greeting)))) + +(register-sub :get-chat-messages + (fn [db _] + (let [chat-id (-> (current-chat-id @db) + (reaction)) + latest-msg (-> (latest-msg-id @db @chat-id) + (reaction))] + ;; latest-msg signals us that a new message has been added + (reaction + (let [_ @latest-msg] + (get-messages @chat-id)))))) + +(register-sub :get-current-chat-id (fn [db _] + (-> (current-chat-id @db) + (reaction)))) \ No newline at end of file diff --git a/syng-im/src/syng_im/utils/listview.cljs b/syng-im/src/syng_im/utils/listview.cljs new file mode 100644 index 0000000000..4908974276 --- /dev/null +++ b/syng-im/src/syng_im/utils/listview.cljs @@ -0,0 +1,7 @@ +(ns syng-im.utils.listview + (:require-macros [natal-shell.data-source :refer [data-source clone-with-rows]])) + +(defn to-datasource [msgs] + (-> (data-source {:rowHasChanged (fn [row1 row2] + (not= row1 row2))}) + (clone-with-rows msgs))) \ No newline at end of file