Merge pull request #85 from status-im/real-data

Listen sms. Comment/replace placeholders.
This commit is contained in:
Jarrad 2016-05-24 10:52:21 +02:00
commit eb72ea776f
32 changed files with 525 additions and 332 deletions

View File

@ -15,7 +15,8 @@
"react-native-circle-checkbox", "react-native-circle-checkbox",
"react-native-randombytes", "react-native-randombytes",
"dismissKeyboard", "dismissKeyboard",
"react-native-linear-gradient" "react-native-linear-gradient",
"react-native-android-sms-listener"
], ],
"imageDirs": [ "imageDirs": [
"images" "images"

View File

@ -129,6 +129,7 @@ dependencies {
compile project(':react-native-contacts') compile project(':react-native-contacts')
compile project(':react-native-i18n') compile project(':react-native-i18n')
compile project(':react-native-linear-gradient') compile project(':react-native-linear-gradient')
compile project(':ReactNativeAndroidSmsListener')
// compile(name:'geth', ext:'aar') // compile(name:'geth', ext:'aar')
compile(group: 'status-im', name: 'android-geth', version: '1.4.0-201604110816-a97a114', ext: 'aar') compile(group: 'status-im', name: 'android-geth', version: '1.4.0-201604110816-a97a114', ext: 'aar')

View File

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application <application

View File

@ -11,6 +11,7 @@ import android.os.Environment;
import com.github.ethereum.go_ethereum.cmd.Geth; import com.github.ethereum.go_ethereum.cmd.Geth;
import com.bitgo.randombytes.RandomBytesPackage; import com.bitgo.randombytes.RandomBytesPackage;
import com.BV.LinearGradient.LinearGradientPackage; import com.BV.LinearGradient.LinearGradientPackage;
import com.centaurwarchief.smslistener.SmsListener;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -80,7 +81,8 @@ public class MainActivity extends ReactActivity {
new ReactNativeContacts(), new ReactNativeContacts(),
new ReactNativeI18n(), new ReactNativeI18n(),
new RandomBytesPackage(), new RandomBytesPackage(),
new LinearGradientPackage() new LinearGradientPackage(),
new SmsListener(this)
); );
} }
} }

View File

@ -16,3 +16,5 @@ include ':randombytes'
project(':randombytes').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-randombytes/app') project(':randombytes').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-randombytes/app')
include ':react-native-linear-gradient' include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android') project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':ReactNativeAndroidSmsListener'
project(':ReactNativeAndroidSmsListener').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-sms-listener/android')

View File

@ -10,6 +10,7 @@
"react": "^0.14.5", "react": "^0.14.5",
"react-native": "^0.24.1", "react-native": "^0.24.1",
"react-native-action-button": "^1.1.4", "react-native-action-button": "^1.1.4",
"react-native-android-sms-listener": "^0.1.3",
"react-native-circle-checkbox": "^0.1.3", "react-native-circle-checkbox": "^0.1.3",
"react-native-contacts": "^0.2.4", "react-native-contacts": "^0.2.4",
"react-native-i18n": "0.0.8", "react-native-i18n": "0.0.8",

View File

@ -11,7 +11,8 @@
^{:voom {:repo "https://github.com/status-im/status-lib.git" ^{:voom {:repo "https://github.com/status-im/status-lib.git"
:branch "syng-rename"}} :branch "syng-rename"}}
[status-im/protocol "0.1.1-20160519_164302-g92930a0"] [status-im/protocol "0.1.1-20160519_164302-g92930a0"]
[natal-shell "0.1.6"]] [natal-shell "0.1.6"]
[com.andrewmcveigh/cljs-time "0.4.0"]]
:plugins [[lein-cljsbuild "1.1.1"] :plugins [[lein-cljsbuild "1.1.1"]
[lein-figwheel "0.5.0-2"]] [lein-figwheel "0.5.0-2"]]
:clean-targets ["target/" "index.ios.js" "index.android.js"] :clean-targets ["target/" "index.ios.js" "index.android.js"]

View File

