diff --git a/.re-natal b/.re-natal index 4fecaf5ca5..a547f09695 100644 --- a/.re-natal +++ b/.re-natal @@ -18,7 +18,8 @@ "react-native-linear-gradient", "react-native-android-sms-listener", "react-native-camera", - "react-native-qrcode" + "react-native-qrcode", + "identicon.js" ], "imageDirs": [ "images" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d8e1a14016..42b5ee2517 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,11 @@ + diff --git a/android/app/src/main/java/com/statusim/GethService.java b/android/app/src/main/java/com/statusim/GethService.java new file mode 100644 index 0000000000..7967807cbd --- /dev/null +++ b/android/app/src/main/java/com/statusim/GethService.java @@ -0,0 +1,136 @@ +package com.statusim; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.AsyncTask; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.support.annotation.Nullable; +import android.util.Log; +import android.os.Environment; + +import java.lang.ref.WeakReference; + +import com.github.ethereum.go_ethereum.cmd.Geth; + +import java.io.File; + +public class GethService extends Service { + + private static final String TAG = "GethService"; + + private static boolean isGethStarted = false; + private static boolean isGethInitialized = false; + private final Handler handler = new Handler(); + + static class IncomingHandler extends Handler { + + private final WeakReference service; + + IncomingHandler(GethService service) { + + this.service = new WeakReference(service); + } + + @Override + public void handleMessage(Message message) { + + GethService service = this.service.get(); + if (service != null) { + if (!service.handleMessage(message)) { + super.handleMessage(message); + } + } + } + } + + final Messenger serviceMessenger = new Messenger(new IncomingHandler(this)); + + protected class StartTask extends AsyncTask { + + public StartTask() { + } + + protected Void doInBackground(Void... args) { + startGeth(); + return null; + } + + protected void onPostExecute(Void results) { + onGethStarted(); + } + } + + protected void onGethStarted() { + Log.d(TAG, "Geth Service started"); + isGethStarted = true; + } + + protected void startGeth() { + Log.d(TAG, "Starting background Geth Service"); + + File extStore = Environment.getExternalStorageDirectory(); + + final String dataFolder = extStore.exists() ? + extStore.getAbsolutePath() : + getApplicationInfo().dataDir; + + final Runnable addPeer = new Runnable() { + public void run() { + Log.w("Geth", "adding peer"); + Geth.run("--exec admin.addPeer(\"enode://e2f28126720452aa82f7d3083e49e6b3945502cb94d9750a15e27ee310eed6991618199f878e5fbc7dfa0e20f0af9554b41f491dc8f1dbae8f0f2d37a3a613aa@139.162.13.89:55555\") attach http://localhost:8545"); + } + }; + + new Thread(new Runnable() { + public void run() { + Geth.run("--shh --ipcdisable --nodiscover --rpc --rpcapi db,eth,net,web3,shh,admin --fast --datadir=" + dataFolder); + } + }).start(); + + handler.postDelayed(addPeer, 5000); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return serviceMessenger.getBinder(); + } + + @Override + public void onCreate() { + super.onCreate(); + System.loadLibrary("gethraw"); + System.loadLibrary("geth"); + + if (!isGethInitialized) { + isGethInitialized = true; + new StartTask().execute(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + //TODO: stop geth + isGethStarted = false; + isGethInitialized = false; + Log.d(TAG, "Geth Service stopped !"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return Service.START_STICKY; + } + + protected boolean handleMessage(Message message) { + return false; + } + + public static boolean isRunning() { + return isGethInitialized; + } +} diff --git a/android/app/src/main/java/com/statusim/MainActivity.java b/android/app/src/main/java/com/statusim/MainActivity.java index cf17707c29..341732d5d7 100644 --- a/android/app/src/main/java/com/statusim/MainActivity.java +++ b/android/app/src/main/java/com/statusim/MainActivity.java @@ -7,17 +7,24 @@ import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; import com.rt2zz.reactnativecontacts.ReactNativeContacts; import android.os.Bundle; -import android.os.Environment; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnCancelListener; -import com.github.ethereum.go_ethereum.cmd.Geth; +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.content.Intent; +import android.content.Context; + import com.bitgo.randombytes.RandomBytesPackage; import com.BV.LinearGradient.LinearGradientPackage; import com.centaurwarchief.smslistener.SmsListener; -import android.os.Handler; + import android.util.Log; import java.util.Arrays; @@ -32,45 +39,69 @@ import io.realm.react.RealmReactPackage; public class MainActivity extends ReactActivity { - final Handler handler = new Handler(); + private static final String TAG = "MainActivity"; + + /** + * Incoming message handler. Calls to its binder are sequential! + */ + protected final IncomingHandler handler = new IncomingHandler(); + + /** Flag indicating if the service is bound. */ + protected boolean isBound; + + /** Sends messages to the service. */ + protected Messenger serviceMessenger = null; + + /** Receives messages from the service. */ + protected Messenger clientMessenger = new Messenger(handler); + + class IncomingHandler extends Handler { + + @Override + public void handleMessage(Message message) { + boolean isClaimed = false; + Log.d(TAG, "!!!!!!!!!!!!!! Received Service Message !!!!!!!!!!!!!!"); + super.handleMessage(message); + } + } + + protected ServiceConnection serviceConnection = new ServiceConnection() { + + public void onServiceConnected(ComponentName className, IBinder service) { + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + serviceMessenger = new Messenger(service); + isBound = true; + onConnected(); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + serviceMessenger = null; + isBound = false; + Log.d(TAG, "!!!!!!!!!!!!!! Geth Service Disconnected !!!!!!!!!!!!!!"); + } + }; + + protected void onConnected() { + Log.d(TAG, "!!!!!!!!!!!!!! Geth Service Connected !!!!!!!!!!!!!!"); + } protected void startStatus() { // Required because of crazy APN settings redirecting localhost (found in GB) Properties properties = System.getProperties(); properties.setProperty("http.nonProxyHosts", "localhost|127.0.0.1"); properties.setProperty("https.nonProxyHosts", "localhost|127.0.0.1"); - - File extStore = Environment.getExternalStorageDirectory(); - - final String dataFolder = extStore.exists() ? - extStore.getAbsolutePath() : - getApplicationInfo().dataDir; - - // Launch! - final Runnable addPeer = new Runnable() { - public void run() { - Log.w("Geth", "adding peer"); - Geth.run("--exec admin.addPeer(\"enode://e2f28126720452aa82f7d3083e49e6b3945502cb94d9750a15e27ee310eed6991618199f878e5fbc7dfa0e20f0af9554b41f491dc8f1dbae8f0f2d37a3a613aa@139.162.13.89:55555\") attach http://localhost:8545"); - } - }; - new Thread(new Runnable() { - public void run() { - Geth.run("--shh --ipcdisable --nodiscover --rpc --rpcapi db,eth,net,web3,shh,admin --fast --datadir=" + dataFolder); - - } - }).start(); - handler.postDelayed(addPeer, 5000); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Required for android-16 (???) - // Crash if put in startStatus() ? - System.loadLibrary("gethraw"); - System.loadLibrary("geth"); - if(!RootUtil.isDeviceRooted()) { startStatus(); } else { @@ -96,9 +127,27 @@ public class MainActivity extends ReactActivity { }).create(); dialog.show(); } + Intent intent = new Intent(this, GethService.class); + if (!GethService.isRunning()) { + startService(intent); + } + if (serviceConnection != null && GethService.isRunning()) { + bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + } } + @Override + protected void onDestroy() { + super.onDestroy(); + try { + unbindService(serviceConnection); + } + catch (Throwable t) { + Log.e(TAG, "Failed to unbind from the geth service", t); + } + } + /** * Returns the name of the main component registered from JavaScript. * This is used to schedule rendering of the component. diff --git a/package.json b/package.json index b26cf32faf..b8c7a1a2a8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "dependencies": { "awesome-phonenumber": "^1.0.13", + "identicon.js": "github:status-im/identicon.js", "react": "^0.14.5", "react-native": "^0.24.1", "react-native-action-button": "^1.1.4", @@ -16,7 +17,7 @@ "react-native-contacts": "^0.2.4", "react-native-i18n": "0.0.8", "react-native-invertible-scroll-view": "^1.0.0", - "react-native-linear-gradient": "^1.5.7", + "react-native-linear-gradient": "1.5.7", "react-native-loading-spinner-overlay": "0.0.8", "react-native-qrcode": "^0.2.2", "react-native-randombytes": "^2.1.0", diff --git a/src/status_im/chat/views/message_input.cljs b/src/status_im/chat/views/message_input.cljs index 4c26e15eb8..e40cfa4f8f 100644 --- a/src/status_im/chat/views/message_input.cljs +++ b/src/status_im/chat/views/message_input.cljs @@ -6,8 +6,7 @@ animated-view icon touchable-highlight - text-input - dismiss-keyboard!]] + text-input]] [status-im.components.animation :as anim] [status-im.chat.views.plain-message :as plain-message] [status-im.chat.views.command :as command] @@ -61,8 +60,7 @@ staged-commands [:get-chat-staged-commands] typing-command? [:typing-command?] commands-input-is-switching? [:animations :commands-input-is-switching?]] - (let [dismiss-keyboard (not (or command typing-command?)) - response? (and command to-msg-id) + (let [response? (and command to-msg-id) message-input? (or (not command) commands-input-is-switching?) animation? commands-input-is-switching?] [text-input (merge {:style (cond @@ -72,7 +70,7 @@ :ref (fn [input] (dispatch [:set-message-input input])) :autoFocus false - :blurOnSubmit dismiss-keyboard + :blurOnSubmit false :onChangeText (fn [text] (when-not animation? ((if message-input? @@ -82,8 +80,7 @@ :onSubmitEditing #(when-not animation? (if message-input? (plain-message/try-send staged-commands - input-message - dismiss-keyboard) + input-message) (command/try-send input-command validator)))} (when command {:accessibility-label :command-input}) @@ -100,8 +97,7 @@ staged-commands [:get-chat-staged-commands] typing-command? [:typing-command?] commands-input-is-switching? [:animations :commands-input-is-switching?]] - (let [dismiss-keyboard (not (or command typing-command?)) - response? (and command to-msg-id) + (let [response? (and command to-msg-id) message-input? (or (not command) commands-input-is-switching?)] [view st/input-container [view st/input-view @@ -117,8 +113,7 @@ (if message-input? (when (plain-message/message-valid? staged-commands input-message) [send-button {:on-press #(plain-message/try-send staged-commands - input-message - dismiss-keyboard) + input-message) :accessibility-label :send-message}]) (if (command/valid? input-command validator) [send-button {:on-press command/send-command diff --git a/src/status_im/chat/views/plain_message.cljs b/src/status_im/chat/views/plain_message.cljs index 7c637ebbd0..0284e0e25f 100644 --- a/src/status_im/chat/views/plain_message.cljs +++ b/src/status_im/chat/views/plain_message.cljs @@ -5,8 +5,7 @@ [status-im.components.react :refer [view animated-view icon - touchable-highlight - dismiss-keyboard!]] + touchable-highlight]] [status-im.components.animation :as anim] [status-im.chat.styles.plain-message :as st] [status-im.constants :refer [response-input-hiding-duration]])) @@ -14,19 +13,14 @@ (defn set-input-message [message] (dispatch [:set-chat-input-text message])) -(defn send [dismiss-keyboard] - (when dismiss-keyboard - (dismiss-keyboard!)) - (dispatch [:send-chat-msg])) - (defn message-valid? [staged-commands message] (or (and (pos? (count message)) (not= "!" message)) (pos? (count staged-commands)))) -(defn try-send [staged-commands message dismiss-keyboard] +(defn try-send [staged-commands message] (when (message-valid? staged-commands message) - (send dismiss-keyboard))) + (dispatch [:send-chat-msg]))) (defn prepare-message-input [message-input] (when message-input diff --git a/src/status_im/chats_list/screen.cljs b/src/status_im/chats_list/screen.cljs index ae6219d985..19b9dfeca1 100644 --- a/src/status_im/chats_list/screen.cljs +++ b/src/status_im/chats_list/screen.cljs @@ -1,8 +1,10 @@ (ns status-im.chats-list.screen + (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch]] [status-im.components.react :refer [list-view list-item view + animated-view text image touchable-highlight]] @@ -13,45 +15,72 @@ action-button-item]] [status-im.components.drawer.view :refer [drawer-view open-drawer]] [status-im.components.styles :refer [color-blue + toolbar-background1 toolbar-background2]] [status-im.components.toolbar :refer [toolbar]] [status-im.components.icons.ionicons :refer [icon]] [status-im.i18n :refer [label]] - [status-im.chats-list.styles :as st])) + [status-im.chats-list.styles :as st] + [status-im.components.tabs.styles :refer [tabs-height]])) -(defn chats-list-toolbar [] +(defview chats-list-toolbar [] + [chats-scrolled? [:get :chats-scrolled?]] [toolbar {:nav-action {:image {:source {:uri :icon_hamburger} :style st/hamburger-icon} :handler open-drawer} :title (label :t/chats) - :background-color toolbar-background2 + :background-color (if chats-scrolled? + toolbar-background1 + toolbar-background2) ;; TODO implement search :action {:image {:source {:uri :icon_search} :style st/search-icon} :handler (fn [])}}]) (defn chats-list [] - (let [chats (subscribe [:get :chats])] + (let [chats (subscribe [:get :chats]) + chats-scrolled? (subscribe [:get :chats-scrolled?]) + animation? (subscribe [:animations :tabs-bar-animation?]) + tabs-bar-value (subscribe [:animations :tabs-bar-value]) + container-height (r/atom 0) + content-height (r/atom 0)] + (dispatch [:set :chats-scrolled? false]) (fn [] [drawer-view [view st/chats-container [chats-list-toolbar] - [list-view {:dataSource (to-datasource @chats) - :renderRow (fn [row _ _] - (list-item [chat-list-item row])) - :style st/list-container}] - [action-button {:buttonColor color-blue - :offsetY 16 - :offsetX 16} - [action-button-item - {:title (label :t/new-chat) - :buttonColor :#9b59b6 - :onPress #(dispatch [:show-group-contacts :people])} - [icon {:name :android-create - :style st/create-icon}]] - [action-button-item - {:title (label :t/new-group-chat) - :buttonColor :#1abc9c - :onPress #(dispatch [:show-group-new])} - [icon {:name :person-stalker - :style st/person-stalker-icon}]]]]]))) + [list-view {:dataSource (to-datasource @chats) + :renderRow (fn [row _ _] + (list-item [chat-list-item row])) + :style st/list-container + ;;; if "maximazing" chat list will make scroll to 0, + ;;; then disable maximazing + :onLayout (fn [event] + (when-not @chats-scrolled? + (let [height (.. event -nativeEvent -layout -height)] + (reset! container-height height)))) + :onContentSizeChange (fn [width height] + (reset! content-height height)) + :onScroll (fn [e] + (let [offset (.. e -nativeEvent -contentOffset -y) + min-content-height (+ @container-height tabs-height) + scrolled? (and (< 0 offset) (< min-content-height @content-height))] + (dispatch [:set :chats-scrolled? scrolled?]) + (dispatch [:set-animation :tabs-bar-animation? true])))}] + [animated-view {:style (st/action-buttons-container @animation? (or @tabs-bar-value 0)) + :pointerEvents :box-none} + [action-button {:buttonColor color-blue + :offsetY 16 + :offsetX 16} + [action-button-item + {:title (label :t/new-chat) + :buttonColor :#9b59b6 + :onPress #(dispatch [:show-group-contacts :people])} + [icon {:name :android-create + :style st/create-icon}]] + [action-button-item + {:title (label :t/new-group-chat) + :buttonColor :#1abc9c + :onPress #(dispatch [:show-group-new])} + [icon {:name :person-stalker + :style st/person-stalker-icon}]]]]]]))) diff --git a/src/status_im/chats_list/styles.cljs b/src/status_im/chats_list/styles.cljs index ca72f89042..6621aedfce 100644 --- a/src/status_im/chats_list/styles.cljs +++ b/src/status_im/chats_list/styles.cljs @@ -6,7 +6,8 @@ online-color text1-color text2-color - new-messages-count-color]])) + new-messages-count-color]] + [status-im.components.tabs.styles :refer [tabs-height]])) (def chat-container {:flexDirection :row @@ -113,3 +114,11 @@ {:fontSize 20 :height 22 :color :white}) + +(defn action-buttons-container [animation? offset-y] + {:position :absolute + :left 0 + :right 0 + :top 0 + :bottom 0 + :transform [{:translateY (if animation? offset-y 1)}]}) diff --git a/src/status_im/components/tabs/styles.cljs b/src/status_im/components/tabs/styles.cljs index 45af60e439..2d873e88cd 100644 --- a/src/status_im/components/tabs/styles.cljs +++ b/src/status_im/components/tabs/styles.cljs @@ -10,21 +10,21 @@ text2-color toolbar-background1]])) +(def tabs-height 59) (def tab-height 56) -(def tabs - {:flex 1 - :position :absolute - :bottom 0 - :right 0 - :left 0 - }) +(defn tabs-container [hidden? animation? offset-y] + {:height tabs-height + :backgroundColor color-white + :marginBottom (if (or hidden? animation?) + (- tabs-height) 0) + :transform [{:translateY (if animation? offset-y 1)}]}) (def top-gradient {:flexDirection :row :height 3}) -(def tabs-container +(def tabs-inner-container {:flexDirection :row :height tab-height :opacity 1 @@ -55,10 +55,9 @@ :alignItems :center}) (defn tab-view-container [offset-x] - {:flex 1 - :position :absolute + {:position :absolute :top 0 :left 0 :right 0 - :bottom tab-height + :bottom 0 :transform [{:translateX offset-x}]}) diff --git a/src/status_im/components/tabs/tabs.cljs b/src/status_im/components/tabs/tabs.cljs index bd12614f3f..a6d10d5d8d 100644 --- a/src/status_im/components/tabs/tabs.cljs +++ b/src/status_im/components/tabs/tabs.cljs @@ -2,6 +2,7 @@ (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] [status-im.components.react :refer [view + animated-view text-input text image @@ -9,7 +10,8 @@ linear-gradient]] [reagent.core :as r] [status-im.components.tabs.styles :as st] - [status-im.components.tabs.tab :refer [tab]])) + [status-im.components.tabs.tab :refer [tab]] + [status-im.components.animation :as anim])) (defn create-tab [index data selected-view-id] (let [data (merge data {:key index @@ -17,10 +19,43 @@ :selected-view-id selected-view-id})] [tab data])) -(defview tabs [{:keys [style tab-list selected-view-id]}] - (let [style (merge st/tabs style)] - [view {:style style} - [linear-gradient {:colors ["rgba(24, 52, 76, 0.01)" "rgba(24, 52, 76, 0.085)" "rgba(24, 52, 76, 0.165)"] - :style st/top-gradient}] - [view st/tabs-container - (doall (map-indexed #(create-tab %1 %2 selected-view-id) tab-list))]])) +(defn animation-logic [{:keys [hidden? val]}] + (let [was-hidden? (atom (not @hidden?))] + (fn [_] + (when (not= @was-hidden? @hidden?) + (let [to-value (if @hidden? 0 (- st/tabs-height))] + (swap! was-hidden? not) + (anim/start + (anim/timing val {:toValue to-value + :duration 300}) + (fn [e] + ;; if to-value was changed, then new animation has started + (when (= to-value (if @hidden? 0 (- st/tabs-height))) + (dispatch [:set-animation :tabs-bar-animation? false]))))))))) + +(defn tabs-container [& children] + (let [chats-scrolled? (subscribe [:get :chats-scrolled?]) + animation? (subscribe [:animations :tabs-bar-animation?]) + tabs-bar-value (subscribe [:animations :tabs-bar-value]) + context {:hidden? chats-scrolled? + :val @tabs-bar-value} + on-update (animation-logic context)] + (anim/set-value @tabs-bar-value 0) + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [& children] + @chats-scrolled? + (into [animated-view {:style (st/tabs-container @chats-scrolled? @animation? @tabs-bar-value) + :pointerEvents (if @chats-scrolled? :none :auto)}] + children))}))) + +(defn tabs [{:keys [tab-list selected-view-id]}] + [tabs-container + [linear-gradient {:colors ["rgba(24, 52, 76, 0.01)" "rgba(24, 52, 76, 0.085)" "rgba(24, 52, 76, 0.165)"] + :style st/top-gradient}] + [view st/tabs-inner-container + (doall (map-indexed #(create-tab %1 %2 selected-view-id) tab-list))]]) diff --git a/src/status_im/contacts/views/new_contact.cljs b/src/status_im/contacts/views/new_contact.cljs index 469ed6b914..4af9d3c5d4 100644 --- a/src/status_im/contacts/views/new_contact.cljs +++ b/src/status_im/contacts/views/new_contact.cljs @@ -7,6 +7,7 @@ image linear-gradient touchable-highlight]] + [status-im.utils.identicon :refer [identicon]] [status-im.components.toolbar :refer [toolbar]] [status-im.components.drawer.view :refer [drawer-view open-drawer]] [status-im.components.styles :refer [color-purple @@ -71,7 +72,7 @@ :custom-content toolbar-title :action {:image {:source {:uri :icon_add} :style icon-search} - :handler #(dispatch [:add-new-contact new-contact])}}] + :handler #(dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}] [view st/form-container [contact-whisper-id-input whisper-identity] [contact-name-input name] diff --git a/src/status_im/db.cljs b/src/status_im/db.cljs index 2b3ad6f88f..25772a58fa 100644 --- a/src/status_im/db.cljs +++ b/src/status_im/db.cljs @@ -44,7 +44,8 @@ :message-input-buttons-scale 1 :messages-offset 0 :commands-input-is-switching? false - :response-resize? false}}) + :response-resize? false + :tabs-bar-value (anim/create-value 0)}}) (def protocol-initialized-path [:protocol-initialized]) (defn chat-input-text-path [chat-id] diff --git a/src/status_im/models/contacts.cljs b/src/status_im/models/contacts.cljs index ec286b17a6..6c3ea62a7a 100644 --- a/src/status_im/models/contacts.cljs +++ b/src/status_im/models/contacts.cljs @@ -1,5 +1,6 @@ (ns status-im.models.contacts (:require [status-im.persistence.realm :as r] + [status-im.utils.identicon :refer [identicon]] [status-im.persistence.realm-queries :refer [include-query exclude-query]])) @@ -8,9 +9,9 @@ (r/sorted :name :asc) r/collection->map)) -(defn create-contact [{:keys [name photo-path] :as contact}] +(defn create-contact [{:keys [name photo-path whisper-identity] :as contact}] (->> {:name (or name "") - :photo-path (or photo-path "")} + :photo-path (or photo-path (identicon whisper-identity))} (merge contact) (r/create :contacts))) diff --git a/src/status_im/utils/identicon.cljs b/src/status_im/utils/identicon.cljs new file mode 100644 index 0000000000..2d8dac64e1 --- /dev/null +++ b/src/status_im/utils/identicon.cljs @@ -0,0 +1,13 @@ +(ns status-im.utils.identicon + (:require [clojure.string :as s] + [status-im.utils.utils :as u])) + +(def default-size 40) + +(def identicon-js (u/require "identicon.js")) + +(defn identicon + ([hash] (identicon hash default-size)) + ([hash options] + (str "data:image/png;base64," (.toString (new identicon-js hash options))))) +