diff --git a/.re-natal b/.re-natal index d4461b52f3..f91fe6bc29 100644 --- a/.re-natal +++ b/.re-natal @@ -25,7 +25,8 @@ "react-native-fs", "react-native-dialogs", "react-native-image-resizer", - "react-native-image-crop-picker" + "react-native-image-crop-picker", + "react-native-webview-bridge" ], "imageDirs": [ "images" @@ -35,4 +36,4 @@ "dev": "env/dev", "prod": "env/prod" } -} \ No newline at end of file +} diff --git a/android/app/build.gradle b/android/app/build.gradle index 5cc175be39..32424dea01 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -137,6 +137,7 @@ dependencies { compile project(':react-native-orientation') compile project(':react-native-fs') compile project(':react-native-image-crop-picker') + compile project(':react-native-webview-bridge') //compile(name:'statusgo-android-16', ext:'aar') compile(group: 'status-im', name: 'status-go', version: 'unlock', ext: 'aar') diff --git a/android/app/src/main/java/com/statusim/MainApplication.java b/android/app/src/main/java/com/statusim/MainApplication.java index 1d5cb26cae..9c9a881d40 100644 --- a/android/app/src/main/java/com/statusim/MainApplication.java +++ b/android/app/src/main/java/com/statusim/MainApplication.java @@ -22,6 +22,7 @@ import com.statusim.geth.module.GethPackage; import com.aakashns.reactnativedialogs.ReactNativeDialogsPackage; import fr.bamlab.rnimageresizer.ImageResizerPackage; import com.reactnative.picker.PickerPackage; +import com.github.alinz.reactnativewebviewbridge.WebViewBridgePackage; import java.util.Arrays; import java.util.List; @@ -52,7 +53,9 @@ public class MainApplication extends Application implements ReactApplication { new GethPackage(), new ReactNativeDialogsPackage(), new ImageResizerPackage(), - new PickerPackage() + new PickerPackage(), + new WebViewBridgePackage() + ); } }; diff --git a/android/settings.gradle b/android/settings.gradle index f45aecd942..d68bcdd911 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -27,5 +27,9 @@ include ':react-native-orientation', ':app' project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android') include ':react-native-fs' project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android') + include ':react-native-image-crop-picker' project(':react-native-image-crop-picker').projectDir = new File(settingsDir, '../node_modules/react-native-image-crop-picker/android') + +include ':react-native-webview-bridge' +project(':react-native-webview-bridge').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview-bridge/android') diff --git a/package.json b/package.json index 5cb7b77261..161c8f3446 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "react-native-tcp": "^1.0.1", "react-native-udp": "^1.2.5", "react-native-vector-icons": "^2.0.3", + "react-native-webview-bridge": "github:rasom/react-native-webview-bridge#master", "readable-stream": "^1.0.33", "realm": "^0.14.0", "stream-browserify": "^1.0.0", diff --git a/resources/commands.js b/resources/commands.js index 9ce9076b9f..4a8b7eb216 100644 --- a/resources/commands.js +++ b/resources/commands.js @@ -274,6 +274,7 @@ function validateBalance(params) { ] }; } + var balance = web3.eth.getBalance(params.command.address); if (bn(val).greaterThan(bn(balance))) { return { diff --git a/resources/status.js b/resources/status.js index 85fc8eca79..b5642359a1 100644 --- a/resources/status.js +++ b/resources/status.js @@ -131,6 +131,9 @@ var status = { var response = new Response(); return response.create(h); }, + autorun: function (commandName) { + _status_catalog.autorun = commandName; + }, types: { TEXT: 'text', NUMBER: 'number', diff --git a/resources/wallet.js b/resources/wallet.js new file mode 100644 index 0000000000..23e6b739ee --- /dev/null +++ b/resources/wallet.js @@ -0,0 +1,20 @@ +function wallet() { + var url = 'http://127.0.0.1:3450'; + + return {webViewUrl: url}; +} + +status.command({ + name: "wallet", + description: "wallet", + color: "#ffa500", + fullscreen: true, + suggestionsTrigger: 'on-send', + params: [{ + name: "webpage", + suggestions: wallet, + type: status.types.TEXT + }] +}); + +status.autorun("wallet"); diff --git a/resources/webview.js b/resources/webview.js new file mode 100644 index 0000000000..b5c7510b74 --- /dev/null +++ b/resources/webview.js @@ -0,0 +1,26 @@ +(function () { + window.statusAPI = { + dispatch: function (event, options) { + console.log("statusAPI.dispatch: " + JSON.stringify(options)); + if (options.callback) { + console.log(options.callback); + statusAPI.callbacks[event] = options.callback; + } + var json = JSON.stringify({ + event: event, + options: options + }); + console.log("sending from webview: " + json); + WebViewBridge.send(json); + }, + callbacks: {} + }; + + WebViewBridge.onMessage = function (messageString) { + console.log("received from react-native: " + messageString); + var message = JSON.parse(messageString); + if (statusAPI.callbacks[message.event]) { + statusAPI.callbacks[message.event](message.params); + } + }; +}()); diff --git a/src/status_im/accounts/handlers.cljs b/src/status_im/accounts/handlers.cljs index fc4d12aa9b..bfeece8859 100644 --- a/src/status_im/accounts/handlers.cljs +++ b/src/status_im/accounts/handlers.cljs @@ -45,11 +45,13 @@ (dispatch-sync [:add-account account]) (dispatch [:login-account address password]))))) -(register-handler - :create-account - (fn [db [_ password]] - (geth/create-account password (fn [result] (account-created result password))) - db)) +(register-handler :create-account + (after #(dispatch [:init-wallet-chat])) + (u/side-effect! + (fn [_ [_ password]] + (geth/create-account + password + #(account-created % password))))) (defn save-account-to-realm! [{:keys [current-account-id accounts]} _] diff --git a/src/status_im/chat/handlers.cljs b/src/status_im/chat/handlers.cljs index d8334e0e13..3e747182c8 100644 --- a/src/status_im/chat/handlers.cljs +++ b/src/status_im/chat/handlers.cljs @@ -1,6 +1,7 @@ (ns status-im.chat.handlers (:require-macros [cljs.core.async.macros :as am]) - (:require [re-frame.core :refer [enrich after debug dispatch path]] + (:require [re-frame.core :refer [enrich after debug dispatch]] + [status-im.models.commands :as commands] [clojure.string :as str] [status-im.components.styles :refer [default-chat-color]] @@ -30,7 +31,9 @@ status-im.chat.handlers.unviewed-messages status-im.chat.handlers.send-message status-im.chat.handlers.receive-message - [cljs.core.async :as a])) + [cljs.core.async :as a] + status-im.chat.handlers.webview-bridge + status-im.chat.handlers.wallet-chat)) (register-handler :set-show-actions (fn [db [_ show-actions]] @@ -250,17 +253,18 @@ init-chat)))) (defn prepare-chat - [{:keys [contacts] :as db} [_ contact-id]] - (let [name (get-in contacts [contact-id :name]) - chat {:chat-id contact-id - :name (or name contact-id) - :color default-chat-color - :group-chat false - :is-active true - :timestamp (.getTime (js/Date.)) - :contacts [{:identity contact-id}] - :dapp-url nil - :dapp-hash nil}] + [{:keys [contacts] :as db} [_ contcat-id options]] + (let [name (get-in contacts [contcat-id :name]) + chat (merge {:chat-id contcat-id + :name (or name contcat-id) + :color default-chat-color + :group-chat false + :is-active true + :timestamp (.getTime (js/Date.)) + :contacts [{:identity contcat-id}] + :dapp-url nil + :dapp-hash nil} + options)] (assoc db :new-chat chat))) (defn add-chat [{:keys [new-chat] :as db} [_ chat-id]] @@ -273,15 +277,22 @@ (chats/create-chat new-chat)) (defn open-chat! - [_ [_ chat-id]] - (dispatch [:navigate-to :chat chat-id])) + [_ [_ chat-id _ navigation-type]] + (dispatch [(or navigation-type :navigate-to) :chat chat-id])) -(register-handler :start-chat +(register-handler ::start-chat! (-> prepare-chat ((enrich add-chat)) ((after save-chat!)) ((after open-chat!)))) +(register-handler :start-chat + (u/side-effect! + (fn [{:keys [chats]} [_ contcat-id options navigation-type]] + (if (chats contcat-id) + (dispatch [(or navigation-type :navigate-to) :chat contcat-id]) + (dispatch [::start-chat! contcat-id options navigation-type]))))) + (register-handler :add-chat (-> prepare-chat ((enrich add-chat)) @@ -392,3 +403,14 @@ (if (get-in db [:chats chat-id]) (update-in db [:chats chat-id] merge new-chat-data) db))) + +(register-handler :check-autorun + (u/side-effect! + (fn [{:keys [current-chat-id] :as db}] + (let [autorun (get-in db [:chats current-chat-id :autorun])] + (when autorun + (am/go + ;;todo: find another way to make it work... + (a/clj message-string) + event (keyword (:event message))] + (log/debug (str "message from webview: " message)) + (case event + :webview-send-transaction (dispatch [:show-contacts contacts-click-handler]) + (log/error (str "Unknown event: " event))))))) + +(register-handler :send-to-webview-bridge + (u/side-effect! + (fn [{:keys [webview-bridge]} [_ data]] + (when webview-bridge + (.sendToBridge webview-bridge (t/clj->json data)))))) diff --git a/src/status_im/chat/screen.cljs b/src/status_im/chat/screen.cljs index fa0fc594eb..b87c2012c9 100644 --- a/src/status_im/chat/screen.cljs +++ b/src/status_im/chat/screen.cljs @@ -288,24 +288,25 @@ [animated-view {:style (st/messages-container messages-offset)} messages])}))) -(defview chat [{platform-specific :platform-specific}] - [group-chat [:chat :group-chat] - show-actions-atom [:show-actions] - command [:get-chat-command] - command? [:command?] - suggestions [:get-suggestions] - to-msg-id [:get-chat-command-to-msg-id] - layout-height [:get :layout-height]] - [view {:style st/chat-view - :onLayout (fn [event] - (let [height (.. event -nativeEvent -layout -height)] - (when (not= height layout-height) - (dispatch [:set-layout-height height]))))} - [chat-toolbar platform-specific] - [messages-container - [messages-view platform-specific group-chat]] - (when group-chat [typing-all platform-specific]) - [response-view] - (when-not command? [suggestion-container]) - [chat-message-new platform-specific] - (when show-actions-atom [actions-view platform-specific])]) +(defn chat [{platform-specific :platform-specific}] + (let [group-chat (subscribe [:chat :group-chat]) + show-actions (subscribe [:show-actions]) + command? (subscribe [:command?]) + layout-height (subscribe [:get :layout-height])] + (r/create-class + {:component-did-mount #(dispatch [:check-autorun]) + :reagent-render + (fn [{platform-specific :platform-specific}] + [view {:style st/chat-view + :onLayout (fn [event] + (let [height (.. event -nativeEvent -layout -height)] + (when (not= height @layout-height) + (dispatch [:set-layout-height height]))))} + [chat-toolbar platform-specific] + [messages-container + [messages-view platform-specific @group-chat]] + (when @group-chat [typing-all platform-specific]) + [response-view] + (when-not @command? [suggestion-container]) + [chat-message-new platform-specific] + (when @show-actions [actions-view platform-specific])])}))) diff --git a/src/status_im/chat/views/message_input.cljs b/src/status_im/chat/views/message_input.cljs index 28759ea9e7..4376a6f9b9 100644 --- a/src/status_im/chat/views/message_input.cljs +++ b/src/status_im/chat/views/message_input.cljs @@ -39,7 +39,7 @@ :onChangeText (when-not disable? command/set-input-message) :onSubmitEditing (on-press-commands-handler command)}) -(defview message-input [input-options {:keys [suggestions-trigger] :as command}] +(defview message-input [input-options command] [command? [:command?] input-message [:get-chat-input-text] input-command [:get-chat-command-content] @@ -66,7 +66,7 @@ [view st/input-view [plain-message/commands-button] [message-input-container - [message-input input-options]] + [message-input input-options command]] ;; TODO emoticons: not implemented [plain-message/smile-button] (when (or command? valid-plain-message?) diff --git a/src/status_im/chat/views/response.cljs b/src/status_im/chat/views/response.cljs index f1378f607f..2caafd4f6f 100644 --- a/src/status_im/chat/views/response.cljs +++ b/src/status_im/chat/views/response.cljs @@ -1,6 +1,7 @@ (ns status-im.chat.views.response (:require-macros [reagent.ratom :refer [reaction]] - [status-im.utils.views :refer [defview]]) + [status-im.utils.views :refer [defview]] + [status-im.utils.slurp :refer [slurp]]) (:require [re-frame.core :refer [subscribe dispatch]] [reagent.core :as r] [status-im.components.react :refer [view @@ -18,7 +19,8 @@ [status-im.components.animation :as anim] [status-im.chat.suggestions-responder :as resp] [status-im.chat.constants :as c] - [status-im.chat.views.command-validation :as cv])) + [status-im.chat.views.command-validation :as cv] + [status-im.components.webview-bridge :refer [webview-bridge]])) (defn drag-icon [] [view st/drag-container @@ -97,10 +99,14 @@ (defview suggestions-web-view [] [url [:web-view-url]] (when url - [web-view {:source {:uri url} - :java-script-enabled true - :style {:height 300} - :on-navigation-state-change on-navigation-change}])) + [webview-bridge + {:ref #(dispatch [:set-webview-bridge %]) + :on-bridge-message #(dispatch [:webview-bridge-message %]) + :source {:uri url} + :java-script-enabled true + :injected-java-script (slurp "resources/webview.js") + :style {:height 300} + :on-navigation-state-change on-navigation-change}])) (defview placeholder [] [suggestions [:get-content-suggestions]] diff --git a/src/status_im/commands/handlers/loading.cljs b/src/status_im/commands/handlers/loading.cljs index acea64c314..f3d874e876 100644 --- a/src/status_im/commands/handlers/loading.cljs +++ b/src/status_im/commands/handlers/loading.cljs @@ -23,12 +23,18 @@ (defn fetch-commands! [db [identity]] (when true - ;;when-let [url (:dapp-url (get-in db [:chats identity]))] - ;; todo fix this after demo - (if true - ;(= "console" identity) + ;-let [url (get-in db [:chats identity :dapp-url])] + (cond + (= "console" identity) + (dispatch [::validate-hash identity (slurp "resources/commands.js")]) + + (= "wallet" identity) + (dispatch [::validate-hash identity (slurp "resources/wallet.js")]) + + :else (dispatch [::validate-hash identity (slurp "resources/commands.js")]) #_(http-get (s/join "/" [url commands-js]) + #(dispatch [::validate-hash identity %]) #(dispatch [::loading-failed! identity ::file-was-not-found]))))) @@ -69,11 +75,12 @@ (into {}))) (defn add-commands - [db [id _ {:keys [commands responses]}]] + [db [id _ {:keys [commands responses autorun] :as data}]] (-> db (update-in [id :commands] merge (mark-as :command commands)) (update-in [id :responses] merge (mark-as :response responses)) - (assoc-in [id :commands-loaded] true))) + (assoc-in [id :commands-loaded] true) + (assoc-in [id :autorun] autorun))) (defn save-commands-js! [_ [id file]] @@ -101,7 +108,26 @@ (reg-handler ::add-commands [(path :chats) - (after save-commands-js!)] + (after save-commands-js!) + (after #(dispatch [:check-autorun])) + (after (fn [_ [id]] + (dispatch [:invoke-commands-loading-callbacks id])))] add-commands) (reg-handler ::loading-failed! (u/side-effect! loading-failed!)) + +(reg-handler :add-commands-loading-callback + (fn [db [chat-id callback]] + (update-in db [::commands-callbacks chat-id] conj callback))) + +(reg-handler :invoke-commands-loading-callbacks + (u/side-effect! + (fn [db [chat-id]] + (let [callbacks (get-in db [::commands-callbacks chat-id])] + (doseq [callback callbacks] + (callback)) + (dispatch [::clear-commands-callbacks chat-id]))))) + +(reg-handler ::clear-commands-callbacks + (fn [db [chat-id]] + (assoc-in db [::commands-callbacks chat-id] nil))) diff --git a/src/status_im/components/drawer/view.cljs b/src/status_im/components/drawer/view.cljs index bf8b5e63bd..43e56a6eb1 100644 --- a/src/status_im/components/drawer/view.cljs +++ b/src/status_im/components/drawer/view.cljs @@ -7,7 +7,6 @@ view text image - navigator drawer-layout-android touchable-opacity]] [status-im.resources :as res] @@ -64,7 +63,7 @@ :handler #(dispatch [:navigate-to :discovery]) :platform-specific platform-specific}] [menu-item {:name (label :t/contacts) - :handler #(dispatch [:show-contacts navigator]) + :handler #(dispatch [:show-contacts]) :platform-specific platform-specific}] [menu-item {:name (label :t/invite-friends) :handler (fn [] diff --git a/src/status_im/components/webview_bridge.cljs b/src/status_im/components/webview_bridge.cljs new file mode 100644 index 0000000000..becbfb33b8 --- /dev/null +++ b/src/status_im/components/webview_bridge.cljs @@ -0,0 +1,9 @@ +(ns status-im.components.webview-bridge + (:require [status-im.utils.utils :as u] + [reagent.core :as r])) + +(def webview-bridge-class + (r/adapt-react-class (u/require "react-native-webview-bridge"))) + +(defn webview-bridge [opts] + [webview-bridge-class opts]) diff --git a/src/status_im/contacts/screen.cljs b/src/status_im/contacts/screen.cljs index 12b64cbc16..eeaacc1d1d 100644 --- a/src/status_im/contacts/screen.cljs +++ b/src/status_im/contacts/screen.cljs @@ -11,7 +11,7 @@ list-item] :as react] [status-im.components.action-button :refer [action-button action-button-item]] - [status-im.contacts.views.contact :refer [contact-extended-view]] + [status-im.contacts.views.contact :refer [contact-extended-view on-press]] [status-im.components.status-bar :refer [status-bar]] [status-im.components.toolbar :refer [toolbar]] [status-im.components.drawer.view :refer [open-drawer]] @@ -41,7 +41,7 @@ (def contacts-limit 10) -(defn contact-group [contacts contacts-count title group top?] +(defn contact-group [contacts contacts-count title group top? click-handler] [view st/contact-group [view st/contact-group-header (when-not top? @@ -55,9 +55,14 @@ ;; todo what if there is no contacts, should we show some information ;; about this? [view {:flexDirection :column} - (for [contact contacts] + (doall ;; TODO not imlemented: contact more button handler - ^{:key contact} [contact-extended-view contact nil nil])] + (map (fn [contact] + (let [whisper-identity (:whisper-identity contact) + click-handler (or click-handler on-press)] + ^{:key contact} + [contact-extended-view contact nil (click-handler whisper-identity) nil])) + contacts))] (when (= contacts-limit (count contacts)) [view st/show-all [touchable-highlight {:on-press #(dispatch [:show-group-contacts group])} @@ -66,6 +71,7 @@ (defn contact-list [{platform-specific :platform-specific}] (let [contacts (subscribe [:get-contacts-with-limit contacts-limit]) contcats-count (subscribe [:contacts-count]) + click-handler (subscribe [:get :contacts-click-handler]) show-toolbar-shadow? (r/atom false)] (fn [] [view st/contacts-list-container @@ -79,17 +85,18 @@ :onScroll (fn [e] (let [offset (.. e -nativeEvent -contentOffset -y)] (reset! show-toolbar-shadow? (<= st/contact-group-header-height offset))))} - ;; TODO not implemented: dapps and persons separation [contact-group @contacts @contcats-count (label :t/contacs-group-dapps) - :dapps true] + :dapps true + @click-handler] [contact-group @contacts @contcats-count (label :t/contacs-group-people) - :people false]] + :people false + @click-handler]] [view st/empty-contact-groups [react/icon :group_big st/empty-contacts-icon] [text {:style st/empty-contacts-text} (label :t/no-contacts)]]) diff --git a/src/status_im/contacts/views/contact.cljs b/src/status_im/contacts/views/contact.cljs index b63ea9c31e..707aff4f7e 100644 --- a/src/status_im/contacts/views/contact.cljs +++ b/src/status_im/contacts/views/contact.cljs @@ -1,7 +1,7 @@ (ns status-im.contacts.views.contact (:require-macros [status-im.utils.views :refer [defview]]) (:require [status-im.components.react :refer [view text icon touchable-highlight]] - [re-frame.core :refer [dispatch subscribe]] + [re-frame.core :refer [dispatch]] [status-im.contacts.styles :as st] [status-im.contacts.views.contact-inner :refer [contact-inner-view]])) @@ -10,23 +10,21 @@ (when letter [text {:style st/letter-text} letter])]) -(defn on-press [chat whisper-identity] - (if chat - #(dispatch [:navigate-to :chat whisper-identity]) - #(dispatch [:start-chat whisper-identity]))) +(defn on-press [whisper-identity] + #(dispatch [:start-chat whisper-identity])) (defview contact-with-letter-view [{:keys [whisper-identity letter] :as contact}] [chat [:get-chat whisper-identity]] [touchable-highlight - {:onPress (on-press chat whisper-identity)} + {:onPress (on-press whisper-identity)} [view st/contact-container [letter-view letter] [contact-inner-view contact]]]) -(defview contact-extended-view [{:keys [whisper-identity] :as contact} info more-click-handler] +(defview contact-extended-view [{:keys [whisper-identity] :as contact} info click-handler more-click-handler] [chat [:get-chat whisper-identity]] [touchable-highlight - {:onPress (on-press chat whisper-identity)} + {:onPress click-handler} [view st/contact-container [contact-inner-view contact info] [touchable-highlight diff --git a/src/status_im/contacts/views/contact_list.cljs b/src/status_im/contacts/views/contact_list.cljs index 5387794b19..6d36ad08c8 100644 --- a/src/status_im/contacts/views/contact_list.cljs +++ b/src/status_im/contacts/views/contact_list.cljs @@ -6,7 +6,7 @@ touchable-highlight list-view list-item]] - [status-im.contacts.views.contact :refer [contact-with-letter-view]] + [status-im.contacts.views.contact :refer [contact-with-letter-view on-press]] [status-im.components.status-bar :refer [status-bar]] [status-im.components.toolbar :refer [toolbar]] [status-im.components.drawer.view :refer [drawer-view open-drawer]] @@ -20,9 +20,12 @@ [status-im.i18n :refer [label]] [status-im.components.styles :as cst])) -(defn render-row [row _ _] - (list-item [contact-with-letter-view row])) - +(defn render-row [click-handler] + (fn [row _ _] + (list-item [contact-with-letter-view row + (or click-handler + (let [whisper-identity (:whisper-identity row)] + (on-press whisper-identity)))]))) (defview contact-list-toolbar [platform-specific] [group [:get :contacts-group]] @@ -37,14 +40,16 @@ :handler (fn [])}}]]) (defview contact-list [{platform-specific :platform-specific}] - [contacts [:contacts-with-letters]] - [drawer-view {:platform-specific platform-specific} - [view st/contacts-list-container - [contact-list-toolbar platform-specific] - ;; todo what if there is no contacts, should we show some information - ;; about this? - (when contacts - [list-view {:dataSource (lw/to-datasource contacts) - :enableEmptySections true - :renderRow render-row - :style st/contacts-list}])]]) + [contacts [:contacts-with-letters] + click-handler [:get :contacts-click-handler]] + (let [click-handler click-handler] + [drawer-view {:platform-specific platform-specific} + [view st/contacts-list-container + [contact-list-toolbar platform-specific] + ;; todo what if there is no contacts, should we show some information + ;; about this? + (when contacts + [list-view {:dataSource (lw/to-datasource contacts) + :enableEmptySections true + :renderRow (render-row click-handler) + :style st/contacts-list}])]])) diff --git a/src/status_im/navigation/handlers.cljs b/src/status_im/navigation/handlers.cljs index 270738d551..67b3111b3f 100644 --- a/src/status_im/navigation/handlers.cljs +++ b/src/status_im/navigation/handlers.cljs @@ -66,8 +66,14 @@ (assoc :new-chat-name nil))))) (register-handler :show-contacts - (fn [db _] - (push-view db :contact-list))) + (fn [db [_ click-handler]] + (-> db + (assoc :contacts-click-handler click-handler) + (push-view :contact-list)))) + +(register-handler :remove-contacts-click-handler + (fn [db] + (dissoc db :contacts-click-handler))) (register-handler :show-group-contacts (fn [db [_ group]]