After Width: | Height: | Size: 651 B |
After Width: | Height: | Size: 316 B |
After Width: | Height: | Size: 526 B |
After Width: | Height: | Size: 529 B |
After Width: | Height: | Size: 985 B |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 307 B |
After Width: | Height: | Size: 898 B |
After Width: | Height: | Size: 284 B |
After Width: | Height: | Size: 584 B |
|
@ -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"]
|
||||
|
|
|
@ -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}))
|
||||
|
||||
|
|
|
@ -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]]))))
|
||||
|
|
|
@ -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}]])
|
|
@ -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}}]])))
|
|
@ -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))))
|
||||
|
|
@ -9,3 +9,10 @@
|
|||
(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)))
|
||||
(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"))
|
|
@ -6,12 +6,13 @@
|
|||
|
||||
;; initial state of app-db
|
||||
(def app-db {:greeting "Hello Clojure in iOS and Android!"
|
||||
:identity-password "replace-me-with-user-entered-password"})
|
||||
: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])
|
||||
(defn latest-msg-id-path [chat-id]
|
||||
[:chats chat-id :arrived-message-id])
|
|
@ -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 --------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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)))
|
|
@ -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))
|
||||
|
|
|
@ -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)))
|
|
@ -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"))
|
||||
|
|
@ -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 _]
|
||||
(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))))
|
|
@ -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)))
|