@ -2,6 +2,7 @@
(:require [re-frame.core :refer [register-handler enrich after debug dispatch]] (:require [re-frame.core :refer [register-handler enrich after debug dispatch]]
[status-im.models.commands :as commands] [status-im.models.commands :as commands]
[clojure.string :as str] [clojure.string :as str]
[status-im.components.styles :refer [default-chat-color]]
[status-im.chat.suggestions :as suggestions] [status-im.chat.suggestions :as suggestions]
[status-im.protocol.api :as api] [status-im.protocol.api :as api]
[status-im.models.messages :as messages] [status-im.models.messages :as messages]
@ -12,7 +13,10 @@
[status-im.models.chats :as chats] [status-im.models.chats :as chats]
[status-im.navigation.handlers :as nav] [status-im.navigation.handlers :as nav]
[status-im.utils.handlers :as u] [status-im.utils.handlers :as u]
[status-im.persistence.realm :as r])) [status-im.persistence.realm :as r]
[status-im.handlers.server :as server]
[status-im.utils.phone-number :refer [format-phone-number]]
[status-im.utils.datetime :as time]))
(register-handler :set-show-actions (register-handler :set-show-actions
(fn [db [_ show-actions]] (fn [db [_ show-actions]]
@ -104,7 +108,8 @@
:to current-chat-id :to current-chat-id
:from identity :from identity
:content-type text-content-type :content-type text-content-type
:outgoing true})] :outgoing true
:timestamp (time/now-ms)})]
(if command (if command
(commands/set-chat-command db command) (commands/set-chat-command db command)
(assoc db :new-message (when-not (str/blank? text) message))))) (assoc db :new-message (when-not (str/blank? text) message)))))
@ -213,15 +218,22 @@
(assoc db :password-saved true))) (assoc db :password-saved true)))
(register-handler :sign-up (register-handler :sign-up
(-> (fn [db [_ phone-number]] (fn [db [_ phone-number]]
;; todo save phone number to db ;; todo save phone number to db
(assoc db :user-phone-number phone-number)) (let [formatted (format-phone-number phone-number)]
((after (fn [& _] (sign-up-service/on-sign-up-response)))))) (-> db
(assoc :user-phone-number formatted)
sign-up-service/start-listening-confirmation-code-sms
(server/sign-up formatted sign-up-service/on-sign-up-response)))))
(register-handler :stop-listening-confirmation-code-sms
(fn [db [_]]
(sign-up-service/stop-listening-confirmation-code-sms db)))
(register-handler :sign-up-confirm (register-handler :sign-up-confirm
(fn [db [_ confirmation-code]] (fn [db [_ confirmation-code]]
(sign-up-service/on-send-code-response confirmation-code) (server/sign-up-confirm confirmation-code sign-up-service/on-send-code-response)
(sign-up-service/set-signed-up db true))) db))
(register-handler :set-signed-up (register-handler :set-signed-up
(fn [db [_ signed-up]] (fn [db [_ signed-up]]
@ -299,6 +311,7 @@
(let [name (get-in contacts [contcat-id :name]) (let [name (get-in contacts [contcat-id :name])
chat {:chat-id contcat-id chat {:chat-id contcat-id
:name name :name name
:color default-chat-color
:group-chat false :group-chat false
:is-active true :is-active true
:timestamp (.getTime (js/Date.)) :timestamp (.getTime (js/Date.))

View File

@ -1,7 +1,7 @@
(ns status-im.chat.screen (ns status-im.chat.screen
(:require-macros [status-im.utils.views :refer [defview]]) (:require-macros [status-im.utils.views :refer [defview]])
(:require [clojure.string :as s] (:require [re-frame.core :refer [subscribe dispatch]]
[re-frame.core :refer [subscribe dispatch]] [clojure.string :as s]
[status-im.components.react :refer [view [status-im.components.react :refer [view
text text
image image
@ -9,9 +9,11 @@
touchable-highlight touchable-highlight
list-view list-view
list-item]] list-item]]
[status-im.components.chat-icon.screen :refer [chat-icon-view-action
chat-icon-view-menu-item]]
[status-im.chat.styles.screen :as st] [status-im.chat.styles.screen :as st]
[status-im.resources :as res]
[status-im.utils.listview :refer [to-datasource]] [status-im.utils.listview :refer [to-datasource]]
[status-im.utils.utils :refer [truncate-str]]
[status-im.components.invertible-scroll-view :refer [invertible-scroll-view]] [status-im.components.invertible-scroll-view :refer [invertible-scroll-view]]
[status-im.components.toolbar :refer [toolbar]] [status-im.components.toolbar :refer [toolbar]]
[status-im.chat.views.message :refer [chat-message]] [status-im.chat.views.message :refer [chat-message]]
@ -32,19 +34,13 @@
(assoc msg :text-color text-color (assoc msg :text-color text-color
:background-color background-color)))) :background-color background-color))))
(defn chat-photo [{:keys [photo-path]}] (defview chat-icon []
[view {:margin 10 [chat-id [:chat :chat-id]
:borderRadius 50} group-chat [:chat :group-chat]
[image {:source (if (s/blank? photo-path) name [:chat :name]
res/user-no-photo color [:chat :color]]
{:uri photo-path}) ;; TODO stub data ('online' property)
:style st/chat-photo}]]) [chat-icon-view-action chat-id group-chat name color true])
(defn contact-online [{:keys [online]}]
(when online
[view st/online-view
[view st/online-dot-left]
[view st/online-dot-right]]))
(defn typing [member] (defn typing [member]
[view st/typing-view [view st/typing-view
@ -54,6 +50,7 @@
(defn typing-all [] (defn typing-all []
[view st/typing-all [view st/typing-all
;; TODO stub data
(for [member ["Geoff" "Justas"]] (for [member ["Geoff" "Justas"]]
^{:key member} [typing member])]) ^{:key member} [typing member])])
@ -94,43 +91,44 @@
[text {:style st/action-subtitle} [text {:style st/action-subtitle}
subtitle])]]]) subtitle])]]])
(defn menu-item-contact-photo [{:keys [photo-path]}] (defview menu-item-icon-profile []
[image {:source (if (s/blank? photo-path) [chat-id [:chat :chat-id]
res/user-no-photo group-chat [:chat :group-chat]
{:uri photo-path}) name [:chat :name]
:style st/menu-item-profile-contact-photo}]) color [:chat :color]]
;; TODO stub data ('online' property)
[chat-icon-view-menu-item chat-id group-chat name color true])
(defn menu-item-contact-online [{:keys [online]}] (defn members-text [members]
(when online (truncate-str (str (s/join ", " (map #(:name %) members)) " and you") 35))
[view st/menu-item-profile-online-view
[view st/menu-item-profile-online-dot-left]
[view st/menu-item-profile-online-dot-right]]))
(defn menu-item-icon-profile []
[view st/icon-view
[menu-item-contact-photo {}]
[menu-item-contact-online {:online true}]])
(defn actions-list-view [] (defn actions-list-view []
(let [{:keys [group-chat chat-id]} (let [{:keys [group-chat chat-id]}
(subscribe [:chat-properties [:group-chat :chat-id]])] (subscribe [:chat-properties [:group-chat :chat-id]])
members (subscribe [:current-chat-contacts])]
(when-let [actions (if @group-chat (when-let [actions (if @group-chat
[{:title "Add Contact to chat" [{:title "Members"
:subtitle (members-text @members)
:icon :menu_group :icon :menu_group
:icon-style {:width 25 :icon-style {:width 25
:height 19} :height 19}
:handler #(dispatch [:navigate-to :add-participants])} ;; TODO not implemented: action Members
{:title "Remove Contact from chat" :handler nil}
:subtitle "Alex, John" {:title "Search chat"
:subtitle "!not implemented"
:icon :search_gray_copy :icon :search_gray_copy
:icon-style {:width 17 :icon-style {:width 17
:height 17} :height 17}
:handler #(dispatch [:navigate-to :remove-participants])} ;; TODO not implemented: action Search chat
{:title "Leave Chat" :handler nil}
{:title "Notifications and sounds"
:subtitle "!not implemented"
;;:subtitle "Chat muted"
:icon :muted :icon :muted
:icon-style {:width 18 :icon-style {:width 18
:height 21} :height 21}
:handler #(dispatch [:leave-group-chat])} ;; TODO not implemented: action Notifications
:handler nil}
{:title "Settings" {:title "Settings"
:icon :settings :icon :settings
:icon-style {:width 20 :icon-style {:width 20
@ -147,19 +145,23 @@
:icon :search_gray_copy :icon :search_gray_copy
:icon-style {:width 17 :icon-style {:width 17
:height 17} :height 17}
;; TODO not implemented: action Search chat
:handler nil} :handler nil}
{:title "Notifications and sounds" {:title "Notifications and sounds"
:subtitle "!not implemented" :subtitle "!not implemented"
;;:subtitle "Notifications on"
:icon :muted :icon :muted
:icon-style {:width 18 :icon-style {:width 18
:height 21} :height 21}
;; TODO not implemented: action Notifications
:handler nil} :handler nil}
{:title "Settings" {:title "Settings"
:subtitle "!not implemented" :subtitle "!not implemented"
:icon :settings :icon :settings
:icon-style {:width 20 :icon-style {:width 20
:height 13} :height 13}
:handler (fn [])}])] ;; TODO not implemented: action Settings
:handler nil}])]
[view st/actions-wrapper [view st/actions-wrapper
[view st/actions-separator] [view st/actions-separator]
[view st/actions-view [view st/actions-view
@ -177,17 +179,19 @@
(fn [] (fn []
[view (st/chat-name-view @show-actions) [view (st/chat-name-view @show-actions)
[text {:style st/chat-name-text} [text {:style st/chat-name-text}
(or @name "Chat name")] (truncate-str (or @name "Chat name") 30)]
(if @group-chat (if @group-chat
[view {:flexDirection :row} [view {:flexDirection :row}
[icon :group st/group-icon] [icon :group st/group-icon]
[text {:style st/members} [text {:style st/members}
(let [cnt (count @contacts)] (let [cnt (inc (count @contacts))]
(str cnt (str cnt
(if (< 1 cnt) (if (< 1 cnt)
" members" " members"
" member") " member")
;; TODO stub data: active members
", " cnt " active"))]] ", " cnt " active"))]]
;; TODO stub data: last activity
[text {:style st/last-activity} "Active a minute ago"])]))) [text {:style st/last-activity} "Active a minute ago"])])))
(defn toolbar-action [] (defn toolbar-action []
@ -196,13 +200,12 @@
(if @show-actions (if @show-actions
[touchable-highlight [touchable-highlight
{:on-press #(dispatch [:set-show-actions false])} {:on-press #(dispatch [:set-show-actions false])}
[view st/icon-view [view st/action
[icon :up st/up-icon]]] [icon :up st/up-icon]]]
[touchable-highlight [touchable-highlight
{:on-press #(dispatch [:set-show-actions true])} {:on-press #(dispatch [:set-show-actions true])}
[view st/icon-view [view st/action
[chat-photo {}] [chat-icon]]]))))
[contact-online {:online true}]]]))))
(defn chat-toolbar [] (defn chat-toolbar []
(let [{:keys [group-chat name contacts]} (let [{:keys [group-chat name contacts]}

View File

@ -4,8 +4,11 @@
[status-im.persistence.simple-kv-store :as kv] [status-im.persistence.simple-kv-store :as kv]
[status-im.protocol.state.storage :as s] [status-im.protocol.state.storage :as s]
[status-im.models.chats :as c] [status-im.models.chats :as c]
[status-im.components.styles :refer [default-chat-color]]
[status-im.utils.utils :refer [log on-error http-post toast]] [status-im.utils.utils :refer [log on-error http-post toast]]
[status-im.utils.random :as random] [status-im.utils.random :as random]
[status-im.utils.sms-listener :refer [add-sms-listener
remove-sms-listener]]
[status-im.utils.phone-number :refer [format-phone-number]] [status-im.utils.phone-number :refer [format-phone-number]]
[status-im.constants :refer [text-content-type [status-im.constants :refer [text-content-type
content-type-command content-type-command
@ -37,19 +40,22 @@
(dispatch [:set-signed-up true])) (dispatch [:set-signed-up true]))
(defn sync-contacts [] (defn sync-contacts []
;; TODO 'on-sync-contacts' is never called
(dispatch [:sync-contacts on-sync-contacts])) (dispatch [:sync-contacts on-sync-contacts]))
(defn on-send-code-response [body] (defn on-send-code-response [body]
(dispatch [:received-msg (dispatch [:received-msg
{:msg-id (random/id) {:msg-id (random/id)
;; todo replace by real check :content (:message body)
:content (if (= "1111" body)
"Confirmed"
"Wrong code")
:content-type text-content-type :content-type text-content-type
:outgoing false :outgoing false
:from "console" :from "console"
:to "me"}])) :to "me"}])
(when (:confirmed body)
(dispatch [:stop-listening-confirmation-code-sms])
(sync-contacts)
;; TODO should be called after sync-contacts?
(dispatch [:set-signed-up true])))
; todo fn name is not too smart, but... ; todo fn name is not too smart, but...
(defn command-content (defn command-content
@ -71,6 +77,19 @@
:from "console" :from "console"
:to "me"}]))) :to "me"}])))
(defn handle-sms [{body :body}]
(when-let [matches (re-matches #"(\d{4})" body)]
(dispatch [:sign-up-confirm (second matches)])))
(defn start-listening-confirmation-code-sms [db]
(when (not (:confirmation-code-sms-listener db))
(assoc db :confirmation-code-sms-listener (add-sms-listener handle-sms))))
(defn stop-listening-confirmation-code-sms [db]
(when-let [listener (:confirmation-code-sms-listener db)]
(remove-sms-listener listener)
(dissoc db :confirmation-code-sms-listener)))
;; -- Saving password ---------------------------------------- ;; -- Saving password ----------------------------------------
(defn save-password [password] (defn save-password [password]
;; TODO validate and save password ;; TODO validate and save password
@ -174,6 +193,7 @@
(def console-chat (def console-chat
{:chat-id "console" {:chat-id "console"
:name "console" :name "console"
:color default-chat-color
:group-chat false :group-chat false
:is-active true :is-active true
:timestamp (.getTime (js/Date.)) :timestamp (.getTime (js/Date.))

View File

@ -20,6 +20,12 @@
:backgroundColor toolbar-background1 :backgroundColor toolbar-background1
:elevation 2}) :elevation 2})
(def action
{:width 56
:height 56
:alignItems :center
:justifyContent :center})
(def icon-view (def icon-view
{:width 56 {:width 56
:height 56}) :height 56})
@ -48,9 +54,7 @@
:height 9}) :height 9})
(def up-icon (def up-icon
{:marginTop 23 {:width 14
:marginLeft 21
:width 14
:height 8}) :height 8})
(def members (def members
@ -108,28 +112,6 @@
:fontSize 12 :fontSize 12
:fontFamily font}) :fontFamily font})
(def online-view
{:position :absolute
:top 30
:left 30
:width 20
:height 20
:borderRadius 50
:backgroundColor online-color
:borderWidth 2
:borderColor color-white})
(def online-dot
{:position :absolute
:top 6
:width 4
:height 4
:borderRadius 50
:backgroundColor color-white})
(def online-dot-left (merge online-dot {:left 3}))
(def online-dot-right (merge online-dot {:left 9}))
(def typing-all (def typing-all
{:marginBottom 20}) {:marginBottom 20})
@ -153,11 +135,6 @@
:fontFamily font :fontFamily font
:color text2-color}) :color text2-color})
(def chat-photo
{:borderRadius 50
:width 36
:height 36})
(def actions-overlay (def actions-overlay
{:position :absolute {:position :absolute
:top 0 :top 0
@ -167,34 +144,3 @@
(def overlay-highlight (def overlay-highlight
{:flex 1}) {:flex 1})
;;----- Menu item Profile ----------------
(def menu-item-profile-contact-photo
{:marginTop 13
:marginLeft 16
:borderRadius 50
:width 24
:height 24})
(def menu-item-profile-online-view
{:position :absolute
:top 26
:left 29
:width 15
:height 15
:borderRadius 50
:backgroundColor online-color
:borderWidth 2
:borderColor color-white})
(def menu-item-profile-online-dot
{:position :absolute
:top 4
:width 3
:height 3
:borderRadius 50
:backgroundColor color-white})
(def menu-item-profile-online-dot-left (merge menu-item-profile-online-dot {:left 1.7}))
(def menu-item-profile-online-dot-right (merge menu-item-profile-online-dot {:left 6.3}))

View File

@ -45,6 +45,7 @@
:onSubmitEditing #(try-send @chat @staged-commands-atom :onSubmitEditing #(try-send @chat @staged-commands-atom
input-message)} input-message)}
input-message] input-message]
;; TODO emoticons: not implemented
[icon :smile st/smile-icon] [icon :smile st/smile-icon]
(when (message-valid? @staged-commands-atom input-message) (when (message-valid? @staged-commands-atom input-message)
[touchable-highlight {:on-press #(send @chat input-message)} [touchable-highlight {:on-press #(send @chat input-message)}

View File

@ -12,7 +12,8 @@
[status-im.components.action-button :refer [action-button [status-im.components.action-button :refer [action-button
action-button-item]] action-button-item]]
[status-im.components.drawer.view :refer [drawer-view open-drawer]] [status-im.components.drawer.view :refer [drawer-view open-drawer]]
[status-im.components.styles :refer [color-blue]] [status-im.components.styles :refer [color-blue
toolbar-background2]]
[status-im.components.toolbar :refer [toolbar]] [status-im.components.toolbar :refer [toolbar]]
[status-im.components.main-tabs :refer [main-tabs]] [status-im.components.main-tabs :refer [main-tabs]]
[status-im.components.icons.ionicons :refer [icon]] [status-im.components.icons.ionicons :refer [icon]]
@ -23,6 +24,8 @@
:style st/hamburger-icon} :style st/hamburger-icon}
:handler open-drawer} :handler open-drawer}
:title "Chats" :title "Chats"
:background-color toolbar-background2
;; TODO implement search
:action {:image {:source {:uri :icon_search} :action {:image {:source {:uri :icon_search}
:style st/search-icon} :style st/search-icon}
:handler (fn [])}}]) :handler (fn [])}}])

View File

@ -9,49 +9,17 @@
new-messages-count-color]] new-messages-count-color]]
[status-im.components.tabs.styles :refer [tab-height]])) [status-im.components.tabs.styles :refer [tab-height]]))
(def contact-photo-container
{:borderRadius 50})
(def contact-photo-image
{:borderRadius 50
:width 40
:height 40})
(def online-container
{:position :absolute
:top 24
:left 24
:width 20
:height 20
:borderRadius 50
:backgroundColor online-color
:borderWidth 2
:borderColor color-white})
(def online-dot
{:position :absolute
:top 6
:width 4
:height 4
:borderRadius 50
:backgroundColor color-white})
(def online-dot-left
(assoc online-dot :left 3))
(def online-dot-right
(assoc online-dot :left 9))
(def chat-container (def chat-container
{:flexDirection :row {:flexDirection :row
:paddingVertical 15 :paddingVertical 15
:paddingHorizontal 16 :paddingHorizontal 16
:height 90}) :height 90})
(def photo-container (def chat-icon-container
{:marginTop 2 {:marginTop -2
:width 44 :marginLeft -4
:height 44}) :width 48
:height 48})
(def item-container (def item-container
{:flexDirection :column {:flexDirection :column

View File

@ -11,10 +11,7 @@
(defn chat-list-item [{:keys [chat-id] :as chat}] (defn chat-list-item [{:keys [chat-id] :as chat}]
[touchable-highlight [touchable-highlight
{:on-press #(dispatch [:navigate-to :chat chat-id])} {:on-press #(dispatch [:navigate-to :chat chat-id])}
;; TODO add [photo-path delivery-status new-messages-count online] values to chat-obj
[view [chat-list-item-inner-view (merge chat [view [chat-list-item-inner-view (merge chat
{:photo-path nil ;; TODO stub data
:delivery-status :seen {:new-messages-count 3
:new-messages-count 3
:timestamp "13:54"
:online true})]]]) :online true})]]])

View File

@ -1,52 +1,45 @@
(ns status-im.chats-list.views.inner-item (ns status-im.chats-list.views.inner-item
(:require [clojure.string :as s] (:require-macros [status-im.utils.views :refer [defview]])
[status-im.components.react :refer [view image icon text]] (:require [status-im.components.react :refer [view image icon text]]
[status-im.components.chat-icon.screen :refer [chat-icon-view-chat-list]]
[status-im.chats-list.styles :as st] [status-im.chats-list.styles :as st]
[status-im.resources :as res])) [status-im.utils.utils :refer [truncate-str]]
[status-im.utils.datetime :as time]))
(defn contact-photo [photo-path]
[view st/contact-photo-container
[image {:source (if (s/blank? photo-path)
res/user-no-photo
{:uri photo-path})
:style st/contact-photo-image}]])
(defn contact-online [online]
(when online
[view st/online-container
[view st/online-dot-left]
[view st/online-dot-right]]))
(defn chat-list-item-inner-view (defn chat-list-item-inner-view
[{:keys [name photo-path delivery-status timestamp new-messages-count online [{:keys [chat-id name color photo-path new-messages-count
group-chat contacts]}] online group-chat contacts] :as chat}]
(let [last-message (first (:messages chat))]
[view st/chat-container [view st/chat-container
[view st/photo-container [view st/chat-icon-container
[contact-photo photo-path] [chat-icon-view-chat-list chat-id group-chat name color online]]
[contact-online online]]
[view st/item-container [view st/item-container
[view st/name-view [view st/name-view
[text {:style st/name-text} name] [text {:style st/name-text} (truncate-str name 20)]
(when group-chat (when group-chat
[icon :group st/group-icon]) [icon :group st/group-icon])
(when group-chat (when group-chat
[text {:style st/memebers-text} [text {:style st/memebers-text}
(if (< 1 (count contacts)) (if (< 0 (count contacts))
(str (count contacts) " members") (str (inc (count contacts)) " members")
"1 member")])] "1 member")])]
[text {:style st/last-message-text [text {:style st/last-message-text
:numberOfLines 2} :numberOfLines 2}
(repeatedly 5 #(str "Hi, I'm " name "! "))]] (when last-message
(:content last-message))]]
[view [view
(when last-message
[view st/status-container [view st/status-container
(when delivery-status ;; TODO currently there is not :delivery-status in last-message
[image {:source (if (= (keyword delivery-status) :seen) (when (:delivery-status last-message)
[image {:source (if (= (keyword (:delivery-status last-message)) :seen)
{:uri :icon_ok_small} {:uri :icon_ok_small}
;; todo change icon ;; todo change icon
{:uri :icon_ok_small}) {:uri :icon_ok_small})
:style st/status-image}]) :style st/status-image}])
[text {:style st/datetime-text} timestamp]] (when (:timestamp last-message)
[text {:style st/datetime-text}
(time/to-short-str (:timestamp last-message))])])
(when (pos? new-messages-count) (when (pos? new-messages-count)
[view st/new-messages-container [view st/new-messages-container
[text {:style st/new-messages-text} new-messages-count]])]]) [text {:style st/new-messages-text} new-messages-count]])]]))

View File

@ -0,0 +1,92 @@
(ns status-im.components.chat-icon.screen
(:require-macros [status-im.utils.views :refer [defview]])
(:require [re-frame.core :refer [subscribe dispatch]]
[status-im.components.react :refer [view
text
image
icon]]
[status-im.components.chat-icon.styles :as st]
[status-im.components.styles :refer [color-purple]]))
(defn default-chat-icon [name styles]
[view (:default-chat-icon styles)
[text {:style (:default-chat-icon-text styles)}
(first name)]])
(defn chat-icon [photo-path styles]
[image {:source {:uri photo-path}
:style (:chat-icon styles)}])
(defn contact-online [online styles]
(when online
[view (:online-view styles)
[view (:online-dot-left styles)]
[view (:online-dot-right styles)]]))
(defview chat-icon-view [chat-id group-chat name online styles]
[photo-path [:chat-photo chat-id]]
[view (:container styles)
(if (and photo-path (not (empty? photo-path)))
[chat-icon photo-path styles]
[default-chat-icon name styles])
(when (not group-chat)
[contact-online online styles])])
(defn chat-icon-view-chat-list [chat-id group-chat name color online]
[chat-icon-view chat-id group-chat name online
{:container st/container-chat-list
:online-view st/online-view
:online-dot-left st/online-dot-left
:online-dot-right st/online-dot-right
:chat-icon st/chat-icon-chat-list
:default-chat-icon (st/default-chat-icon-chat-list color)
:default-chat-icon-text st/default-chat-icon-text}])
(defn chat-icon-view-action [chat-id group-chat name color online]
[chat-icon-view chat-id group-chat name online
{:container st/container
:online-view st/online-view
:online-dot-left st/online-dot-left
:online-dot-right st/online-dot-right
:chat-icon st/chat-icon
:default-chat-icon (st/default-chat-icon color)
:default-chat-icon-text st/default-chat-icon-text}])
(defn chat-icon-view-menu-item [chat-id group-chat name color online]
[chat-icon-view chat-id group-chat name online
{:container st/container-menu-item
:online-view st/online-view-menu-item
:online-dot-left st/online-dot-left-menu-item
:online-dot-right st/online-dot-right-menu-item
:chat-icon st/chat-icon-menu-item
:default-chat-icon (st/default-chat-icon-menu-item color)
:default-chat-icon-text st/default-chat-icon-text}])
(defn profile-icon-view [photo-path name color online]
(let [styles {:container st/container-profile
:online-view st/online-view-profile
:online-dot-left st/online-dot-left-profile
:online-dot-right st/online-dot-right-profile
:chat-icon st/chat-icon-profile
:default-chat-icon (st/default-chat-icon-profile color)
:default-chat-icon-text st/default-chat-icon-text}]
[view (:container styles)
(if (and photo-path (not (empty? photo-path)))
[chat-icon photo-path styles]
[default-chat-icon name styles])
[contact-online online styles]]))
(defview profile-icon []
[contact [:contact]]
(let [;; TODO stub data
online true
color color-purple]
[profile-icon-view (:photo-path contact) (:name contact) color online]))
(defview my-profile-icon []
[name [:get :username]
photo-path [:get :photo-path]]
(let [;; TODO stub data
online true
color color-purple]
[profile-icon-view photo-path name color online]))

View File

@ -0,0 +1,130 @@
(ns status-im.components.chat-icon.styles
(:require [status-im.components.styles :refer [font
title-font
color-white
chat-background
online-color
selected-message-color
separator-color
text1-color
text2-color
toolbar-background1]]))
(defn default-chat-icon [color]
{:margin 4
:width 36
:height 36
:alignItems :center
:justifyContent :center
:borderRadius 50
:backgroundColor color})
(defn default-chat-icon-chat-list [color]
(merge (default-chat-icon color)
{:width 40
:height 40}))
(defn default-chat-icon-menu-item [color]
(merge (default-chat-icon color)
{:width 24
:height 24}))
(defn default-chat-icon-profile [color]
(merge (default-chat-icon color)
{:width 64
:height 64}))
(def default-chat-icon-text
{:marginTop -2
:color color-white
:fontFamily font
:fontSize 16
:lineHeight 20})
(def chat-icon
{:margin 4
:borderRadius 50
:width 36
:height 36})
(def chat-icon-chat-list
(merge chat-icon
{:width 40
:height 40}))
(def chat-icon-menu-item
(merge chat-icon
{:width 24
:height 24}))
(def chat-icon-profile
(merge chat-icon
{:width 64
:height 64}))
(def online-view
{:position :absolute
:bottom 0
:right 0
:width 20
:height 20
:borderRadius 50
:backgroundColor online-color
:borderWidth 2
:borderColor color-white})
(def online-view-menu-item
(merge online-view
{:width 15
:height 15}))
(def online-view-profile
(merge online-view
{:width 24
:height 24}))
(def online-dot
{:position :absolute
:top 6
:width 4
:height 4
:borderRadius 50
:backgroundColor color-white})
(def online-dot-left (merge online-dot {:left 3}))
(def online-dot-right (merge online-dot {:left 9}))
(def online-dot-menu-item
(merge online-dot
{:top 4
:width 3
:height 3}))
(def online-dot-left-menu-item
(merge online-dot-menu-item {:left 1.7}))
(def online-dot-right-menu-item
(merge online-dot-menu-item {:left 6.3}))
(def online-dot-profile
(merge online-dot
{:top 8
:width 4
:height 4}))
(def online-dot-left-profile
(merge online-dot-profile {:left 5}))
(def online-dot-right-profile
(merge online-dot-profile {:left 11}))
(def container
{:width 44
:height 44})
(def container-chat-list
{:width 48
:height 48})
(def container-menu-item
{:width 32
:height 32})
(def container-profile
{:width 72
:height 72})

View File

@ -26,3 +26,4 @@
(def separator-color "#0000001f") (def separator-color "#0000001f")
(def toolbar-background1 color-white) (def toolbar-background1 color-white)
(def toolbar-background2 color-light-gray) (def toolbar-background2 color-light-gray)
(def default-chat-color color-purple)

View File

@ -4,18 +4,9 @@
(def server-address "http://rpc0.status.im:20000/") (def server-address "http://rpc0.status.im:20000/")
;; (def server-address "http://10.0.3.2:3000/") ;; (def server-address "http://10.0.3.2:3000/")
;; (def server-address "http://localhost:3000/")
(def text-content-type "text/plain") (def text-content-type "text/plain")
(def content-type-command "command") (def content-type-command "command")
(def content-type-command-request "command-request") (def content-type-command-request "command-request")
(def content-type-status "status") (def content-type-status "status")
(comment
(map (fn [c]
{:background c
:foreground c}) group-chat-colors)
(reverse group-chat-colors)
)

View File

@ -73,7 +73,7 @@
(defn request-stored-contacts [contacts] (defn request-stored-contacts [contacts]
(let [contacts-by-hash (get-contacts-by-hash contacts) (let [contacts-by-hash (get-contacts-by-hash contacts)
data (keys contacts-by-hash)] data (or (keys contacts-by-hash) ())]
(http-post "get-contacts" {:phone-number-hashes data} (http-post "get-contacts" {:phone-number-hashes data}
(fn [{:keys [contacts]}] (fn [{:keys [contacts]}]
(let [contacts' (add-identity contacts-by-hash contacts)] (let [contacts' (add-identity contacts-by-hash contacts)]

View File

@ -12,9 +12,8 @@
(let [contacts (reaction (:contacts @db))] (let [contacts (reaction (:contacts @db))]
(reaction (sort-by :name (vals @contacts)))))) (reaction (sort-by :name (vals @contacts))))))
(defn contacts-by-current-chat [fn db] (defn contacts-by-chat [fn db chat-id]
(let [current-chat-id (:current-chat-id @db) (let [chat (reaction (get-in @db [:chats chat-id]))
chat (reaction (get-in @db [:chats current-chat-id]))
contacts (reaction (:contacts @db))] contacts (reaction (:contacts @db))]
(reaction (reaction
(when @chat (when @chat
@ -25,6 +24,10 @@
(fn #(current-participants (:whisper-identity %)) (fn #(current-participants (:whisper-identity %))
(vals @contacts))))))) (vals @contacts)))))))
(defn contacts-by-current-chat [fn db]
(let [current-chat-id (:current-chat-id @db)]
(contacts-by-chat fn db current-chat-id)))
(register-sub :contact (register-sub :contact
(fn [db _] (fn [db _]
(let [identity (:contact-identity @db)] (let [identity (:contact-identity @db)]
@ -37,3 +40,15 @@
(register-sub :current-chat-contacts (register-sub :current-chat-contacts
(fn [db _] (fn [db _]
(contacts-by-current-chat filter db))) (contacts-by-current-chat filter db)))
(register-sub :chat-photo
(fn [db [_ chat-id]]
(let [chat-id (or chat-id (:current-chat-id @db))
chat (reaction (get-in @db [:chats chat-id]))
contacts (contacts-by-chat filter db chat-id)]
(reaction
(when @chat
(if (:group-chat @chat)
;; TODO return group chat icon
nil
(:photo-path (first @contacts))))))))

View File

@ -23,6 +23,7 @@
:view-id default-view :view-id default-view
:navigation-stack (list default-view) :navigation-stack (list default-view)
;; TODO fix hardcoded values ;; TODO fix hardcoded values
:photo-path nil
:username "My Name" :username "My Name"
:phone-number "3147984309" :phone-number "3147984309"
:email "myemail@gmail.com" :email "myemail@gmail.com"

View File

@ -12,6 +12,7 @@
scroll-view scroll-view
touchable-highlight]] touchable-highlight]]
[status-im.components.toolbar :refer [toolbar]] [status-im.components.toolbar :refer [toolbar]]
[status-im.components.chat-icon.screen :refer [chat-icon-view-action]]
[status-im.group-settings.styles.group-settings :as st] [status-im.group-settings.styles.group-settings :as st]
[status-im.group-settings.views.member :refer [member-view]])) [status-im.group-settings.views.member :refer [member-view]]))
@ -21,6 +22,7 @@
(defn close-member-menu [] (defn close-member-menu []
(dispatch [:set :selected-participants #{}])) (dispatch [:set :selected-participants #{}]))
;; TODO not in design
(defview member-menu [] (defview member-menu []
[{:keys [name] :as participant} [:selected-participant]] [{:keys [name] :as participant} [:selected-participant]]
(when participant (when participant
@ -61,6 +63,7 @@
(close-chat-color-picker) (close-chat-color-picker)
(dispatch [:set-chat-color])) (dispatch [:set-chat-color]))
;; TODO not in design
(defview chat-color-picker [] (defview chat-color-picker []
[show-color-picker [:group-settings :show-color-picker] [show-color-picker [:group-settings :show-color-picker]
new-color [:get :new-chat-color]] new-color [:get :new-chat-color]]
@ -88,10 +91,10 @@
(dispatch [:group-settings :show-color-picker true])) (dispatch [:group-settings :show-color-picker true]))
(defn settings-view [] (defn settings-view []
;; TODO implement settings handlers
(let [settings [{:custom-icon [chat-color-icon] (let [settings [{:custom-icon [chat-color-icon]
:title "Change color" :title "Change color"
:handler show-chat-color-picker} :handler show-chat-color-picker}
;; TODO not implemented: Notifications
(merge {:title "Notifications and sounds" (merge {:title "Notifications and sounds"
:subtitle "!not implemented" :subtitle "!not implemented"
:handler nil} :handler nil}
@ -106,21 +109,25 @@
:icon-style {:width 12 :icon-style {:width 12
:height 12} :height 12}
:title "Clear history" :title "Clear history"
;; TODO show confirmation dialog?
:handler #(dispatch [:clear-history])} :handler #(dispatch [:clear-history])}
{:icon :bin {:icon :bin
:icon-style {:width 12 :icon-style {:width 12
:height 18} :height 18}
:title "Delete and leave" :title "Delete and leave"
;; TODO show confirmation dialog?
:handler #(dispatch [:leave-group-chat])}]] :handler #(dispatch [:leave-group-chat])}]]
[view st/settings-container [view st/settings-container
(for [setting settings] (for [setting settings]
^{:key setting} [setting-view setting])])) ^{:key setting} [setting-view setting])]))
(defview chat-icon [] (defview chat-icon []
[name [:chat :name] [chat-id [:chat :chat-id]
group-chat [:chat :group-chat]
name [:chat :name]
color [:chat :color]] color [:chat :color]]
[view (st/chat-icon color) [view st/action
[text {:style st/chat-icon-text} (first name)]]) [chat-icon-view-action chat-id group-chat name color false]])
(defn new-group-toolbar [] (defn new-group-toolbar []
[toolbar {:title "Chat settings" [toolbar {:title "Chat settings"
@ -164,6 +171,7 @@
[chat-name] [chat-name]
[text {:style st/members-text} "Members"] [text {:style st/members-text} "Members"]
[touchable-highlight {:on-press #(dispatch [:navigate-to :add-participants])} [touchable-highlight {:on-press #(dispatch [:navigate-to :add-participants])}
;; TODO add participants view is not in design
[view st/add-members-container [view st/add-members-container
[icon :add-gray st/add-members-icon] [icon :add-gray st/add-members-icon]
[text {:style st/add-members-text} [text {:style st/add-members-text}

View File

@ -52,20 +52,11 @@
(def chat-members-container (def chat-members-container
{:marginBottom 10}) {:marginBottom 10})
(defn chat-icon [color] (def action
{:margin 10 {:width 56
:width 36 :height 56
:height 36 :alignItems :center
:borderRadius 50 :justifyContent :center})
:backgroundColor color})
(def chat-icon-text
{:marginTop 7
:marginLeft 13
:color color-white
:fontFamily font
:fontSize 16
:lineHeight 20})
(def group-settings (def group-settings
{:flex 1 {:flex 1

View File

@ -1,6 +1,7 @@
(ns status-im.new-group.handlers (ns status-im.new-group.handlers
(:require [status-im.protocol.api :as api] (:require [status-im.protocol.api :as api]
[re-frame.core :refer [register-handler after dispatch debug enrich]] [re-frame.core :refer [register-handler after dispatch debug enrich]]
[status-im.components.styles :refer [default-chat-color]]
[status-im.models.chats :as chats] [status-im.models.chats :as chats]
[clojure.string :as s])) [clojure.string :as s]))
@ -37,6 +38,7 @@
(group-name-from-contacts db))] (group-name-from-contacts db))]
(assoc db :new-chat {:chat-id new-group-id (assoc db :new-chat {:chat-id new-group-id
:name chat-name :name chat-name
:color default-chat-color
:group-chat true :group-chat true
:is-active true :is-active true
:timestamp (.getTime (js/Date.)) :timestamp (.getTime (js/Date.))

View File

@ -1,5 +1,6 @@
(ns status-im.persistence.realm (ns status-im.persistence.realm
(:require [cljs.reader :refer [read-string]] (:require [cljs.reader :refer [read-string]]
[status-im.components.styles :refer [default-chat-color]]
[status-im.utils.logging :as log] [status-im.utils.logging :as log]
[status-im.utils.types :refer [to-string]]) [status-im.utils.types :refer [to-string]])
(:refer-clojure :exclude [exists?])) (:refer-clojure :exclude [exists?]))
@ -44,7 +45,7 @@
:properties {:chat-id "string" :properties {:chat-id "string"
:name "string" :name "string"
:color {:type "string" :color {:type "string"
:default "#a187d5"} :default default-chat-color}
:group-chat {:type "bool" :group-chat {:type "bool"
:indexed true} :indexed true}
:is-active "bool" :is-active "bool"

View File

@ -1,7 +1,6 @@
(ns status-im.profile.screen (ns status-im.profile.screen
(:require-macros [status-im.utils.views :refer [defview]]) (:require-macros [status-im.utils.views :refer [defview]])
(:require [clojure.string :as s] (:require [re-frame.core :refer [subscribe dispatch]]
[re-frame.core :refer [subscribe dispatch]]
[status-im.components.react :refer [view [status-im.components.react :refer [view
text text
image image
@ -9,21 +8,10 @@
scroll-view scroll-view
touchable-highlight touchable-highlight
touchable-opacity]] touchable-opacity]]
[status-im.resources :as res] [status-im.components.chat-icon.screen :refer [profile-icon
my-profile-icon]]
[status-im.profile.styles :as st])) [status-im.profile.styles :as st]))
(defn user-photo [{:keys [photo-path]}]
[image {:source (if (s/blank? photo-path)
res/user-no-photo
{:uri photo-path})
:style st/user-photo}])
(defn user-online [{:keys [online]}]
(when online
[view st/user-online-container
[view st/user-online-dot-left]
[view st/user-online-dot-right]]))
(defn profile-property-view [{:keys [name value]}] (defn profile-property-view [{:keys [name value]}]
[view st/profile-property-view-container [view st/profile-property-view-container
[view st/profile-property-view-sub-container [view st/profile-property-view-sub-container
@ -43,9 +31,9 @@
[icon :back st/back-btn-icon]]] [icon :back st/back-btn-icon]]]
[view st/status-block [view st/status-block
[view st/user-photo-container [view st/user-photo-container
[user-photo {}] [profile-icon]]
[user-online {:online true}]]
[text {:style st/user-name} name] [text {:style st/user-name} name]
;; TODO stub data
[text {:style st/status} "!not implemented"] [text {:style st/status} "!not implemented"]
[view st/btns-container [view st/btns-container
[touchable-highlight {:onPress #(message-user whisper-identity)} [touchable-highlight {:onPress #(message-user whisper-identity)}
@ -61,14 +49,18 @@
:value name}] :value name}]
[profile-property-view {:name "Phone number" [profile-property-view {:name "Phone number"
:value phone-number}] :value phone-number}]
;; TODO stub data
[profile-property-view {:name "Email" [profile-property-view {:name "Email"
:value "!not implemented"}] :value "!not implemented"}]
[view st/report-user-container [view st/report-user-container
[touchable-opacity {} [touchable-highlight {:on-press (fn []
;; TODO not implemented
)}
[text {:style st/report-user-text} "REPORT USER"]]]]]) [text {:style st/report-user-text} "REPORT USER"]]]]])
(defview my-profile [] (defview my-profile []
[username [:get :username] [username [:get :username]
photo-path [:get :photo-path]
phone-number [:get :phone-number] phone-number [:get :phone-number]
email [:get :email] email [:get :email]
status [:get :status]] status [:get :status]]
@ -85,8 +77,7 @@
[icon :dots st/actions-btn-icon]]] [icon :dots st/actions-btn-icon]]]
[view st/status-block [view st/status-block
[view st/user-photo-container [view st/user-photo-container
[user-photo {}] [my-profile-icon]]
[user-online {:online true}]]
[text {:style st/user-name} username] [text {:style st/user-name} username]
[text {:style st/status} status]] [text {:style st/status} status]]
[view st/profile-properties-container [view st/profile-properties-container

View File

@ -11,37 +11,6 @@
text1-color text1-color
text2-color]])) text2-color]]))
(def user-photo
{:borderRadius 50
:width 64
:height 64})
(def user-online-container
{:position :absolute
:top 44
:left 44
:width 24
:height 24
:borderRadius 50
:backgroundColor online-color
:borderWidth 2
:borderColor color-white})
(def user-online-dot
{:position :absolute
:top 8
:left 5
:width 4
:height 4
:borderRadius 50
:backgroundColor color-white})
(def user-online-dot-left
(assoc user-online-dot :left 5))
(def user-online-dot-right
(assoc user-online-dot :left 11))
(def profile-property-view-container (def profile-property-view-container
{:height 85 {:height 85
:paddingHorizontal 16}) :paddingHorizontal 16})
@ -101,10 +70,10 @@
:width 249}) :width 249})
(def user-photo-container (def user-photo-container
{:marginTop 26}) {:marginTop 22})
(def user-name (def user-name
{:marginTop 20 {:marginTop 16
:fontSize 18 :fontSize 18
:fontFamily font :fontFamily font
:color text1-color}) :color text1-color})

View File

@ -0,0 +1,24 @@
(ns status-im.utils.datetime
(:require [cljs-time.core :as t :refer [date-time now plus days hours before?]]
[cljs-time.coerce :refer [from-long to-long]]
[cljs-time.format :as format :refer [formatters
formatter
unparse]]))
(def time-zone-offset (hours (- (/ (.getTimezoneOffset (js/Date.)) 60))))
(defn to-short-str [ms]
(let [date (from-long ms)
local (plus date time-zone-offset)
today-date (t/today)
today (date-time (t/year today-date)
(t/month today-date)
(t/day today-date))
yesterday (plus today (days -1))]
(cond
(before? local yesterday) (unparse (formatter "dd MMM") local)
(before? local today) "Yesterday"
:else (unparse (formatters :hour-minute) local))))
(defn now-ms []
(to-long (now)))

View File

@ -0,0 +1,19 @@
(ns status-im.utils.sms-listener
(:require [status-im.components.react :refer [android?]]))
(def sms-listener (.-default (js/require "react-native-android-sms-listener")))
;; Only android is supported!
(defn add-sms-listener
"Message format: {:originatingAddress string, :body string}. Returns
cancelable subscription."
[listen-fn]
(when android?
(.addListener sms-listener
(fn [message]
(listen-fn (js->clj message :keywordize-keys true))))))
(defn remove-sms-listener [subscription]
(when android?
(.remove subscription)))

View File

@ -47,3 +47,8 @@
(.catch (or on-error (.catch (or on-error
(fn [error] (fn [error]
(toast (str error)))))))) (toast (str error))))))))
(defn truncate-str [s max]
(if (< max (count s))
(str (subs s 0 (- max 3)) "...")
s))