diff --git a/.re-natal b/.re-natal index 4fecaf5ca5..74c8704bf6 100644 --- a/.re-natal +++ b/.re-natal @@ -17,8 +17,11 @@ "dismissKeyboard", "react-native-linear-gradient", "react-native-android-sms-listener", + "react-native-status", "react-native-camera", - "react-native-qrcode" + "react-native-qrcode", + "react-native-orientation", + "identicon.js" ], "imageDirs": [ "images" diff --git a/android/app/build.gradle b/android/app/build.gradle index 21e197ea42..d2e57e779b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -131,8 +131,9 @@ dependencies { compile project(':react-native-linear-gradient') compile project(':ReactNativeAndroidSmsListener') compile project(':react-native-camera') -// compile(name:'geth', ext:'aar') - compile(group: 'status-im', name: 'android-geth', version: '1.4.0-201604110816-a97a114', ext: 'aar') + compile project(':react-native-status') + compile project(':react-native-orientation') + compile(group: 'status-im', name: 'status-go', version: '0.1.0-201606301634-5d7b29', ext: 'aar') compile fileTree(dir: "node_modules/realm/android/libs", include: ["*.jar"]) } @@ -142,3 +143,4 @@ task copyDownloadableDepsToLibs(type: Copy) { from configurations.compile into 'libs' } + 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..7a11e0e739 --- /dev/null +++ b/android/app/src/main/java/com/statusim/GethService.java @@ -0,0 +1,129 @@ +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.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.status_im.status_go.Statusgo; + +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; + + new Thread(new Runnable() { + public void run() { + Statusgo.StartNode(dataFolder); + } + }).start(); + } + + public void signalEvent(String jsonEvent) { + + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return serviceMessenger.getBinder(); + } + + @Override + public void onCreate() { + super.onCreate(); + System.loadLibrary("statusgo"); + + 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..4524cb19cb 100644 --- a/android/app/src/main/java/com/statusim/MainActivity.java +++ b/android/app/src/main/java/com/statusim/MainActivity.java @@ -7,70 +7,104 @@ 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 com.github.yamill.orientation.OrientationPackage; + -import android.os.Handler; import android.util.Log; import java.util.Arrays; import java.util.List; import java.util.Properties; import java.io.File; +import com.statusim.Jail.JailPackage; import com.lwansbrough.RCTCamera.*; import com.i18n.reactnativei18n.ReactNativeI18n; import io.realm.react.RealmReactPackage; - +import android.content.Intent; +import android.content.res.Configuration; 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 +130,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. @@ -125,6 +177,7 @@ public class MainActivity extends ReactActivity { protected List getPackages() { return Arrays.asList( new MainReactPackage(), + new JailPackage(), new RealmReactPackage(), new VectorIconsPackage(), new ReactNativeContacts(), @@ -132,7 +185,16 @@ public class MainActivity extends ReactActivity { new RandomBytesPackage(), new LinearGradientPackage(), new RCTCameraPackage(), - new SmsListener(this) + new SmsListener(this), + new OrientationPackage(this) ); } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + Intent intent = new Intent("onConfigurationChanged"); + intent.putExtra("newConfig", newConfig); + this.sendBroadcast(intent); + } } diff --git a/android/app/src/main/res/drawable-hdpi/icon_close_white.png b/android/app/src/main/res/drawable-hdpi/icon_close_white.png new file mode 100644 index 0000000000..79f55e700a Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_close_white.png differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_dollar_green.png b/android/app/src/main/res/drawable-hdpi/icon_dollar_green.png new file mode 100644 index 0000000000..32cf51e421 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_dollar_green.png differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_drag_white.png b/android/app/src/main/res/drawable-hdpi/icon_drag_white.png new file mode 100644 index 0000000000..6a079b6f6f Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_drag_white.png differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_group_big.png b/android/app/src/main/res/drawable-hdpi/icon_group_big.png new file mode 100644 index 0000000000..68687a6dfa Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_group_big.png differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_input_list.png b/android/app/src/main/res/drawable-hdpi/icon_input_list.png new file mode 100644 index 0000000000..033344a21f Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_input_list.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_close_white.png b/android/app/src/main/res/drawable-mdpi/icon_close_white.png new file mode 100644 index 0000000000..956a2c04fb Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_close_white.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_dollar_green.png b/android/app/src/main/res/drawable-mdpi/icon_dollar_green.png new file mode 100644 index 0000000000..fc6f77f756 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_dollar_green.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_drag_white.png b/android/app/src/main/res/drawable-mdpi/icon_drag_white.png new file mode 100644 index 0000000000..7845600020 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_drag_white.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_group_big.png b/android/app/src/main/res/drawable-mdpi/icon_group_big.png new file mode 100644 index 0000000000..c4bf8a7989 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_group_big.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_input_list.png b/android/app/src/main/res/drawable-mdpi/icon_input_list.png new file mode 100644 index 0000000000..df86c2c636 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_input_list.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_close_white.png b/android/app/src/main/res/drawable-xhdpi/icon_close_white.png new file mode 100644 index 0000000000..5c8005f904 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_close_white.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_dollar_green.png b/android/app/src/main/res/drawable-xhdpi/icon_dollar_green.png new file mode 100644 index 0000000000..cdea0d4af4 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_dollar_green.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_drag_white.png b/android/app/src/main/res/drawable-xhdpi/icon_drag_white.png new file mode 100644 index 0000000000..2b200673bb Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_drag_white.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_group_big.png b/android/app/src/main/res/drawable-xhdpi/icon_group_big.png new file mode 100644 index 0000000000..8b47926213 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_group_big.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_input_list.png b/android/app/src/main/res/drawable-xhdpi/icon_input_list.png new file mode 100644 index 0000000000..45c40f49c8 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_input_list.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_close_white.png b/android/app/src/main/res/drawable-xxhdpi/icon_close_white.png new file mode 100644 index 0000000000..32e84becb6 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_close_white.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_dollar_green.png b/android/app/src/main/res/drawable-xxhdpi/icon_dollar_green.png new file mode 100644 index 0000000000..dac30955fe Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_dollar_green.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_drag_white.png b/android/app/src/main/res/drawable-xxhdpi/icon_drag_white.png new file mode 100644 index 0000000000..23fa971c5e Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_drag_white.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_group_big.png b/android/app/src/main/res/drawable-xxhdpi/icon_group_big.png new file mode 100644 index 0000000000..a1bff88bb0 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_group_big.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_input_list.png b/android/app/src/main/res/drawable-xxhdpi/icon_input_list.png new file mode 100644 index 0000000000..9e4fd528b3 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_input_list.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_close_white.png b/android/app/src/main/res/drawable-xxxhdpi/icon_close_white.png new file mode 100644 index 0000000000..68dbc7fb36 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_close_white.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_dollar_green.png b/android/app/src/main/res/drawable-xxxhdpi/icon_dollar_green.png new file mode 100644 index 0000000000..1f3c1c57d1 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_dollar_green.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_drag_white.png b/android/app/src/main/res/drawable-xxxhdpi/icon_drag_white.png new file mode 100644 index 0000000000..fd3f08e9ae Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_drag_white.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_group_big.png b/android/app/src/main/res/drawable-xxxhdpi/icon_group_big.png new file mode 100644 index 0000000000..bf47ac4f7b Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_group_big.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_input_list.png b/android/app/src/main/res/drawable-xxxhdpi/icon_input_list.png new file mode 100644 index 0000000000..d136f96ef8 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_input_list.png differ diff --git a/android/settings.gradle b/android/settings.gradle index 8dc71f3210..c238c1e7d2 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -13,10 +13,16 @@ project(':react-native-vector-icons').projectDir = new File(rootProject.projectD include ':realm' project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android') 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/android') include ':react-native-linear-gradient' 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') + +include ':react-native-status' +project(':react-native-status').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-status/android') include ':react-native-camera' project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android') + +include ':react-native-orientation', ':app' +project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android') diff --git a/package.json b/package.json index 4bc3d4994d..7890c53cb7 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,11 +17,13 @@ "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.0.0", + "react-native-randombytes": "^2.1.0", "react-native-vector-icons": "^1.3.4", - "realm": "^0.11.1" + "react-native-orientation": "^1.17.0", + "realm": "^0.11.1", + "react-native-status": "git+ssh://git@github.com/status-im/react-native-status" } } diff --git a/resources/commands.js b/resources/commands.js new file mode 100644 index 0000000000..1191fdd311 --- /dev/null +++ b/resources/commands.js @@ -0,0 +1,209 @@ +status.command({ + name: "location", + description: "Send location", + color: "#9a5dcf", + preview: function (params) { + var text = status.components.text( + { + style: { + marginTop: 5, + marginHorizontal: 0, + fontSize: 14, + fontFamily: "font", + color: "black" + } + }, params.value); + var uri = "https://maps.googleapis.com/maps/api/staticmap?center=" + + params.value + + "&size=100x100&maptype=roadmap&key=AIzaSyBNsj1qoQEYPb3IllmWMAscuXW0eeuYqAA&language=en" + + "&markers=size:mid%7Ccolor:0xff0000%7Clabel:%7C" + + params.value; + + var image = status.components.image( + { + source: {uri: uri}, + style: { + width: 100, + height: 100 + } + } + ); + + return status.components.view({}, [text, image]); + } +}).param({ + name: "address", + type: status.types.TEXT, + placeholder: "Address" +}); + +var phones = [ + { + number: "89171111111", + description: "Number format 1" + }, + { + number: "89371111111", + description: "Number format 1" + }, + { + number: "+79171111111", + description: "Number format 2" + }, + { + number: "9171111111", + description: "Number format 3" + } +]; + +function suggestionsContainerStyle(suggestionsCount) { + return { + marginVertical: 1, + marginHorizontal: 0, + height: Math.min(150, (56 * suggestionsCount)), + backgroundColor: "white", + borderRadius: 5 + }; +} + +var suggestionContainerStyle = { + paddingLeft: 16, + backgroundColor: "white" +}; + +var suggestionSubContainerStyle = { + height: 56, + borderBottomWidth: 1, + borderBottomColor: "#0000001f" +}; + +var valueStyle = { + marginTop: 9, + fontSize: 14, + fontFamily: "font", + color: "#000000de" +}; + +var descriptionStyle = { + marginTop: 1.5, + fontSize: 14, + fontFamily: "font", + color: "#838c93de" +}; + +function startsWith(str1, str2) { + // String.startsWith(...) doesn't work in otto + return str1.lastIndexOf(str2, 0) == 0 && str1 != str2; +} + +function phoneSuggestions(params) { + var ph, suggestions; + if (!params.value || params.value == "") { + ph = phones; + } else { + ph = phones.filter(function (phone) { + return startsWith(phone.number, params.value); + }); + } + + if (ph.length == 0) { + return; + } + + suggestions = ph.map(function (phone) { + return status.components.touchable( + {onPress: [status.events.SET_VALUE, phone.number]}, + status.components.view(suggestionContainerStyle, + [status.components.view(suggestionSubContainerStyle, + [ + status.components.text( + {style: valueStyle}, + phone.number + ), + status.components.text( + {style: descriptionStyle}, + phone.description + ) + ])]) + ); + }); + + return status.components.scrollView( + suggestionsContainerStyle(ph.length), + suggestions + ); +} + +status.response({ + name: "phone", + description: "Send phone number", + color: "#5fc48d", + params: [{ + name: "phone", + type: status.types.PHONE, + suggestions: phoneSuggestions, + placeholder: "Phone number" + }], + handler: function (params) { + return { + event: "sign-up", + params: [params.value] + }; + } +}); + +status.command({ + name: "help", + description: "Help", + color: "#7099e6", + params: [{ + name: "query", + type: status.types.TEXT + }] +}); + +status.response({ + name: "confirmation-code", + color: "#7099e6", + description: "Confirmation code", + params: [{ + name: "code", + type: status.types.NUMBER + }], + handler: function (params) { + return { + event: "confirm-sign-up", + params: [params.value] + }; + } +}); + +status.response({ + name: "keypair", + color: "#7099e6", + description: "Keypair password", + icon: "icon_lock_white", + params: [{ + name: "password", + type: status.types.PASSWORD + }], + handler: function (params) { + return { + event: "save-password", + params: [params.value] + }; + }, + preview: function (params) { + return status.components.text( + { + style: { + marginTop: 5, + marginHorizontal: 0, + fontSize: 14, + fontFamily: "font", + color: "black" + } + }, "*****"); + } +}); + diff --git a/resources/status.js b/resources/status.js new file mode 100644 index 0000000000..2eec9a3d44 --- /dev/null +++ b/resources/status.js @@ -0,0 +1,110 @@ +var _status_catalog = { + commands: {}, + responses: {} +}; + +function Command() { +} +function Response() { +} + +Command.prototype.addToCatalog = function () { + _status_catalog.commands[this.name] = this; +}; + +Command.prototype.param = function (parameter) { + this.params.push(parameter); + + return this; +}; + +Command.prototype.create = function (com) { + this.name = com.name; + this.description = com.description; + this.handler = com.handler; + this.color = com.color; + this.icon = com.icon; + this.params = com.params || []; + this.preview = com.preview; + this.addToCatalog(); + + return this; +}; + + +Response.prototype = Object.create(Command.prototype); +Response.prototype.addToCatalog = function () { + _status_catalog.responses[this.name] = this; +}; +Response.prototype.onReceiveResponse = function (handler) { + this.onReceive = handler; +}; + +function call(pathStr, paramsStr) { + var params = JSON.parse(paramsStr), + path = JSON.parse(pathStr), + fn, res; + + fn = path.reduce(function (catalog, name) { + if (catalog && catalog[name]) { + return catalog[name]; + } + }, + _status_catalog + ); + + if(!fn) { + return null; + } + + res = fn(params); + + return JSON.stringify(res); +} + +function text(options, s) { + return ['text', options, s]; +} + +function view(options, elements) { + return ['view', options].concat(elements); +} + +function image(options) { + return ['image', options]; +} + +function touchable(options, element) { + return ['touchable', options, element]; +} + +function scrollView(options, elements) { + return ['scroll-view', options].concat(elements); +} + +var status = { + command: function (n, d, h) { + var command = new Command(); + return command.create(n, d, h); + }, + response: function (n, d, h) { + var response = new Response(); + return response.create(n, d, h); + }, + types: { + TEXT: 'text', + NUMBER: 'number', + PHONE: 'phone', + PASSWORD: 'password' + }, + events: { + SET_VALUE: 'set-value' + }, + components: { + view: view, + text: text, + image: image, + touchable: touchable, + scrollView: scrollView + } +}; diff --git a/src/status_im/android/core.cljs b/src/status_im/android/core.cljs index 154c7104ce..5405f58393 100644 --- a/src/status_im/android/core.cljs +++ b/src/status_im/android/core.cljs @@ -5,9 +5,10 @@ [re-frame.core :refer [subscribe dispatch dispatch-sync]] [status-im.handlers] [status-im.subs] - [status-im.components.react :refer [navigator app-registry]] + [status-im.components.react :refer [navigator app-registry device-event-emitter + orientation]] [status-im.components.main-tabs :refer [main-tabs]] - [status-im.contacts.screen :refer [contact-list]] + [status-im.contacts.views.contact-list :refer [contact-list] ] [status-im.contacts.views.new-contact :refer [new-contact]] [status-im.qr-scanner.screen :refer [qr-scanner]] [status-im.discovery.screen :refer [discovery]] @@ -33,24 +34,49 @@ true)))] (add-event-listener "hardwareBackPress" new-listener))) +(defn orientation->keyword [o] + (keyword (.toLowerCase o))) + (defn app-root [] (let [signed-up (subscribe [:get :signed-up]) - view-id (subscribe [:get :view-id])] - (fn [] - (case (if @signed-up @view-id :chat) - :discovery [main-tabs] - :discovery-tag [discovery-tag] - :add-participants [new-participants] - :remove-participants [remove-participants] - :chat-list [main-tabs] - :new-group [new-group] - :group-settings [group-settings] - :contact-list [main-tabs] - :new-contact [new-contact] - :qr-scanner [qr-scanner] - :chat [chat] - :profile [profile] - :my-profile [my-profile])))) + view-id (subscribe [:get :view-id]) + keyboard-height (subscribe [:get :keyboard-height])] + (r/create-class + {:component-will-mount + (fn [] + (let [o (orientation->keyword (.getInitialOrientation orientation))] + (dispatch [:set :orientation o])) + (.addOrientationListener + orientation + #(dispatch [:set :orientation (orientation->keyword %)])) + (.lockToPortrait orientation) + (.addListener device-event-emitter + "keyboardDidShow" + (fn [e] + (let [h (.. e -endCoordinates -height)] + (when-not (= h keyboard-height) + (dispatch [:set :keyboard-height h]))))) + (.addListener device-event-emitter + "keyboardDidHide" + (when-not (= 0 keyboard-height) + #(dispatch [:set :keyboard-height 0])))) + :render + (fn [] + (case (if @signed-up @view-id :chat) + :discovery [main-tabs] + :discovery-tag [discovery-tag] + :add-participants [new-participants] + :remove-participants [remove-participants] + :chat-list [main-tabs] + :new-group [new-group] + :group-settings [group-settings] + :contact-list [main-tabs] + :group-contacts [contact-list] + :new-contact [new-contact] + :qr-scanner [qr-scanner] + :chat [chat] + :profile [profile] + :my-profile [my-profile]))}))) (defn init [] (dispatch-sync [:initialize-db]) @@ -59,8 +85,6 @@ (dispatch [:initialize-protocol]) (dispatch [:load-user-phone-number]) (dispatch [:load-contacts]) - ;; load commands from remote server (todo: uncomment) - ;; (dispatch [:load-commands]) (dispatch [:init-console-chat]) (dispatch [:init-chat]) (init-back-button-handler!) diff --git a/src/status_im/chat/constants.cljs b/src/status_im/chat/constants.cljs new file mode 100644 index 0000000000..d61bd707ca --- /dev/null +++ b/src/status_im/chat/constants.cljs @@ -0,0 +1,9 @@ +(ns status-im.chat.constants) + +(def input-height 56) +(def request-info-height 61) +(def response-height-normal 211) +(def minimum-suggestion-height (+ input-height request-info-height)) +(def suggestions-header-height 22) +(def minimum-command-suggestions-height + (+ input-height suggestions-header-height)) diff --git a/src/status_im/chat/handlers.cljs b/src/status_im/chat/handlers.cljs index 0d0af3a2f3..05ab26564f 100644 --- a/src/status_im/chat/handlers.cljs +++ b/src/status_im/chat/handlers.cljs @@ -1,5 +1,5 @@ (ns status-im.chat.handlers - (:require [re-frame.core :refer [register-handler enrich after debug dispatch]] + (: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]] @@ -7,29 +7,39 @@ [status-im.protocol.api :as api] [status-im.models.messages :as messages] [status-im.constants :refer [text-content-type - content-type-command]] + content-type-command + content-type-command-request + default-number-of-messages]] [status-im.utils.random :as random] [status-im.chat.sign-up :as sign-up-service] [status-im.models.chats :as chats] [status-im.navigation.handlers :as nav] - [status-im.utils.handlers :as u] + [status-im.utils.handlers :refer [register-handler] :as u] [status-im.persistence.realm :as r] [status-im.handlers.server :as server] + [status-im.handlers.content-suggestions :refer [get-content-suggestions]] [status-im.utils.phone-number :refer [format-phone-number]] - [status-im.utils.datetime :as time])) + [status-im.utils.datetime :as time] + [status-im.components.jail :as j] + [status-im.utils.types :refer [json->clj]] + [status-im.commands.utils :refer [generate-hiccup]])) (register-handler :set-show-actions (fn [db [_ show-actions]] (assoc db :show-actions show-actions))) (register-handler :load-more-messages - (fn [db _] - db - ;; TODO implement - #_(let [chat-id (get-in db [:chat :current-chat-id]) - messages [:chats chat-id :messages] - new-messages (gen-messages 10)] - (update-in db messages concat new-messages)))) + (fn [{:keys [current-chat-id] :as db} _] + (let [all-loaded? (get-in db [:chats current-chat-id :all-loaded?])] + (if all-loaded? + db + (let [messages-path [:chats current-chat-id :messages] + messages (get-in db messages-path) + new-messages (messages/get-messages current-chat-id (count messages)) + all-loaded? (> default-number-of-messages (count new-messages))] + (-> db + (update-in messages-path concat new-messages) + (assoc-in [:chats current-chat-id :all-loaded?] all-loaded?))))))) (defn safe-trim [s] (when (string? s) @@ -41,38 +51,131 @@ (assoc-in [:chats current-chat-id :command-input] {}) (update-in [:chats current-chat-id :input-text] safe-trim)))) +(defn invoke-suggestions-handler! + [{:keys [current-chat-id canceled-command] :as db} _] + (when-not canceled-command + (let [{:keys [command content]} (get-in db [:chats current-chat-id :command-input]) + {:keys [name type]} command + path [(if (= :command type) :commands :responses) + name + :params + 0 + :suggestions] + params {:value content}] + (j/call current-chat-id + path + params + #(dispatch [:suggestions-handler {:command command + :content content + :chat-id current-chat-id} %]))))) + +(register-handler :start-cancel-command + (u/side-effect! + (fn [db _] + (dispatch [:animate-cancel-command])))) + +(def command-prefix "c ") + +(defn cancel-command! + [{:keys [canceled-command]}] + (when canceled-command + (dispatch [:start-cancel-command]))) + (register-handler :set-chat-command-content - (fn [db [_ content]] - (commands/set-chat-command-content db content))) + [(after invoke-suggestions-handler!) + (after cancel-command!)] + (fn [{:keys [current-chat-id] :as db} [_ content]] + (let [starts-as-command? (str/starts-with? content command-prefix) + path [:chats current-chat-id :command-input :command :type] + command? (= :command (get-in db path))] + (as-> db db + (commands/set-chat-command-content db content) + (assoc-in db [:chats current-chat-id :input-text] nil) + (assoc db :canceled-command (and command? (not starts-as-command?))))))) (defn update-input-text [{:keys [current-chat-id] :as db} text] (assoc-in db [:chats current-chat-id :input-text] text)) +(defn invoke-command-preview! + [{:keys [current-chat-id staged-command]} _] + (let [{:keys [command content]} staged-command + {:keys [name type]} command + path [(if (= :command type) :commands :responses) + name + :preview] + params {:value content}] + (j/call current-chat-id + path + params + #(dispatch [:command-preview current-chat-id %])))) + (register-handler :stage-command + (after invoke-command-preview!) (fn [{:keys [current-chat-id] :as db} _] - (let [db (update-input-text db nil) - {:keys [command content]} + (let [db (update-input-text db nil) + {:keys [command content to-msg-id]} (get-in db [:chats current-chat-id :command-input]) - command-info {:command command - :content content - :handler (:handler command)}] - (commands/stage-command db command-info)))) + content' (if (= :command (:type command)) + (subs content 2) + content) + command-info {:command command + :content content' + :to-message to-msg-id}] + (-> db + (commands/stage-command command-info) + (assoc :staged-command command-info))))) + +(register-handler :set-message-input [] + (fn [db [_ input]] + (assoc db :message-input input))) + +(register-handler :blur-message-input + (u/side-effect! + (fn [db _] + (when-let [message-input (:message-input db)] + (.blur message-input))))) (register-handler :set-response-chat-command + [(after invoke-suggestions-handler!) + (after #(dispatch [:command-edit-mode])) + (after #(dispatch [:set-chat-input-text ""]))] (fn [db [_ to-msg-id command-key]] - (commands/set-response-chat-command db to-msg-id command-key))) + (-> db + (commands/set-response-chat-command to-msg-id command-key) + (assoc :canceled-command false)))) (defn update-text - [db [_ text]] - (update-input-text db text)) + [{:keys [current-chat-id] :as db} [_ text]] + (let [suggestions (get-in db [:command-suggestions current-chat-id])] + (if-not (= 1 (count suggestions)) + (update-input-text db text) + (assoc db :disable-input true)))) (defn update-command [db [_ text]] - (let [{:keys [command]} (suggestions/check-suggestion db text)] - (commands/set-chat-command db command))) + (if-not (commands/get-chat-command db) + (let [{:keys [command]} (suggestions/check-suggestion db text)] + (if command + (commands/set-chat-command db command) + db)) + db)) + +(defn check-suggestions + [{:keys [current-chat-id] :as db} [_ text]] + (let [suggestions (suggestions/get-suggestions db text)] + (assoc-in db [:command-suggestions current-chat-id] suggestions))) + +(defn select-suggestion! + [{:keys [current-chat-id] :as db} [_ text]] + (let [suggestions (get-in db [:command-suggestions current-chat-id])] + (when (= 1 (count suggestions)) + (dispatch [:set-chat-command (ffirst suggestions)])))) (register-handler :set-chat-input-text - ((enrich update-command) update-text)) + [(enrich update-command) + (after select-suggestion!) + (after #(dispatch [:animate-command-suggestions]))] + ((enrich update-text) check-suggestions)) (defn console? [s] (= "console" s)) @@ -94,37 +197,53 @@ (defn add-message-to-db [db chat-id message] (let [messages [:chats chat-id :messages]] - (update-in db messages conj message))) + (update-in db messages conj (assoc message :chat-id chat-id + :new? true)))) + +(defn set-message-shown + [db chat-id msg-id] + (update-in db [:chats chat-id :messages] (fn [messages] + (map (fn [msg] + (if (= msg-id (:msg-id msg)) + (assoc msg :new? false) + msg)) + messages)))) + +(register-handler :set-message-shown + (fn [db [_ {:keys [chat-id msg-id]}]] + (set-message-shown db chat-id msg-id))) (defn prepare-message [{:keys [identity current-chat-id] :as db} _] - (let [text (get-in db [:chats current-chat-id :input-text]) - {:keys [command]} (suggestions/check-suggestion db (str text " ")) + (let [text (get-in db [:chats current-chat-id :input-text]) + [command] (suggestions/check-suggestion db (str text " ")) message (check-author-direction db current-chat-id - {:msg-id (random/id) - :chat-id current-chat-id - :content text - :to current-chat-id - :from identity - :content-type text-content-type - :outgoing true - :timestamp (time/now-ms)})] + {:msg-id (random/id) + :chat-id current-chat-id + :content text + :to current-chat-id + :from identity + :content-type text-content-type + :outgoing true + :timestamp (time/now-ms)})] (if command (commands/set-chat-command db command) (assoc db :new-message (when-not (str/blank? text) message))))) -(defn prepare-command [identity chat-id staged-command] - (let [command-key (get-in staged-command [:command :command]) - content {:command (name command-key) - :content (:content staged-command)}] - {:msg-id (random/id) - :from identity - :to chat-id - :content content - :content-type content-type-command - :outgoing true - :handler (:handler staged-command)})) +(defn prepare-command + [identity chat-id {:keys [preview preview-string content command to-message]}] + (let [content {:command (command :name) + :content content}] + {:msg-id (random/id) + :from identity + :to chat-id + :content content + :content-type content-type-command + :outgoing true + :preview preview-string + :rendered-preview preview + :to-message to-message})) (defn prepare-staged-commans [{:keys [current-chat-id identity] :as db} _] @@ -177,7 +296,29 @@ (defn save-commands-to-realm! [{:keys [new-commands current-chat-id]} _] (doseq [new-command new-commands] - (messages/save-message current-chat-id (dissoc new-command :handler)))) + (messages/save-message + current-chat-id + (dissoc new-command :rendered-preview :to-message)))) + +(defn dispatch-responded-requests! + [{:keys [new-commands current-chat-id]} _] + (doseq [{:keys [to-message]} new-commands] + (when to-message + (dispatch [:request-answered! current-chat-id to-message])))) + +(defn invoke-commands-handlers! + [{:keys [new-commands current-chat-id]}] + (doseq [{:keys [content] :as com} new-commands] + (let [{:keys [command content]} content + type (:type command) + path [(if (= :command type) :commands :responses) + command + :handler] + params {:value content}] + (j/call current-chat-id + path + params + #(dispatch [:command-handler! com %]))))) (defn handle-commands [{:keys [new-commands]}] @@ -196,6 +337,9 @@ ((after send-message!)) ((after save-message-to-realm!)) ((after save-commands-to-realm!)) + ((after dispatch-responded-requests!)) + ;; todo maybe it is better to track if it was handled or not + ((after invoke-commands-handlers!)) ((after handle-commands)))) (register-handler :unstage-command @@ -203,9 +347,13 @@ (commands/unstage-command db staged-command))) (register-handler :set-chat-command - (fn [db [_ command-key]] - ;; todo what is going on there?! - (commands/set-chat-command db command-key))) + [(after invoke-suggestions-handler!) + (after #(dispatch [:command-edit-mode]))] + (fn [{:keys [current-chat-id] :as db} [_ command-key]] + (-> db + (commands/set-chat-command command-key) + (assoc-in [:chats current-chat-id :command-input :content] "c ") + (assoc :disable-input true)))) (register-handler :init-console-chat (fn [db [_]] @@ -240,20 +388,23 @@ (defn load-messages! ([db] (load-messages! db nil)) - ([db _] - (->> (:current-chat-id db) - messages/get-messages - (assoc db :messages)))) + ([{:keys [current-chat-id] :as db} _] + (assoc db :messages (messages/get-messages current-chat-id)))) (defn init-chat ([db] (init-chat db nil)) ([{:keys [messages current-chat-id] :as db} _] (assoc-in db [:chats current-chat-id :messages] messages))) +(defn load-commands! + [{:keys [current-chat-id]}] + (dispatch [:load-commands! current-chat-id])) + (register-handler :init-chat + (after #(dispatch [:load-requests!])) (-> load-messages! ((enrich init-chat)) - debug)) + ((after load-commands!)))) (defn initialize-chats [{:keys [loaded-chats] :as db} _] @@ -261,7 +412,7 @@ (map (fn [{:keys [chat-id] :as chat}] [chat-id chat])) (into {})) - ids (set (keys chats))] + ids (set (keys chats))] (-> db (assoc :chats chats) (assoc :chats-ids ids) @@ -278,6 +429,11 @@ [{:keys [new-message]} [_ {chat-id :from}]] (messages/save-message chat-id new-message)) +(defn dispatch-request! + [{:keys [new-message]} [_ {chat-id :from}]] + (when (= (:content-type new-message) content-type-command-request) + (dispatch [:add-request chat-id new-message]))) + (defn receive-message [db [_ {chat-id :from :as message}]] (let [message' (check-author-direction db chat-id message)] @@ -286,8 +442,9 @@ (assoc :new-message message')))) (register-handler :received-msg - (-> receive-message - ((after store-message!)))) + [(after store-message!) + (after dispatch-request!)] + receive-message) (register-handler :group-received-msg (u/side-effect! @@ -299,6 +456,7 @@ (let [chat-id (or id current-chat-id) messages (get-in db [:chats chat-id :messages]) db' (assoc db :current-chat-id chat-id)] + (dispatch [:load-requests! chat-id]) (if (seq messages) db' (-> db' @@ -336,9 +494,11 @@ ((after save-chat!)) ((after open-chat!)))) -(register-handler :switch-command-suggestions - (fn [db [_]] - (suggestions/switch-command-suggestions db))) +(register-handler :switch-command-suggestions! + (u/side-effect! + (fn [db] + (let [text (if (suggestions/typing-command? db) "" "!")] + (dispatch [:set-chat-input-text text]))))) (defn remove-chat [{:keys [current-chat-id] :as db} _] @@ -381,3 +541,30 @@ ;((after leaving-message!)) ((after delete-messages!)) ((after delete-chat!)))) + +(defn edit-mode-handler [mode] + (fn [{:keys [current-chat-id] :as db} _] + (assoc-in db [:edit-mode current-chat-id] mode))) + +(register-handler :command-edit-mode + (edit-mode-handler :command)) + +(register-handler :text-edit-mode + (after #(dispatch [:set-chat-input-text ""])) + (edit-mode-handler :text)) + +(register-handler :set-layout-height + [(after + (fn [{:keys [current-chat-id] :as db}] + (let [suggestions (get-in db [:suggestions current-chat-id]) + mode (get-in db [:edit-mode current-chat-id])] + (when (and (= :command mode) (seq suggestions)) + (dispatch [:fix-response-height]))))) + (after + (fn [{:keys [current-chat-id] :as db}] + (let [suggestions (get-in db [:command-suggestions current-chat-id]) + mode (get-in db [:edit-mode current-chat-id])] + (when (and (= :text mode)) (seq suggestions) + (dispatch [:fix-commands-suggestions-height])))))] + (fn [db [_ h]] + (assoc db :layout-height h))) diff --git a/src/status_im/chat/handlers/animation.cljs b/src/status_im/chat/handlers/animation.cljs new file mode 100644 index 0000000000..c14a22b22d --- /dev/null +++ b/src/status_im/chat/handlers/animation.cljs @@ -0,0 +1,112 @@ +(ns status-im.chat.handlers.animation + (:require [re-frame.core :refer [after dispatch debug path]] + [status-im.utils.handlers :refer [register-handler]] + [status-im.handlers.content-suggestions :refer [get-content-suggestions]] + [status-im.chat.constants :refer [input-height request-info-height + minimum-command-suggestions-height + response-height-normal minimum-suggestion-height]] + [status-im.constants :refer [response-input-hiding-duration]])) + +;; todo magic value +(def middle-height 270) + +(defn animation-handler + ([name handler] (animation-handler name nil handler)) + ([name middleware handler] + (register-handler name [(path :animations) middleware] handler))) + +(animation-handler :animate-cancel-command + (after #(dispatch [:text-edit-mode])) + (fn [db _] + (assoc db :to-response-height input-height))) + +(def response-height (+ input-height response-height-normal)) + +(defn update-response-height [db] + (assoc-in db [:animations :to-response-height] response-height)) + +(register-handler :animate-command-suggestions + (fn [{:keys [current-chat-id] :as db} _] + (let [suggestions? (seq (get-in db [:command-suggestions current-chat-id])) + current (get-in db [:animations :command-suggestions-height]) + height (if suggestions? middle-height input-height) + changed? (if (and suggestions? + (not (nil? current)) + (not= input-height current)) + identity inc)] + (-> db + (update :animations assoc :command-suggestions-height height) + (update-in [:animations :commands-height-changed] changed?))))) + +(defn get-minimum-height + [{:keys [current-chat-id] :as db}] + (let [path [:chats current-chat-id :command-input :command :type] + type (get-in db path)] + (if (= :response type) + minimum-suggestion-height + input-height))) + +(register-handler :animate-show-response + [(after #(dispatch [:command-edit-mode]))] + (fn [{:keys [current-chat-id] :as db}] + (let [suggestions? (seq (get-in db [:suggestions current-chat-id])) + height (if suggestions? + middle-height + (get-minimum-height db))] + (assoc-in db [:animations :to-response-height] height)))) + +(defn fix-height + [height-key height-signal-key suggestions-key minimum] + (fn [{:keys [current-chat-id] :as db} [_ vy current]] + (let [max-height (get-in db [:layout-height]) + moving-down? (pos? vy) + moving-up? (not moving-down?) + under-middle-position? (<= current middle-height) + over-middle-position? (not under-middle-position?) + suggestions (get-in db [suggestions-key current-chat-id]) + old-fixed (get-in db [:animations height-key]) + + new-fixed (cond (not suggestions) + (minimum db) + + (and (nil? vy) (nil? current) + (> old-fixed middle-height)) + max-height + + (and (nil? vy) (nil? current) + (< old-fixed middle-height)) + (minimum db) + + (and under-middle-position? moving-up?) + middle-height + + (and over-middle-position? moving-down?) + middle-height + + (and over-middle-position? moving-up?) + max-height + + (and under-middle-position? moving-down?) + (minimum db))] + (-> db + (assoc-in [:animations height-key] new-fixed) + (update-in [:animations height-signal-key] inc))))) + +(defn commands-min-height + [{:keys [current-chat-id] :as db}] + (let [suggestions (get-in db [:command-suggestions current-chat-id])] + (if (seq suggestions) + minimum-command-suggestions-height + input-height))) + +(register-handler :fix-commands-suggestions-height + (fix-height :command-suggestions-height + :commands-height-changed + :command-suggestions + commands-min-height)) + +(register-handler :fix-response-height + (fix-height :to-response-height + :response-height-changed + :suggestions + get-minimum-height)) diff --git a/src/status_im/chat/handlers/requests.cljs b/src/status_im/chat/handlers/requests.cljs new file mode 100644 index 0000000000..d229b0dc94 --- /dev/null +++ b/src/status_im/chat/handlers/requests.cljs @@ -0,0 +1,57 @@ +(ns status-im.chat.handlers.requests + (:require [re-frame.core :refer [after dispatch enrich]] + [status-im.utils.handlers :refer [register-handler]] + [status-im.persistence.realm :as realm] + [status-im.utils.handlers :as u])) + +(defn store-request! + [{:keys [new-request]}] + (realm/write + (fn [] + (realm/create :requests new-request)))) + +(defn add-request + [db [_ chat-id {:keys [msg-id content]}]] + (let [request {:chat-id chat-id + :message-id msg-id + :type (:command content) + :added (js/Date.)} + request' (update request :type keyword)] + (-> db + (update-in [:chats chat-id :requests] conj request') + (assoc :new-request request)))) + +(defn load-requests! + [{:keys [current-chat-id] :as db} [_ chat-id]] + (let [chat-id' (or chat-id current-chat-id) + requests (-> :requests + ;; todo maybe limit is needed + (realm/get-by-fields {:chat-id chat-id' + :status "open"}) + (realm/sorted :added :desc) + (realm/collection->map)) + requests' (map #(update % :type keyword) requests)] + (assoc-in db [:chats chat-id' :requests] requests'))) + +(defn mark-request-as-answered! + [_ [_ chat-id message-id]] + (realm/write + (fn [] + (-> :requests + (realm/get-by-fields + {:chat-id chat-id + :message-id message-id}) + (realm/single) + (.-status) + (set! "answered"))))) + +(register-handler :add-request + (after store-request!) + add-request) + +(register-handler :load-requests! load-requests!) + +(register-handler :request-answered! + (after (fn [_ [_ chat-id]] + (dispatch [:load-requests! chat-id]))) + (u/side-effect! mark-request-as-answered!)) diff --git a/src/status_im/chat/screen.cljs b/src/status_im/chat/screen.cljs index 3ff250e238..d2b7126cbb 100644 --- a/src/status_im/chat/screen.cljs +++ b/src/status_im/chat/screen.cljs @@ -3,6 +3,7 @@ (:require [re-frame.core :refer [subscribe dispatch]] [clojure.string :as s] [status-im.components.react :refer [view + animated-view text image icon @@ -12,13 +13,17 @@ [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.utils.listview :refer [to-datasource]] + [status-im.utils.listview :refer [to-datasource-inverted]] [status-im.utils.utils :refer [truncate-str]] [status-im.components.invertible-scroll-view :refer [invertible-scroll-view]] [status-im.components.toolbar :refer [toolbar]] [status-im.chat.views.message :refer [chat-message]] + [status-im.chat.views.suggestions :refer [suggestion-container]] + [status-im.chat.views.response :refer [response-view]] [status-im.chat.views.new-message :refer [chat-message-new]] - [status-im.i18n :refer [label label-pluralize]])) + [status-im.i18n :refer [label label-pluralize]] + [status-im.components.animation :as anim] + [reagent.core :as r])) (defn contacts-by-identity [contacts] @@ -36,10 +41,10 @@ :background-color background-color)))) (defview chat-icon [] - [chat-id [:chat :chat-id] + [chat-id [:chat :chat-id] group-chat [:chat :group-chat] - name [:chat :name] - color [:chat :color]] + name [:chat :name] + color [:chat :color]] ;; TODO stub data ('online' property) [chat-icon-view-action chat-id group-chat name color true]) @@ -55,12 +60,12 @@ (for [member ["Geoff" "Justas"]] ^{:key member} [typing member])]) -(defn message-row [contact-by-identity group-chat] +(defn message-row [contact-by-identity group-chat messages-count] (fn [row _ idx] (let [msg (-> row (add-msg-color contact-by-identity) (assoc :group-chat group-chat) - (assoc :last-msg (zero? (js/parseInt idx))))] + (assoc :last-msg (= (js/parseInt idx) (dec messages-count))))] (list-item [chat-message msg])))) (defn on-action-selected [position] @@ -93,10 +98,10 @@ subtitle])]]]) (defview menu-item-icon-profile [] - [chat-id [:chat :chat-id] + [chat-id [:chat :chat-id] group-chat [:chat :group-chat] - name [:chat :name] - color [:chat :color]] + name [:chat :name] + color [:chat :color]] ;; TODO stub data ('online' property) [chat-icon-view-menu-item chat-id group-chat name color true]) @@ -135,12 +140,12 @@ :icon-style {:width 20 :height 13} :handler #(dispatch [:show-group-settings])}] - [{:title (label :t/profile) + [{:title (label :t/profile) :custom-icon [menu-item-icon-profile] - :icon :menu_group - :icon-style {:width 25 - :height 19} - :handler #(dispatch [:show-profile @chat-id])} + :icon :menu_group + :icon-style {:width 25 + :height 19} + :handler #(dispatch [:show-profile @chat-id])} {:title (label :t/search-chat) :subtitle (label :t/not-implemented) :icon :search_gray_copy @@ -216,18 +221,53 @@ [messages [:chat :messages] contacts [:chat :contacts]] (let [contacts' (contacts-by-identity contacts)] - [list-view {:renderRow (message-row contacts' group-chat) - :renderScrollComponent #(invertible-scroll-view (js->clj %)) - :onEndReached #(dispatch [:load-more-messages]) - :enableEmptySections true - :dataSource (to-datasource messages)}])) + [list-view {:renderRow (message-row contacts' group-chat (count messages)) + :renderScrollComponent #(invertible-scroll-view (js->clj %)) + :onEndReached #(dispatch [:load-more-messages]) + :enableEmptySections true + :keyboardShouldPersistTaps true + :dataSource (to-datasource-inverted messages)}])) + +(defn messages-container-animation-logic + [{:keys [offset val]}] + (fn [_] + (anim/start (anim/spring val {:toValue @offset})))) + +(defn messages-container [messages] + (let [offset (subscribe [:messages-offset]) + messages-offset (anim/create-value 0) + context {:offset offset + :val messages-offset} + on-update (messages-container-animation-logic context)] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [messages] + @offset + [animated-view {:style (st/messages-container messages-offset)} + messages])}))) (defview chat [] [group-chat [:chat :group-chat] - show-actions-atom [:show-actions]] - [view st/chat-view + 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] - [messages-view group-chat] + [messages-container + [messages-view group-chat]] (when group-chat [typing-all]) + [response-view] + (when-not command? [suggestion-container]) [chat-message-new] (when show-actions-atom [actions-view])]) diff --git a/src/status_im/chat/sign_up.cljs b/src/status_im/chat/sign_up.cljs index 9633d94464..e4140eab16 100644 --- a/src/status_im/chat/sign_up.cljs +++ b/src/status_im/chat/sign_up.cljs @@ -173,7 +173,7 @@ (dispatch [:received-msg {:msg-id msg-id :content (command-content - :keypair-password + :keypair (label :t/keypair-generated)) :content-type content-type-command-request :outgoing false @@ -184,6 +184,9 @@ (def console-chat {:chat-id "console" :name "console" + ; todo remove/change dapp config fot console + :dapp-url "http://localhost:8185/resources" + :dapp-hash 858845357 :color default-chat-color :group-chat false :is-active true diff --git a/src/status_im/chat/styles/content_suggestions.cljs b/src/status_im/chat/styles/content_suggestions.cljs index 198468a549..97c403bca7 100644 --- a/src/status_im/chat/styles/content_suggestions.cljs +++ b/src/status_im/chat/styles/content_suggestions.cljs @@ -1,22 +1,21 @@ (ns status-im.chat.styles.content-suggestions (:require [status-im.components.styles :refer [font - color-light-blue-transparent - color-white - color-black - color-blue - color-blue-transparent - selected-message-color - online-color - separator-color - text1-color - text2-color - text3-color]])) + color-light-blue-transparent + color-white + color-black + color-blue + color-blue-transparent + selected-message-color + online-color + separator-color + text1-color + text2-color + text3-color]])) (def suggestion-height 56) (def suggestion-container - {:flexDirection :column - :paddingLeft 16 + {:paddingLeft 16 :backgroundColor color-white}) (def suggestion-sub-container @@ -37,18 +36,12 @@ :color text2-color}) (defn suggestions-container [suggestions-count] - {:flexDirection :row + {:flex 1 :marginVertical 1 :marginHorizontal 0 :height (min 150 (* suggestion-height suggestions-count)) :backgroundColor color-white :borderRadius 5}) -(def drag-down-touchable - {:height 22 - :alignItems :center - :justifyContent :center}) - -(def drag-down-icon - {:width 16 - :height 16}) +(def container + {:backgroundColor color-white}) diff --git a/src/status_im/chat/styles/dragdown.cljs b/src/status_im/chat/styles/dragdown.cljs new file mode 100644 index 0000000000..7505021a6f --- /dev/null +++ b/src/status_im/chat/styles/dragdown.cljs @@ -0,0 +1,12 @@ +(ns status-im.chat.styles.dragdown + (:require [status-im.components.styles :refer [color-white]])) + +(def drag-down-touchable + {:height 22 + :background-color color-white + :alignItems :center + :justifyContent :center}) + +(def drag-down-icon + {:width 16 + :height 16}) diff --git a/src/status_im/chat/styles/input.cljs b/src/status_im/chat/styles/input.cljs index 474cd2a115..6dd49b23be 100644 --- a/src/status_im/chat/styles/input.cljs +++ b/src/status_im/chat/styles/input.cljs @@ -16,13 +16,18 @@ :backgroundColor color-white :elevation 4}) +(def command-container + {:left 0 + :backgroundColor :white + :position :absolute}) + (defn command-text-container [{:keys [color]}] {:flexDirection :column :marginTop 16 :marginBottom 16 :marginLeft 16 - :marginRight 0 + :marginRight 8 :backgroundColor color :height 24 :borderRadius 50}) @@ -37,7 +42,8 @@ (def command-input {:flex 1 :marginLeft 8 - :marginTop 7 + :marginTop -2 + :padding 0 :fontSize 14 :fontFamily font :color text1-color}) diff --git a/src/status_im/chat/styles/message.cljs b/src/status_im/chat/styles/message.cljs index 79e3cb24a7..93f1472389 100644 --- a/src/status_im/chat/styles/message.cljs +++ b/src/status_im/chat/styles/message.cljs @@ -1,15 +1,15 @@ (ns status-im.chat.styles.message (:require [status-im.components.styles :refer [font - color-light-blue-transparent - color-white - color-black - color-blue - selected-message-color - online-color - text1-color - text2-color]] + color-light-blue-transparent + color-white + color-black + color-blue + selected-message-color + online-color + text1-color + text2-color]] [status-im.constants :refer [text-content-type - content-type-command]])) + content-type-command]])) (def style-message-text {:fontSize 14 @@ -131,20 +131,35 @@ (def command-request-message-view {:borderRadius 14 :padding 12 + :paddingRight 28 :backgroundColor color-white}) (def command-request-from-text (merge style-sub-text {:marginBottom 2})) -(defn command-request-image-view - [command] - {:position :absolute - :top 12 - :right 0 - :width 32 +(def command-request-image-touchable + {:position :absolute + :top 4 + :right -8 + :alignItems :center + :justifyContent :center + :width 48 + :height 48}) + +(defn command-request-image-view [command scale] + {:width 32 :height 32 :borderRadius 50 - :backgroundColor (:color command)}) + :backgroundColor (:color command) + :transform [{:scale scale}]}) + +(def command-image-view + {:position :absolute + :top 0 + :right 0 + :width 24 + :height 24 + :alignItems :center}) (def command-request-image {:position :absolute @@ -178,11 +193,10 @@ :color color-white}) (def command-image - {:position :absolute - :top 4 - :right 0 - :width 12 - :height 13}) + {:margin-top 5 + :width 12 + :height 13 + :tint-color :#a9a9a9cc}) (def command-text (merge style-message-text @@ -309,6 +323,9 @@ (def message-date-text (assoc style-sub-text :textAlign :center)) +(defn message-container [height] + {:height height}) + (def new-message-container {:backgroundColor color-white :elevation 4}) diff --git a/src/status_im/chat/styles/message_input.cljs b/src/status_im/chat/styles/message_input.cljs new file mode 100644 index 0000000000..c27a359646 --- /dev/null +++ b/src/status_im/chat/styles/message_input.cljs @@ -0,0 +1,30 @@ +(ns status-im.chat.styles.message-input + (:require [status-im.components.styles :refer [color-white + color-blue]] + [status-im.chat.constants :refer [input-height]])) + +(def message-input-container + {:flex 1 + :marginRight 0}) + +(def input-container + {:flexDirection :column}) + +(def input-view + {:flexDirection :row + :height input-height + :backgroundColor color-white}) + +(def send-icon + {:marginTop 10.5 + :marginLeft 12 + :width 15 + :height 15}) + +(def send-container + {:marginTop 10 + :marginRight 10 + :width 36 + :height 36 + :borderRadius 50 + :backgroundColor color-blue}) diff --git a/src/status_im/chat/styles/plain_input.cljs b/src/status_im/chat/styles/plain_input.cljs deleted file mode 100644 index b8806518dd..0000000000 --- a/src/status_im/chat/styles/plain_input.cljs +++ /dev/null @@ -1,55 +0,0 @@ -(ns status-im.chat.styles.plain-input - (:require [status-im.components.styles :refer [font - text2-color - color-white - color-blue]])) - -(def input-container - {:flexDirection :column}) - -(def input-view - {:flexDirection :row - :height 56 - :backgroundColor color-white}) - -(def switch-commands-touchable - {:width 56 - :height 56 - :alignItems :center - :justifyContent :center}) - -(def list-icon - {:width 13 - :height 12}) - -(def close-icon - {:width 12 - :height 12}) - -(def message-input - {:flex 1 - :marginTop -2 - :padding 0 - :fontSize 14 - :fontFamily font - :color text2-color}) - -(def smile-icon - {:marginTop 18 - :marginRight 18 - :width 20 - :height 20}) - -(def send-icon - {:marginTop 10.5 - :marginLeft 12 - :width 15 - :height 15}) - -(def send-container - {:marginTop 10 - :marginRight 10 - :width 36 - :height 36 - :borderRadius 50 - :backgroundColor color-blue}) diff --git a/src/status_im/chat/styles/plain_message.cljs b/src/status_im/chat/styles/plain_message.cljs new file mode 100644 index 0000000000..fa639b39fa --- /dev/null +++ b/src/status_im/chat/styles/plain_message.cljs @@ -0,0 +1,45 @@ +(ns status-im.chat.styles.plain-message + (:require [status-im.components.styles :refer [font + text2-color]])) + +(defn message-input-button-touchable [w] + {:width w + :height 56 + :alignItems :center + :justifyContent :center}) + +(defn message-input-button [scale] + {:transform [{:scale scale}] + :width 24 + :height 24 + :alignItems :center + :justifyContent :center}) + +(def list-icon + {:width 16 + :height 16}) + +(def requests-icon + {:background-color :#7099e6 + :width 8 + :height 8 + :border-radius 8 + :left 0 + :top 0 + :position :absolute}) + +(def close-icon + {:width 12 + :height 12}) + +(def message-input + {:flex 1 + :marginTop -2 + :padding 0 + :fontSize 14 + :fontFamily font + :color text2-color}) + +(def smile-icon + {:width 20 + :height 20}) diff --git a/src/status_im/chat/styles/response.cljs b/src/status_im/chat/styles/response.cljs new file mode 100644 index 0000000000..a0a5c08cd5 --- /dev/null +++ b/src/status_im/chat/styles/response.cljs @@ -0,0 +1,94 @@ +(ns status-im.chat.styles.response + (:require [status-im.components.styles :refer [font + color-white + color-blue + text1-color + text2-color + chat-background + color-black]] + [status-im.chat.constants :refer [input-height request-info-height + response-height-normal]])) + +(def drag-container + {:height 16 + :alignItems :center + :justifyContent :center}) + +(def drag-icon + {:width 14 + :height 3}) + +(def command-icon-container + {:marginTop 1 + :marginLeft 12 + :width 32 + :height 32 + :alignItems :center + :justifyContent :center + :borderRadius 50 + :backgroundColor color-white}) + +(def command-icon + {:width 9 + :height 15}) + +(def info-container + {:flex 1 + :marginLeft 12}) + +(def command-name + {:marginTop 0 + :fontSize 12 + :fontFamily font + :color color-white}) + +(def message-info + {:marginTop 1 + :fontSize 12 + :fontFamily font + :opacity 0.69 + :color color-white}) + +(defn response-view [height] + {:flexDirection :column + :position :absolute + :left 0 + :right 0 + :bottom 0 + :height height + :backgroundColor color-white + :elevation 2}) + +(def input-placeholder + {:height input-height}) + +(defn request-info [color] + {:flexDirection :column + :height request-info-height + :backgroundColor color}) + +(def inner-container + {:flexDirection :row + :height 45}) + +(def cancel-container + {:marginTop 2.5 + :marginRight 16 + :width 24 + :height 24}) + +(def cancel-icon + {:marginTop 6 + :marginLeft 6 + :width 12 + :height 12}) + +(defn command-input [ml disbale?] + {:flex 1 + :marginRight 16 + :margin-left (- ml 5) + :marginTop -2 + :padding 0 + :fontSize 14 + :fontFamily font + :color (if disbale? color-white text1-color)}) diff --git a/src/status_im/chat/styles/screen.cljs b/src/status_im/chat/styles/screen.cljs index 35e4d0b3d7..49133f8d32 100644 --- a/src/status_im/chat/styles/screen.cljs +++ b/src/status_im/chat/styles/screen.cljs @@ -14,6 +14,10 @@ {:flex 1 :backgroundColor chat-background}) +(defn messages-container [bottom] + {:flex 1 + :bottom bottom}) + (def toolbar-view {:flexDirection :row :height 56 diff --git a/src/status_im/chat/styles/suggestions.cljs b/src/status_im/chat/styles/suggestions.cljs index 2d5eaaab8e..2985922cd7 100644 --- a/src/status_im/chat/styles/suggestions.cljs +++ b/src/status_im/chat/styles/suggestions.cljs @@ -1,33 +1,44 @@ (ns status-im.chat.styles.suggestions (:require [status-im.components.styles :refer [font - color-light-blue-transparent - color-white - color-black - color-blue - color-blue-transparent - selected-message-color - online-color - separator-color - text1-color - text2-color - text3-color]])) + color-light-blue-transparent + color-white + color-black + color-gray + color-blue + color-blue-transparent + selected-message-color + online-color + separator-color + text1-color + text2-color + text3-color]])) -(def suggestion-height 88) +(def suggestion-height 60) + +(def suggestion-highlight + {:margin-left 57}) (def suggestion-container - {:flexDirection :column - :paddingLeft 16 - :backgroundColor color-white}) + {:backgroundColor color-white}) (def suggestion-sub-container {:height suggestion-height :borderBottomWidth 1 - :borderBottomColor separator-color}) + :borderBottomColor separator-color + :flex-direction :row}) + +(def command-description-container + {:flex 0.6}) + +(def command-label-container + {:flex 0.4 + :flex-direction :column + :align-items :flex-end + :margin-right 16}) (defn suggestion-background [{:keys [color]}] - {:alignSelf :flex-start - :marginTop 10 + {:marginTop 10 :height 24 :backgroundColor color :borderRadius 50}) @@ -39,6 +50,14 @@ :fontFamily font :color color-white}) +(def title-container + {:margin-left 57 + :margin-bottom 16}) + +(def title-text + {:font-size 13 + :color :#8f838c93}) + (def value-text {:marginTop 6 :fontSize 14 @@ -51,19 +70,53 @@ :fontFamily font :color text2-color}) -(defn suggestions-container [suggestions-count] - {:flexDirection :row - :marginVertical 1 - :marginHorizontal 0 - :height (min 168 (* suggestion-height suggestions-count)) - :backgroundColor color-white - :borderRadius 5}) +(defn container [height] + {:flexDirection :column + :position :absolute + :left 0 + :right 0 + :bottom 0 + :height height + :backgroundColor color-white + :elevation 2}) -(def drag-down-touchable - {:height 22 - :alignItems :center +(def request-container + {:height 56 + :flex-direction :row}) + +(def request-icon-container + {:height 56 + :width 57 + :align-items :center :justifyContent :center}) -(def drag-down-icon - {:width 16 - :height 16}) +(defn request-icon-background [color] + {:height 32 + :width 32 + :border-radius 32 + :background-color color + :align-items :center + :justifyContent :center}) + +(def request-icon + {:width 12 + :height 12}) + +(def request-info-container + {:height 56 + :padding-top 10}) + +(def request-info-description + {:font-size 12 + :color color-black}) + +(def request-message-info + {:font-size 12 + :margin-top 2 + :color color-gray}) + +(def header-icon + {:background-color :#838c93 + :width 14 + :border-radius 1 + :height 3}) diff --git a/src/status_im/chat/subs.cljs b/src/status_im/chat/subs.cljs index d5df5f5716..ae90e45bc0 100644 --- a/src/status_im/chat/subs.cljs +++ b/src/status_im/chat/subs.cljs @@ -1,12 +1,12 @@ (ns status-im.chat.subs (:require-macros [reagent.ratom :refer [reaction]]) - (:require [re-frame.core :refer [register-sub]] - [status-im.db :as db] - ;todo handlers in subs?... - [status-im.chat.suggestions :refer - [get-suggestions typing-command? get-content-suggestions]] + (:require [re-frame.core :refer [register-sub dispatch subscribe path]] [status-im.models.commands :as commands] - [status-im.handlers.content-suggestions :refer [get-content-suggestions]])) + [status-im.constants :refer [response-suggesstion-resize-duration]] + [status-im.chat.constants :as c] + [status-im.handlers.content-suggestions :refer [get-content-suggestions]] + [status-im.chat.views.plain-message :as plain-message] + [status-im.chat.views.command :as command])) (register-sub :chat-properties (fn [db [_ properties]] @@ -40,16 +40,25 @@ (register-sub :get-suggestions (fn [db _] - (let [input-text (->> (:current-chat-id @db) - db/chat-input-text-path - (get-in @db) - (reaction))] - (reaction (get-suggestions @db @input-text))))) + (let [chat-id (subscribe [:get-current-chat-id])] + (reaction (get-in @db [:command-suggestions @chat-id]))))) (register-sub :get-commands (fn [db _] (reaction (commands/get-commands @db)))) +(register-sub :get-responses + (fn [db _] + (let [current-chat (@db :current-chat-id)] + (reaction (or (get-in @db [:chats current-chat :responses]) {}))))) + +(register-sub :get-commands-and-responses + (fn [db _] + (let [current-chat (@db :current-chat-id)] + (reaction _ (or (->> (get-in @db [:chats current-chat]) + ((juxt :commands :responses)) + (apply merge)) {}))))) + (register-sub :get-chat-input-text (fn [db _] (->> [:chats (:current-chat-id @db) :input-text] @@ -62,14 +71,39 @@ (get-in @db) (reaction)))) +(register-sub :valid-plain-message? + (fn [_ _] + (let [input-message (subscribe [:get-chat-input-text]) + staged-commands (subscribe [:get-chat-staged-commands])] + (reaction + (plain-message/message-valid? @staged-commands @input-message))))) + +(register-sub :valid-command? + (fn [_ [_ validator]] + (let [input (subscribe [:get-chat-command-content])] + (reaction (command/valid? @input validator))))) + (register-sub :get-chat-command (fn [db _] (reaction (commands/get-chat-command @db)))) +(register-sub :get-command-parameter + (fn [db] + (let [command (subscribe [:get-chat-command]) + chat-id (subscribe [:get-current-chat-id])] + (reaction + (let [path [:chats @chat-id :command-input :parameter-idx] + n (get-in @db path)] + (when n (nth (:params @command) n))))))) + (register-sub :get-chat-command-content (fn [db _] (reaction (commands/get-chat-command-content @db)))) +(register-sub :get-chat-command-to-msg-id + (fn [db _] + (reaction (commands/get-chat-command-to-msg-id @db)))) + (register-sub :chat-command-request (fn [db _] (reaction (commands/get-chat-command-request @db)))) @@ -83,12 +117,58 @@ (fn [db [_ chat-id]] (reaction (get-in @db [:chats chat-id])))) -(register-sub :typing-command? - (fn [db _] - (reaction (typing-command? @db)))) - (register-sub :get-content-suggestions (fn [db _] - (let [command (reaction (commands/get-chat-command @db)) - text (reaction (commands/get-chat-command-content @db))] - (reaction (get-content-suggestions @db @command @text))))) + (reaction (get-in @db [:suggestions (:current-chat-id @db)])))) + +(register-sub :command? + (fn [db] + (->> (get-in @db [:edit-mode (:current-chat-id @db)]) + (= :command) + (reaction)))) + +(register-sub :command-type + (fn [] + (let [command (subscribe [:get-chat-command])] + (reaction (:type @command))))) + +(register-sub :messages-offset + (fn [] + (let [command? (subscribe [:command?]) + type (subscribe [:command-type]) + command-suggestions (subscribe [:get-content-suggestions]) + suggestions (subscribe [:get-suggestions])] + (reaction + (cond (and @command? (= @type :response)) + c/request-info-height + + (and @command? (= @type :command) (seq @command-suggestions)) + c/suggestions-header-height + + (and (not @command?) (seq @suggestions)) + c/suggestions-header-height + + :else 0))))) + +(register-sub :command-icon-width + (fn [] + (let [width (subscribe [:get :command-icon-width]) + type (subscribe [:command-type])] + (reaction (if (= :command @type) + @width + 0))))) + +(register-sub :get-requests + (fn [db] + (let [chat-id (subscribe [:get-current-chat-id])] + (reaction (get-in @db [:chats @chat-id :requests]))))) + +(register-sub :get-response + (fn [db [_ n]] + (let [chat-id (subscribe [:get-current-chat-id])] + (reaction (get-in @db [:chats @chat-id :responses n]))))) + +(register-sub :is-request-answered? + (fn [_ [_ message-id]] + (let [requests (subscribe [:get-requests])] + (reaction (not (some #(= message-id (:message-id %)) @requests)))))) diff --git a/src/status_im/chat/suggestions.cljs b/src/status_im/chat/suggestions.cljs index dcdc0d3db4..73f90e884d 100644 --- a/src/status_im/chat/suggestions.cljs +++ b/src/status_im/chat/suggestions.cljs @@ -1,23 +1,26 @@ (ns status-im.chat.suggestions (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] [status-im.db :as db] - [status-im.models.commands :refer [commands - suggestions - get-commands - get-chat-command-request - get-chat-command-to-msg-id - clear-staged-commands]] + [status-im.models.commands :refer [get-commands + get-chat-command-request + get-chat-command-to-msg-id + clear-staged-commands]] [status-im.utils.utils :refer [log on-error http-get]] [clojure.string :as s])) (defn suggestion? [text] (= (get text 0) "!")) -(defn get-suggestions [db text] - (if (suggestion? text) - ;; TODO change 'commands' to 'suggestions' - (filterv #(.startsWith (:text %) text) (get-commands db)) - [])) +(defn can-be-suggested? [text] + (fn [{:keys [name]}] + (.startsWith (str "!" name) text))) + +(defn get-suggestions + [{:keys [current-chat-id] :as db} text] + (let [commands (get-in db [:chats current-chat-id :commands])] + (if (suggestion? text) + (filter (fn [[_ v]] ((can-be-suggested? text) v)) commands) + []))) (defn get-command [db text] (when (suggestion? text) @@ -45,22 +48,13 @@ staged-commands)) (clear-staged-commands db))) -(defn execute-commands-js [body] - (.eval js/window body) - (let [commands (.-commands js/window)] - (dispatch [:set-commands (map #(update % :command keyword) - (js->clj commands :keywordize-keys true))]))) - -(defn load-commands [] - (http-get "chat-commands.js" execute-commands-js nil)) - (defn check-suggestion [db message] (when-let [suggestion-text (when (string? message) (re-matches #"^![^\s]+\s" message))] - (let [suggestion-text' (s/trim suggestion-text) - [suggestion] (filter #(= suggestion-text' (:text %)) - (get-commands db))] - suggestion))) + (let [suggestion-text' (s/trim suggestion-text)] + (->> (get-commands db) + (filter #(= suggestion-text' (->> % second :name (str "!")))) + first)))) (defn typing-command? [db] (-> db diff --git a/src/status_im/chat/suggestions_responder.cljs b/src/status_im/chat/suggestions_responder.cljs new file mode 100644 index 0000000000..eebdd45608 --- /dev/null +++ b/src/status_im/chat/suggestions_responder.cljs @@ -0,0 +1,39 @@ +(ns status-im.chat.suggestions-responder + (:require [status-im.components.drag-drop :as drag] + [status-im.components.animation :as anim] + [status-im.components.react :as react] + [re-frame.core :refer [dispatch]])) + +;; todo bad name. Ideas? +(defn enough-dy [gesture] + (> (Math/abs (.-dy gesture)) 10)) + +(defn on-move [response-height kb-height orientation] + (fn [_ gesture] + (when (enough-dy gesture) + (let [w (react/get-dimensions "window") + ;; depending on orientation use height or width of screen + prop (if (= :portrait @orientation) + :height + :width) + ;; subtract keyboard height to get "real height" of screen + ;; then subtract gesture position to get suggestions height + ;; todo maybe it is better to use margin-top instead height + ;; it is not obvious + to-value (- (prop w) @kb-height (.-moveY gesture))] + (anim/start + (anim/spring response-height {:toValue to-value})))))) + +(defn on-release [response-height handler-name] + (fn [_ gesture] + (when (enough-dy gesture) + (dispatch [handler-name + (.-vy gesture) + ;; todo access to "private" property + ;; better to find another way... + (.-_value response-height)])))) + +(defn pan-responder [response-height kb-height orientation handler-name] + (drag/create-pan-responder + {:on-move (on-move response-height kb-height orientation) + :on-release (on-release response-height handler-name)})) diff --git a/src/status_im/chat/views/command.cljs b/src/status_im/chat/views/command.cljs index 2b550dc7bf..49f5135b7e 100644 --- a/src/status_im/chat/views/command.cljs +++ b/src/status_im/chat/views/command.cljs @@ -1,16 +1,15 @@ (ns status-im.chat.views.command + (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch]] [status-im.components.react :refer [view - icon - text - text-input - touchable-highlight]] - [status-im.chat.views.content-suggestions :refer - [content-suggestions-view]] + icon + text + text-input + touchable-highlight]] [status-im.chat.styles.input :as st])) (defn cancel-command-input [] - (dispatch [:cancel-command])) + (dispatch [:start-cancel-command])) (defn set-input-message [message] (dispatch [:set-chat-command-content message])) @@ -24,28 +23,22 @@ (validator message) (pos? (count message)))) -(defn simple-command-input-view [command input-options & {:keys [validator]}] - (let [message-atom (subscribe [:get-chat-command-content])] - (fn [command input-options & {:keys [validator]}] - (let [message @message-atom] - [view st/command-input-and-suggestions-container - [content-suggestions-view] - [view st/command-input-container - [view (st/command-text-container command) - [text {:style st/command-text} (:text command)]] - [text-input (merge {:style st/command-input - :autoFocus true - :onChangeText set-input-message - :onSubmitEditing (fn [] - (when (valid? message validator) - (send-command))) - :accessibility-label :command-input} - input-options) - message] - (if (valid? message validator) - [touchable-highlight {:on-press send-command - :accessibility-label :stage-command} - [view st/send-container [icon :send st/send-icon]]] - [touchable-highlight {:on-press cancel-command-input} - [view st/cancel-container - [icon :close-gray st/cancel-icon]]])]])))) +(defn try-send [message validator] + (when (valid? message validator) + (send-command))) + +(defview command-icon [command] + [icon-width [:get :command-icon-width]] + [view st/command-container + [view {:style (st/command-text-container command) + :onLayout (fn [event] + (let [width (.. event -nativeEvent -layout -width)] + (when (not= icon-width width) + (dispatch [:set :command-icon-width width]))))} + [text {:style st/command-text} (str "!" (:name command))]]]) + +(defn cancel-button [] + [touchable-highlight {:on-press cancel-command-input} + [view st/cancel-container + [icon :close-gray st/cancel-icon]]]) + diff --git a/src/status_im/chat/views/confirmation_code.cljs b/src/status_im/chat/views/confirmation_code.cljs deleted file mode 100644 index b1d5f4c94e..0000000000 --- a/src/status_im/chat/views/confirmation_code.cljs +++ /dev/null @@ -1,6 +0,0 @@ -(ns status-im.chat.views.confirmation-code - (:require - [status-im.chat.views.command :refer [simple-command-input-view]])) - -(defn confirmation-code-input-view [command] - [simple-command-input-view command {:keyboardType :numeric}]) diff --git a/src/status_im/chat/views/content_suggestions.cljs b/src/status_im/chat/views/content_suggestions.cljs deleted file mode 100644 index 20fb1df9e5..0000000000 --- a/src/status_im/chat/views/content_suggestions.cljs +++ /dev/null @@ -1,36 +0,0 @@ -(ns status-im.chat.views.content-suggestions - (:require-macros [status-im.utils.views :refer [defview]]) - (:require [re-frame.core :refer [subscribe dispatch]] - [status-im.components.react :refer [view - icon - text - touchable-highlight - list-view - list-item]] - [status-im.chat.styles.content-suggestions :as st] - [status-im.utils.listview :refer [to-datasource]])) - -(defn set-command-content [content] - (dispatch [:set-chat-command-content content])) - -(defn suggestion-list-item [{:keys [value description]}] - [touchable-highlight {:onPress #(set-command-content value)} - [view st/suggestion-container - [view st/suggestion-sub-container - [text {:style st/value-text} value] - [text {:style st/description-text} description]]]]) - -(defn render-row [row _ _] - (list-item [suggestion-list-item row])) - -(defview content-suggestions-view [] - [suggestions [:get-content-suggestions]] - (when (seq suggestions) - [view - [touchable-highlight {:style st/drag-down-touchable - ;; TODO hide suggestions? - :onPress (fn [])} - [view [icon :drag_down st/drag-down-icon]]] - [view (st/suggestions-container (count suggestions)) - [list-view {:dataSource (to-datasource suggestions) - :renderRow render-row}]]])) diff --git a/src/status_im/chat/views/message.cljs b/src/status_im/chat/views/message.cljs index ceb430fc76..c7f910e799 100644 --- a/src/status_im/chat/views/message.cljs +++ b/src/status_im/chat/views/message.cljs @@ -1,23 +1,29 @@ (ns status-im.chat.views.message + (:require-macros [status-im.utils.views :refer [defview]]) (:require [clojure.string :as s] [re-frame.core :refer [subscribe dispatch]] + [reagent.core :as r] [status-im.components.react :refer [view - text - image - touchable-highlight]] + text + image + animated-view + touchable-highlight]] + [status-im.components.animation :as anim] + [status-im.chat.views.request-message :refer [message-content-command-request]] [status-im.chat.styles.message :as st] [status-im.models.commands :refer [parse-command-msg-content - parse-command-request]] + parse-command-request]] [status-im.resources :as res] + [status-im.utils.datetime :as time] [status-im.constants :refer [text-content-type - content-type-status - content-type-command - content-type-command-request]])) + content-type-status + content-type-command + content-type-command-request]])) -(defn message-date [{:keys [date]}] +(defn message-date [timestamp] [view {} [view st/message-date-container - [text {:style st/message-date-text} date]]]) + [text {:style st/message-date-text} (time/to-short-str timestamp)]]]) (defn contact-photo [{:keys [photo-path]}] [view st/contact-photo-container @@ -50,55 +56,30 @@ [view st/track-mark] [text {:style st/track-duration-text} "03:39"]]]) -(defn message-content-command [content] - (let [commands-atom (subscribe [:get-commands])] - (fn [content] - (let [commands @commands-atom - {:keys [command content]} - (parse-command-msg-content commands content)] - [view st/content-command-view - [view st/command-container - [view (st/command-view command) - [text {:style st/command-name} - (:text command)]]] - [image {:source (:icon command) - :style st/command-image}] - [text {:style st/command-text} - ;; TODO isn't smart - (if (= (:command command) :keypair-password) - "******" - content)]])))) +(defview message-content-command [content preview] + [commands [:get-commands-and-responses]] + (let [{:keys [command content]} (parse-command-msg-content commands content) + {:keys [name icon type]} command] + [view st/content-command-view + [view st/command-container + [view (st/command-view command) + [text {:style st/command-name} + (str (if (= :command type) "!" "") name)]]] + (when icon + [view st/command-image-view + [image {:source {:uri icon} + :style st/command-image}]]) + (if preview + preview + [text {:style st/command-text} content])])) (defn set-chat-command [msg-id command] - (dispatch [:set-response-chat-command msg-id (:command command)])) + (dispatch [:set-response-chat-command msg-id (keyword (:name command))])) (defn label [{:keys [command]}] - (->> (name command) + (->> (when command (name command)) (str "request-"))) -(defn message-content-command-request - [{:keys [msg-id content from incoming-group]}] - (let [commands-atom (subscribe [:get-commands])] - (fn [{:keys [msg-id content from incoming-group]}] - (let [commands @commands-atom - {:keys [command content]} (parse-command-request commands content)] - [touchable-highlight {:onPress #(set-chat-command msg-id command) - :accessibility-label (label command)} - [view st/comand-request-view - [view st/command-request-message-view - (when incoming-group - [text {:style st/command-request-from-text} - from]) - [text {:style st/style-message-text} - content]] - [view (st/command-request-image-view command) - [image {:source (:request-icon command) - :style st/command-request-image}]] - (when (:request-text command) - [view st/command-request-text-view - [text {:style st/style-sub-text} - (:request-text command)]])]])))) - (defn message-view [message content] [view (st/message-view message) @@ -128,9 +109,9 @@ [message-content-status message]) (defmethod message-content content-type-command - [wrapper {:keys [content] :as message}] + [wrapper {:keys [content rendered-preview] :as message}] [wrapper message - [message-view message [message-content-command content]]]) + [message-view message [message-content-command content rendered-preview]]]) (defmethod message-content :default [wrapper {:keys [content-type content] :as message}] @@ -185,12 +166,50 @@ (when (and outgoing delivery-status) [message-delivery-status {:delivery-status delivery-status}])])) +(defn message-container-animation-logic [{:keys [to-value val callback]}] + (fn [_] + (let [to-value @to-value] + (when (< 0 to-value) + (anim/start + (anim/spring val {:toValue to-value + :friction 4 + :tension 10}) + (fn [arg] + (when (.-finished arg) + (callback)))))))) + +(defn message-container [message & children] + (if (:new? message) + (let [layout-height (r/atom 0) + anim-value (anim/create-value 1) + anim-callback #(dispatch [:set-message-shown message]) + context {:to-value layout-height + :val anim-value + :callback anim-callback} + on-update (message-container-animation-logic context)] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [message & children] + @layout-height + [animated-view {:style (st/message-container anim-value)} + (into [view {:onLayout (fn [event] + (let [height (.. event -nativeEvent -layout -height)] + (reset! layout-height height)))}] + children)])})) + (into [view] children))) + (defn chat-message - [{:keys [outgoing delivery-status date new-day group-chat] + [{:keys [outgoing delivery-status timestamp new-day group-chat] :as message}] - [view {} - (when new-day [message-date {:date date}]) - [view {} + [message-container message + ;; TODO there is no new-day info in message + (when new-day + [message-date timestamp]) + [view (let [incoming-group (and group-chat (not outgoing))] [message-content (if incoming-group diff --git a/src/status_im/chat/views/message_input.cljs b/src/status_im/chat/views/message_input.cljs new file mode 100644 index 0000000000..c96b36f22e --- /dev/null +++ b/src/status_im/chat/views/message_input.cljs @@ -0,0 +1,71 @@ +(ns status-im.chat.views.message-input + (:require-macros [status-im.utils.views :refer [defview]]) + (:require [re-frame.core :refer [subscribe]] + [status-im.components.react :refer [view + animated-view + icon + touchable-highlight + text-input]] + [status-im.chat.views.plain-message :as plain-message] + [status-im.chat.views.command :as command] + [status-im.chat.styles.message-input :as st] + [status-im.chat.styles.plain-message :as st-message] + [status-im.chat.styles.response :as st-response])) + +(defn send-button [{:keys [on-press accessibility-label]}] + [touchable-highlight {:on-press on-press + :accessibility-label accessibility-label} + [view st/send-container + [icon :send st/send-icon]]]) + +(defn message-input-container [input] + [view st/message-input-container input]) + +(defn plain-input-options [disbale?] + {:style st-message/message-input + :onChangeText (when-not disbale? plain-message/set-input-message) + :editable (not disbale?) + :onSubmitEditing plain-message/send}) + +(defn command-input-options [icon-width disbale?] + {:style (st-response/command-input icon-width disbale?) + :onChangeText (when-not disbale? command/set-input-message) + :onSubmitEditing command/send-command}) + +(defview message-input [input-options] + [command? [:command?] + input-message [:get-chat-input-text] + input-command [:get-chat-command-content] + icon-width [:command-icon-width] + disbale? [:get :disable-input]] + [text-input (merge + (if command? + (command-input-options icon-width disbale?) + (plain-input-options disbale?)) + {:autoFocus false + :blurOnSubmit false + :accessibility-label :input} + input-options) + (if command? input-command input-message)]) + +(defview plain-message-input-view [{:keys [input-options validator]}] + [command? [:command?] + {:keys [type] :as command} [:get-chat-command] + input-command [:get-chat-command-content] + valid-plain-message? [:valid-plain-message?] + valid-command? [:valid-command? validator]] + [view st/input-container + [view st/input-view + [plain-message/commands-button] + [message-input-container + [message-input input-options validator]] + ;; TODO emoticons: not implemented + [plain-message/smile-button] + (when (if command? valid-command? valid-plain-message?) + (let [on-press (if command? + command/send-command + plain-message/send)] + [send-button {:on-press on-press + :accessibility-label :send-message}])) + (when (and command? (= :command type)) + [command/command-icon command])]]) diff --git a/src/status_im/chat/views/money.cljs b/src/status_im/chat/views/money.cljs deleted file mode 100644 index 0051b597f6..0000000000 --- a/src/status_im/chat/views/money.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns status-im.chat.views.money - (:require - [status-im.chat.views.command :refer [simple-command-input-view]])) - -(defn money-input-view [command] - [simple-command-input-view command - {:keyboardType :numeric}]) diff --git a/src/status_im/chat/views/new_message.cljs b/src/status_im/chat/views/new_message.cljs index 29b203a067..54bb81f2d7 100644 --- a/src/status_im/chat/views/new_message.cljs +++ b/src/status_im/chat/views/new_message.cljs @@ -1,14 +1,11 @@ (ns status-im.chat.views.new-message + (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe]] [status-im.components.react :refer [view]] - [status-im.chat.views.plain-input :refer [plain-message-input-view]] - [status-im.chat.views.command :refer [simple-command-input-view]] - [status-im.chat.views.phone :refer [phone-input-view]] - [status-im.chat.views.password :refer [password-input-view]] - [status-im.chat.views.confirmation-code :refer [confirmation-code-input-view]] - [status-im.chat.views.money :refer [money-input-view]] + [status-im.chat.views.message-input :refer [plain-message-input-view]] [status-im.chat.views.staged-command :refer [simple-command-staged-view]] + [status-im.utils.phone-number :refer [valid-mobile-number?]] [status-im.chat.styles.message :as st])) (defn staged-command-view [stage-command] @@ -19,27 +16,30 @@ (for [command staged-commands] ^{:key command} [staged-command-view command])]) -(defn default-command-input-view [command] - [simple-command-input-view command {}]) +(defn get-options [{:keys [type placeholder]} command-type] + (let [options (case (keyword type) + :phone {:input-options {:keyboardType :phone-pad} + :validator valid-mobile-number?} + :password {:input-options {:secureTextEntry true}} + :number {:input-options {:keyboardType :numeric}} + ;; todo maybe nil is fine for now :) + nil #_(throw (js/Error. "Uknown command type")))] + (if (= :response command-type) + (if placeholder + (assoc-in options [:input-options :placeholder] placeholder) + options) + (assoc-in options [:input-options :placeholder] "")))) -(defn special-input-view [command] - (case (:command command) - :phone [phone-input-view command] - :keypair-password [password-input-view command] - :confirmation-code [confirmation-code-input-view command] - :money [money-input-view command] - :request [money-input-view command] - [default-command-input-view command])) +(defview show-input [] + [parameter [:get-command-parameter] + command? [:command?] + type [:command-type]] + [plain-message-input-view + (when command? (get-options parameter type))]) -(defn chat-message-new [] - (let [command-atom (subscribe [:get-chat-command]) - staged-commands-atom (subscribe [:get-chat-staged-commands])] - (fn [] - (let [command @command-atom - staged-commands @staged-commands-atom] - [view st/new-message-container - (when (and staged-commands (pos? (count staged-commands))) - [staged-commands-view staged-commands]) - (if command - [special-input-view command] - [plain-message-input-view])])))) +(defview chat-message-new [] + [staged-commands [:get-chat-staged-commands]] + [view st/new-message-container + (when (seq staged-commands) + [staged-commands-view staged-commands]) + [show-input]]) diff --git a/src/status_im/chat/views/password.cljs b/src/status_im/chat/views/password.cljs deleted file mode 100644 index cc87e05fff..0000000000 --- a/src/status_im/chat/views/password.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(ns status-im.chat.views.password - (:require - [status-im.chat.views.command - :refer [simple-command-input-view]])) - -(defn password-input-view [command] - [simple-command-input-view command {:secureTextEntry true}]) diff --git a/src/status_im/chat/views/phone.cljs b/src/status_im/chat/views/phone.cljs deleted file mode 100644 index f74ad65568..0000000000 --- a/src/status_im/chat/views/phone.cljs +++ /dev/null @@ -1,9 +0,0 @@ -(ns status-im.chat.views.phone - (:require - [status-im.chat.views.command - :refer [simple-command-input-view]] - [status-im.utils.phone-number :refer [valid-mobile-number?]])) - -(defn phone-input-view [command] - [simple-command-input-view command {:keyboardType :phone-pad} - :validator valid-mobile-number?]) diff --git a/src/status_im/chat/views/plain_input.cljs b/src/status_im/chat/views/plain_input.cljs deleted file mode 100644 index dcbc75780a..0000000000 --- a/src/status_im/chat/views/plain_input.cljs +++ /dev/null @@ -1,54 +0,0 @@ -(ns status-im.chat.views.plain-input - (:require [re-frame.core :refer [subscribe dispatch]] - [status-im.components.react :refer [view - icon - touchable-highlight - text-input]] - [status-im.chat.views.suggestions :refer [suggestions-view]] - [status-im.chat.styles.plain-input :as st])) - -(defn set-input-message [message] - (dispatch [:set-chat-input-text message])) - -(defn send [chat input-message] - (let [{:keys [group-chat chat-id]} chat] - (dispatch [:send-chat-msg]))) - -(defn message-valid? [staged-commands message] - (or (and (pos? (count message)) - (not= "!" message)) - (pos? (count staged-commands)))) - -(defn try-send [chat staged-commands message] - (when (message-valid? staged-commands message) - (send chat message))) - -(defn plain-message-input-view [] - (let [chat (subscribe [:get-current-chat]) - input-message-atom (subscribe [:get-chat-input-text]) - staged-commands-atom (subscribe [:get-chat-staged-commands]) - typing-command? (subscribe [:typing-command?])] - (fn [] - (let [input-message @input-message-atom] - [view st/input-container - [suggestions-view] - [view st/input-view - [touchable-highlight {:on-press #(dispatch [:switch-command-suggestions]) - :style st/switch-commands-touchable} - [view nil - (if @typing-command? - [icon :close-gray st/close-icon] - [icon :list st/list-icon])]] - [text-input {:style st/message-input - :autoFocus (pos? (count @staged-commands-atom)) - :onChangeText set-input-message - :onSubmitEditing #(try-send @chat @staged-commands-atom - input-message)} - input-message] - ;; TODO emoticons: not implemented - [icon :smile st/smile-icon] - (when (message-valid? @staged-commands-atom input-message) - [touchable-highlight {:on-press #(send @chat input-message) - :accessibility-label :send-message} - [view st/send-container - [icon :send st/send-icon]]])]])))) diff --git a/src/status_im/chat/views/plain_message.cljs b/src/status_im/chat/views/plain_message.cljs new file mode 100644 index 0000000000..f1dc3a0cde --- /dev/null +++ b/src/status_im/chat/views/plain_message.cljs @@ -0,0 +1,103 @@ +(ns status-im.chat.views.plain-message + (:require-macros [status-im.utils.views :refer [defview]]) + (:require [re-frame.core :refer [subscribe dispatch]] + [reagent.core :as r] + [status-im.components.react :refer [view + animated-view + icon + text + touchable-highlight]] + [status-im.components.animation :as anim] + [status-im.chat.styles.plain-message :as st] + [status-im.constants :refer [response-input-hiding-duration]])) + +(defn set-input-message [message] + (dispatch [:set-chat-input-text message])) + +(defn send [] + (dispatch [:send-chat-msg])) + +(defn message-valid? [staged-commands message] + (or (and (pos? (count message)) + (not= "!" message)) + (pos? (count staged-commands)))) + +(defn button-animation-logic [{:keys [command? val]}] + (fn [_] + (let [to-scale (if @command? 0 1)] + (anim/start (anim/spring val {:toValue to-scale + :tension 30}))))) + +(defn list-container [min] + (fn [{:keys [command? width]}] + (let [n-width (if @command? min 56) + delay (if @command? 100 0)] + (anim/start (anim/timing width {:toValue n-width + :duration response-input-hiding-duration + :delay delay}) + #(dispatch [:set :disable-input false]))))) + +(defn commands-button [] + (let [command? (subscribe [:command?]) + requests (subscribe [:get-requests]) + suggestions (subscribe [:get-suggestions]) + buttons-scale (anim/create-value (if @command? 1 0)) + container-width (anim/create-value (if @command? 20 56)) + context {:command? command? + :val buttons-scale + :width container-width} + on-update (fn [_] + ((button-animation-logic context)) + ((list-container 20) context))] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [] + [touchable-highlight {:on-press #(dispatch [:switch-command-suggestions!]) + :disabled @command?} + [animated-view {:style (st/message-input-button-touchable + container-width)} + [animated-view {:style (st/message-input-button buttons-scale)} + (if (seq @suggestions) + [icon :close_gray st/close-icon] + [icon :input_list st/list-icon]) + (when (and (seq @requests) + (not (seq @suggestions))) + [view st/requests-icon])]]])}))) + +(defn smile-animation-logic [{:keys [command? val width]}] + (fn [_] + (let [to-scale (if @command? 0 1)] + (when-not @command? (anim/set-value width 56)) + (anim/start (anim/spring val {:toValue to-scale + :tension 30}) + (fn [e] + (when (and @command? (.-finished e)) + (anim/set-value width 0.1))))))) + +(defn smile-button [] + (let [command? (subscribe [:command?]) + buttons-scale (anim/create-value (if @command? 1 0)) + container-width (anim/create-value (if @command? 0.1 56)) + context {:command? command? + :val buttons-scale + :width container-width} + on-update (smile-animation-logic context)] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [] + [touchable-highlight {:on-press (fn [] + ;; TODO emoticons: not implemented + ) + :disabled @command?} + [animated-view {:style (st/message-input-button-touchable + container-width)} + [animated-view {:style (st/message-input-button buttons-scale)} + [icon :smile st/smile-icon]]]])}))) diff --git a/src/status_im/chat/views/request_message.cljs b/src/status_im/chat/views/request_message.cljs new file mode 100644 index 0000000000..2bbd4e11df --- /dev/null +++ b/src/status_im/chat/views/request_message.cljs @@ -0,0 +1,84 @@ +(ns status-im.chat.views.request-message + (:require [re-frame.core :refer [subscribe dispatch]] + [reagent.core :as r] + [status-im.components.react :refer [view + animated-view + text + image + touchable-highlight]] + [status-im.chat.styles.message :as st] + [status-im.models.commands :refer [parse-command-request]] + [status-im.components.animation :as anim])) + +(def request-message-icon-scale-delay 600) + +(defn set-chat-command [msg-id command] + (dispatch [:set-response-chat-command msg-id (keyword (:name command))])) + +(defn label [{:keys [command]}] + (->> (name command) + (str "request-"))) + +(def min-scale 1) +(def max-scale 1.3) + +(defn button-animation [val to-value loop? answered?] + (anim/anim-sequence + [(anim/anim-delay + (if (and @loop? (not @answered?)) + request-message-icon-scale-delay + 0)) + (anim/spring val {:toValue to-value})])) + +(defn request-button-animation-logic + [{:keys [to-value val loop? answered?] :as context}] + (anim/start + (button-animation val to-value loop? answered?) + #(if (and @loop? (not @answered?)) + (let [new-value (if (= to-value min-scale) max-scale min-scale) + context' (assoc context :to-value new-value)] + (request-button-animation-logic context')) + (anim/start + (button-animation val min-scale loop? answered?))))) + +(defn request-button [msg-id command] + (let [scale-anim-val (anim/create-value min-scale) + answered? (subscribe [:is-request-answered? msg-id]) + loop? (r/atom true) + context {:to-value max-scale + :val scale-anim-val + :answered? answered? + :loop? loop?}] + (r/create-class + {:component-did-mount + (when-not @answered? #(request-button-animation-logic context)) + :component-will-unmount + #(reset! loop? false) + :reagent-render + (fn [msg-id command] + [touchable-highlight + {:on-press (when-not @answered? + #(set-chat-command msg-id command)) + :style st/command-request-image-touchable} + [animated-view {:style (st/command-request-image-view command scale-anim-val)} + [image {:source {:uri (:icon command)} + :style st/command-request-image}]]])}))) + +(defn message-content-command-request + [{:keys [msg-id content from incoming-group]}] + (let [commands-atom (subscribe [:get-responses])] + (fn [{:keys [msg-id content from incoming-group]}] + (let [commands @commands-atom + {:keys [command content]} (parse-command-request commands content)] + [view st/comand-request-view + [view st/command-request-message-view + (when incoming-group + [text {:style st/command-request-from-text} + from]) + [text {:style st/style-message-text} + content]] + [request-button msg-id command] + (when (:request-text command) + [view st/command-request-text-view + [text {:style st/style-sub-text} + (:request-text command)]])])))) diff --git a/src/status_im/chat/views/response.cljs b/src/status_im/chat/views/response.cljs new file mode 100644 index 0000000000..eaad55d093 --- /dev/null +++ b/src/status_im/chat/views/response.cljs @@ -0,0 +1,96 @@ +(ns status-im.chat.views.response + (:require-macros [reagent.ratom :refer [reaction]] + [status-im.utils.views :refer [defview]]) + (:require [re-frame.core :refer [subscribe dispatch]] + [reagent.core :as r] + [status-im.components.react :refer [view + animated-view + icon + image + text + text-input + touchable-highlight]] + [status-im.components.drag-drop :as drag] + [status-im.chat.styles.response :as st] + [status-im.chat.styles.dragdown :as ddst] + [status-im.components.animation :as anim] + [status-im.chat.suggestions-responder :as resp])) + +(defn drag-icon [] + [view st/drag-container + [icon :drag-white st/drag-icon]]) + +(defn command-icon [] + [view st/command-icon-container + ;; TODO stub data: command icon + [icon :dollar-green st/command-icon]]) + +(defn info-container [command] + [view st/info-container + [text {:style st/command-name} + (:description command)] + [text {:style st/message-info} + ;; TODO stub data: request message info + "By ???, MMM 1st at HH:mm"]]) + +(defn request-info [response-height] + (let [orientation (subscribe [:get :orientation]) + kb-height (subscribe [:get :keyboard-height]) + pan-responder (resp/pan-responder response-height + kb-height + orientation + :fix-response-height) + command (subscribe [:get-chat-command])] + (fn [response-height] + (if (= :response (:type @command)) + [view (merge (drag/pan-handlers pan-responder) + {:style (st/request-info (:color @command))}) + [drag-icon] + [view st/inner-container + [command-icon nil] + [info-container @command] + [touchable-highlight {:on-press #(dispatch [:start-cancel-command])} + [view st/cancel-container + [icon :close-white st/cancel-icon]]]]] + [view (merge (drag/pan-handlers pan-responder) + {:style ddst/drag-down-touchable}) + [icon :drag_down ddst/drag-down-icon]])))) + +(defn container-animation-logic [{:keys [to-value val]}] + (let [to-value @to-value] + (anim/start (anim/spring val {:toValue to-value})))) + +(defn container [response-height & children] + (let [;; todo to-response-height, cur-response-height must be specific + ;; for each chat + to-response-height (subscribe [:animations :to-response-height]) + changed (subscribe [:animations :response-height-changed]) + context {:to-value to-response-height + :val response-height} + on-update #(container-animation-logic context)] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [response-height & children] + @to-response-height @changed + (into [animated-view {:style (st/response-view response-height)}] + children))}))) + +(defview placeholder [] + [suggestions [:get-content-suggestions]] + (when (seq suggestions) + [view st/input-placeholder])) + +(defview response-suggestions-view [] + [suggestions [:get-content-suggestions]] + (when (seq suggestions) suggestions)) + +(defn response-view [] + (let [response-height (anim/create-value 0)] + [container response-height + [request-info response-height] + [response-suggestions-view] + [placeholder]])) diff --git a/src/status_im/chat/views/staged_command.cljs b/src/status_im/chat/views/staged_command.cljs index be8c811459..424367c56e 100644 --- a/src/status_im/chat/views/staged_command.cljs +++ b/src/status_im/chat/views/staged_command.cljs @@ -1,9 +1,9 @@ (ns status-im.chat.views.staged-command (:require [re-frame.core :refer [subscribe dispatch]] [status-im.components.react :refer [view - image - text - touchable-highlight]] + image + text + touchable-highlight]] [status-im.resources :as res] [status-im.chat.styles.input :as st])) @@ -16,13 +16,12 @@ [view st/staged-command-background [view st/staged-command-info-container [view (st/staged-command-text-container command) - [text {:style st/staged-command-text} (:text command)]] + [text {:style st/staged-command-text} (str "!" (:name command))]] [touchable-highlight {:style st/staged-command-cancel :onPress #(cancel-command-input staged-command)} [image {:source res/icon-close-gray :style st/staged-command-cancel-icon}]]] - [text {:style st/staged-command-content} - ;; TODO isn't smart - (if (= (:command command) :keypair-password) - "******" - (:content staged-command))]]])) + (if-let [preview (:preview staged-command)] + preview + [text {:style st/staged-command-content} + (:content staged-command)])]])) diff --git a/src/status_im/chat/views/suggestions.cljs b/src/status_im/chat/views/suggestions.cljs index 9a170cd5a5..ca2bd8d0fe 100644 --- a/src/status_im/chat/views/suggestions.cljs +++ b/src/status_im/chat/views/suggestions.cljs @@ -1,46 +1,126 @@ (ns status-im.chat.views.suggestions + (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch]] [status-im.components.react :refer [view - text - icon - touchable-highlight - list-view - list-item]] + scroll-view + text + icon + image + touchable-highlight + list-view + list-item + animated-view]] [status-im.utils.listview :refer [to-datasource]] - [status-im.chat.styles.suggestions :as st])) + [status-im.chat.styles.suggestions :as st] + [status-im.chat.styles.dragdown :as ddst] + [reagent.core :as r] + [status-im.components.animation :as anim] + [status-im.components.drag-drop :as drag] + [status-im.chat.suggestions-responder :as resp] + [status-im.chat.constants :as c])) (defn set-command-input [command] (dispatch [:set-chat-command command])) -(defn suggestion-list-item - [{:keys [description command] - label :text - :as suggestion}] +(defview request-item [{:keys [type message-id]}] + [{:keys [color icon description] :as response} [:get-response type]] [touchable-highlight - {:onPress #(set-command-input (keyword command))} - [view st/suggestion-container - [view st/suggestion-sub-container - [view (st/suggestion-background suggestion) - [text {:style st/suggestion-text} label]] - [text {:style st/value-text} label] - [text {:style st/description-text} description]]]]) + {:on-press #(dispatch [:set-response-chat-command message-id type])} + [view st/request-container + [view st/request-icon-container + [view (st/request-icon-background color) + [image {:source {:uri icon} + :style st/request-icon}]]] + [view st/request-info-container + [text {:style st/request-info-description} description] + ;; todo stub + [text {:style st/request-message-info} + "By console, today at 14:50"]]]]) + +(defn render-request-row + [{:keys [chat-id message-id] :as row} _ _] + (list-item + ^{:key [chat-id message-id]} + [request-item row])) + +(defn suggestion-list-item + [[command {:keys [description] + name :name + :as suggestion}]] + (let [label (str "!" name)] + [touchable-highlight + {:onPress #(set-command-input command) + :style st/suggestion-highlight} + [view st/suggestion-container + [view st/suggestion-sub-container + [view st/command-description-container + [text {:style st/value-text} label] + [text {:style st/description-text} description]] + [view st/command-label-container + [view (st/suggestion-background suggestion) + [text {:style st/suggestion-text} label]]]]]])) (defn render-row [row _ _] (list-item [suggestion-list-item row])) -(defn suggestions-view [] - (let [suggestions-atom (subscribe [:get-suggestions])] - (fn [] - (let [suggestions @suggestions-atom] - (when (seq suggestions) - [view - [touchable-highlight {:style st/drag-down-touchable - :onPress (fn [] - ;; TODO hide suggestions? - )} - [view - [icon :drag_down st/drag-down-icon]]] - [view (st/suggestions-container (count suggestions)) - [list-view {:dataSource (to-datasource suggestions) - :enableEmptySections true - :renderRow render-row}]]]))))) +(defn title [s] + [view st/title-container + [text {:style st/title-text} s]]) + +(defview suggestions-view [] + [suggestions [:get-suggestions] + requests [:get-requests]] + [scroll-view {:keyboardShouldPersistTaps true} + (when (seq requests) [title "Requests"]) + (when (seq requests) + [view + [list-view {:dataSource (to-datasource requests) + :keyboardShouldPersistTaps true + :renderRow render-request-row}]]) + [title "Commands"] + [view + [list-view {:dataSource (to-datasource suggestions) + :keyboardShouldPersistTaps true + :renderRow render-row}]]]) + +(defn header [h] + (let [orientation (subscribe [:get :orientation]) + kb-height (subscribe [:get :keyboard-height]) + pan-responder (resp/pan-responder h + kb-height + orientation + :fix-commands-suggestions-height)] + (fn [_] + [view + (merge (drag/pan-handlers pan-responder) + {:style ddst/drag-down-touchable}) + [view st/header-icon]]))) + +(defn container-animation-logic [{:keys [to-value val]}] + (when-let [to-value @to-value] + (anim/start (anim/spring val {:toValue to-value})))) + +(defn container [h & elements] + (let [;; todo to-response-height, cur-response-height must be specific + ;; for each chat + to-response-height (subscribe [:animations :command-suggestions-height]) + changed (subscribe [:animations :commands-height-changed]) + context {:to-value to-response-height + :val h} + on-update #(container-animation-logic context)] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [h & elements] + @to-response-height @changed + (into [animated-view {:style (st/container h)}] elements))}))) + +(defn suggestion-container [] + (let [h (anim/create-value c/input-height)] + [container h + [header h] + [suggestions-view] + [view {:height c/input-height}]])) diff --git a/src/status_im/chats_list/screen.cljs b/src/status_im/chats_list/screen.cljs index 572f0ac5a9..9e38f48307 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]] @@ -11,42 +13,68 @@ [status-im.chats-list.views.chat-list-item :refer [chat-list-item]] [status-im.components.action-button :refer [action-button action-button-item]] - [status-im.components.drawer.view :refer [drawer-view open-drawer]] + [status-im.components.drawer.view :refer [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}] + [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 + ;;; 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 [:navigate-to :contact-list])} + :onPress #(dispatch [:show-group-contacts :people])} [icon {:name :android-create :style st/create-icon}]] [action-button-item 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/chats_list/views/inner_item.cljs b/src/status_im/chats_list/views/inner_item.cljs index 783578c3a1..2803b5d6e4 100644 --- a/src/status_im/chats_list/views/inner_item.cljs +++ b/src/status_im/chats_list/views/inner_item.cljs @@ -7,7 +7,7 @@ [status-im.utils.datetime :as time])) (defn chat-list-item-inner-view - [{:keys [chat-id name color photo-path new-messages-count + [{:keys [chat-id name color new-messages-count online group-chat contacts] :as chat}] (let [last-message (first (:messages chat))] [view st/chat-container diff --git a/src/status_im/commands/handlers/jail.cljs b/src/status_im/commands/handlers/jail.cljs new file mode 100644 index 0000000000..d5852f5ee7 --- /dev/null +++ b/src/status_im/commands/handlers/jail.cljs @@ -0,0 +1,79 @@ +(ns status-im.commands.handlers.jail + (:require [re-frame.core :refer [after dispatch subscribe trim-v debug]] + [status-im.utils.handlers :as u] + [status-im.utils.utils :refer [http-get toast]] + [status-im.components.jail :as j] + [status-im.utils.types :refer [json->clj]] + [status-im.commands.utils :refer [generate-hiccup reg-handler]] + [clojure.string :as s])) + +(defn init-render-command! + [_ [chat-id command message-id data]] + (j/call chat-id [command :render] data + #(dispatch [::render-command chat-id message-id %]))) + +(defn render-command + [db [chat-id message-id markup]] + (let [hiccup (generate-hiccup markup)] + (assoc-in db [:rendered-commands chat-id message-id] hiccup))) + +(def console-events + {:save-password #(dispatch [:save-password %]) + :sign-up #(dispatch [:sign-up %]) + :confirm-sign-up #(dispatch [:sign-up-confirm %])}) + +(def regular-events {}) + +(defn command-hadler! + [_ [{:keys [to]} {:keys [result]} ]] + (when result + (let [{:keys [event params]} result + events (if (= "console" to) + (merge regular-events console-events) + regular-events)] + (when-let [handler (events (keyword event))] + (apply handler params))))) + +(defn suggestions-handler! + [db [{:keys [chat-id]} {:keys [result]} ]] + (assoc-in db [:suggestions chat-id] (generate-hiccup result))) + +(defn suggestions-events-handler! + [db [[n data]]] + (case (keyword n) + :set-value (dispatch [:set-chat-command-content data]) + ;; todo show error? + nil)) + +(defn command-preview + [db [chat-id {:keys [result]}]] + (if result + (let [path [:chats chat-id :staged-commands] + commands-cnt (count (get-in db path))] + ;; todo (dec commands-cnt) looks like hack have to find better way to + ;; do this + (update-in db (conj path (dec commands-cnt)) assoc + :preview (generate-hiccup result) + :preview-string (str result))) + db)) + +(defn print-error-message! [message] + (fn [_ params] + (when (:error (last params)) + (toast (s/join "\n" [message params])) + (println message params)))) + +(reg-handler :init-render-command! init-render-command!) +(reg-handler ::render-command render-command) + +(reg-handler :command-handler! + (after (print-error-message! "Error on command handling")) + (u/side-effect! command-hadler!)) +(reg-handler :suggestions-handler + [(after #(dispatch [:animate-show-response])) + (after (print-error-message! "Error on param suggestions"))] + suggestions-handler!) +(reg-handler :suggestions-event! (u/side-effect! suggestions-events-handler!)) +(reg-handler :command-preview + (after (print-error-message! "Error on command preview")) + command-preview) diff --git a/src/status_im/commands/handlers/loading.cljs b/src/status_im/commands/handlers/loading.cljs new file mode 100644 index 0000000000..ccecdc86e5 --- /dev/null +++ b/src/status_im/commands/handlers/loading.cljs @@ -0,0 +1,102 @@ +(ns status-im.commands.handlers.loading + (:require-macros [status-im.utils.slurp :refer [slurp]]) + (:require [re-frame.core :refer [after dispatch subscribe trim-v debug]] + [status-im.utils.handlers :as u] + [status-im.utils.utils :refer [http-get toast]] + [clojure.string :as s] + [status-im.persistence.realm :as realm] + [status-im.components.jail :as j] + [status-im.utils.types :refer [json->clj]] + [status-im.commands.utils :refer [reg-handler]])) + +(def commands-js "commands.js") + +(defn load-commands! + [_ [identity]] + (dispatch [::fetch-commands! identity]) + ;; todo uncomment + #_(if-let [{:keys [file]} (realm/get-one-by-field :commands :chat-id + identity)] + (dispatch [::parse-commands! identity file]) + (dispatch [::fetch-commands! identity]))) + +(defn fetch-commands! + [db [identity]] + (when-let [url (:dapp-url (get-in db [:chats identity]))] + (if (= "console" identity) + (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]))))) + +(defn dispatch-loaded! + [db [identity file]] + (if (::valid-hash db) + (dispatch [::parse-commands! identity file]) + (dispatch [::loading-failed! identity ::wrong-hash]))) + +(defn get-hash-by-identity + [db identity] + (get-in db [:chats identity :dapp-hash])) + +(defn get-hash-by-file + [file] + ;; todo tbd hashing algorithm + (hash file)) + +(defn parse-commands! [_ [identity file]] + (j/parse identity file + (fn [result] + (let [{:keys [error result]} (json->clj result)] + (if error + (dispatch [::loading-failed! identity ::error-in-jail error]) + (dispatch [::add-commands identity file result])))))) + +(defn validate-hash + [db [identity file]] + (let [valid? true + ;; todo check + #_(= (get-hash-by-identity db identity) + (get-hash-by-file file))] + (assoc db ::valid-hash valid?))) + +(defn mark-as [as coll] + (->> coll + (map (fn [[k v]] [k (assoc v :type as)])) + (into {}))) + +(defn add-commands + [db [id _ {:keys [commands responses]}]] + (-> db + (update-in [:chats id :commands] merge (mark-as :command commands)) + (update-in [:chats id :responses] merge (mark-as :response responses)))) + +(defn save-commands-js! + [_ [id file]] + (realm/create-object :commands {:chat-id id :file file})) + +(defn loading-failed! + [db [id reason details]] + (let [url (get-in db [:chats id :dapp-url])] + (let [m (s/join "\n" ["commands.js loading failed" + url + id + (name reason) + details])] + (toast m) + (println m)))) + +(reg-handler :load-commands! (u/side-effect! load-commands!)) +(reg-handler ::fetch-commands! (u/side-effect! fetch-commands!)) + +(reg-handler ::validate-hash + (after dispatch-loaded!) + validate-hash) + +(reg-handler ::parse-commands! (u/side-effect! parse-commands!)) + +(reg-handler ::add-commands + (after save-commands-js!) + add-commands) + +(reg-handler ::loading-failed! (u/side-effect! loading-failed!)) diff --git a/src/status_im/commands/utils.cljs b/src/status_im/commands/utils.cljs new file mode 100644 index 0000000000..af728dabad --- /dev/null +++ b/src/status_im/commands/utils.cljs @@ -0,0 +1,48 @@ +(ns status-im.commands.utils + (:require [clojure.set :as set] + [clojure.walk :as w] + [status-im.components.react :refer [text scroll-view view + image touchable-highlight]] + [re-frame.core :refer [dispatch trim-v debug]] + [status-im.utils.handlers :refer [register-handler]])) + +(defn json->clj [json] + (if (= json "undefined") + nil + (js->clj (.parse js/JSON json) :keywordize-keys true))) + +(def elements + {:text text + :view view + :scroll-view scroll-view + :image image + :touchable touchable-highlight}) + +(defn get-element [n] + (elements (keyword (.toLowerCase n)))) + +(def events #{:onPress}) + +(defn wrap-event [event] + #(dispatch [:suggestions-event! event])) + +(defn check-events [m] + (let [ks (set (keys m)) + evs (set/intersection ks events)] + (reduce #(update %1 %2 wrap-event) m evs))) + +(defn generate-hiccup [markup] + ;; todo implement validation + (w/prewalk + (fn [el] + (if (and (vector? el) (string? (first el))) + (-> el + (update 0 get-element) + (update 1 check-events)) + el)) + markup)) + +(defn reg-handler + ([name handler] (reg-handler name nil handler)) + ([name middleware handler] + (register-handler name [trim-v middleware] handler))) diff --git a/src/status_im/components/animation.cljs b/src/status_im/components/animation.cljs new file mode 100644 index 0000000000..74d3cb3f78 --- /dev/null +++ b/src/status_im/components/animation.cljs @@ -0,0 +1,54 @@ +(ns status-im.components.animation + (:require [status-im.components.react :refer [animated]])) + +(defn start + ([anim] (.start anim)) + ([anim callback] (.start anim callback))) + +(defn timing [anim-value config] + (.timing animated anim-value (clj->js config))) + +(defn spring [anim-value config] + (.spring animated anim-value (clj->js config))) + +(defn decay [anim-value config] + (.decay animated anim-value (clj->js config))) + +(defn anim-sequence [animations] + (.sequence animated (clj->js animations))) + +(defn anim-delay [duration] + (.delay animated duration)) + +(defn event [config] + (.event animated (clj->js [nil, config]))) + +(defn add-listener [anim-value listener] + (.addListener anim-value listener)) + +(defn remove-all-listeners [anim-value] + (.removeAllListeners anim-value)) + +(defn stop-animation [anim-value] + (.stopAnimation anim-value)) + +(defn value [anim-value] + (.-value anim-value)) + +(defn set-value [anim-value value] + (.setValue anim-value value)) + +(defn create-value [value] + (js/React.Animated.Value. value)) + +(defn x [value-xy] + (.-x value-xy)) + +(defn y [value-xy] + (.-y value-xy)) + +(defn get-layout [value-xy] + (js->clj (.getLayout value-xy))) + +(defn create-value-xy [x y] + (js/React.Animated.ValueXY. (clj->js {:x x, :y y}))) diff --git a/src/status_im/components/chat_icon/screen.cljs b/src/status_im/components/chat_icon/screen.cljs index 0d93e38cbb..3b6437c193 100644 --- a/src/status_im/components/chat_icon/screen.cljs +++ b/src/status_im/components/chat_icon/screen.cljs @@ -6,7 +6,7 @@ image icon]] [status-im.components.chat-icon.styles :as st] - [status-im.components.styles :refer [color-purple]] + [status-im.components.styles :refer [default-chat-color]] [clojure.string :as s])) (defn default-chat-icon [name styles] @@ -63,6 +63,26 @@ :default-chat-icon (st/default-chat-icon-menu-item color) :default-chat-icon-text st/default-chat-icon-text}]) +(defn contact-icon-view [contact styles] + (let [photo-path (:photo-path contact) + ;; TODO stub data + online true] + [view (:container styles) + (if-not (s/blank? photo-path) + [chat-icon photo-path styles] + [default-chat-icon (:name contact) styles]) + [contact-online online styles]])) + +(defn contact-icon-contacts-tab [contact] + [contact-icon-view contact + {: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 default-chat-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 @@ -81,7 +101,7 @@ [contact [:contact]] (let [;; TODO stub data online true - color color-purple] + color default-chat-color] [profile-icon-view (:photo-path contact) (:name contact) color online])) (defview my-profile-icon [] @@ -89,5 +109,5 @@ photo-path [:get :photo-path]] (let [;; TODO stub data online true - color color-purple] + color default-chat-color] [profile-icon-view photo-path name color online])) diff --git a/src/status_im/components/drag_drop.cljs b/src/status_im/components/drag_drop.cljs new file mode 100644 index 0000000000..708efdfb12 --- /dev/null +++ b/src/status_im/components/drag_drop.cljs @@ -0,0 +1,12 @@ +(ns status-im.components.drag-drop + (:require [status-im.components.react :refer [animated pan-responder]] + [status-im.components.animation :as anim])) + +(defn pan-handlers [pan-responder] + (js->clj (.-panHandlers pan-responder))) + +(defn create-pan-responder [{:keys [on-move on-release]}] + (.create pan-responder + (clj->js {:onStartShouldSetPanResponder (fn [] true) + :onPanResponderMove on-move + :onPanResponderRelease on-release}))) diff --git a/src/status_im/components/jail.cljs b/src/status_im/components/jail.cljs new file mode 100644 index 0000000000..daf278f4f8 --- /dev/null +++ b/src/status_im/components/jail.cljs @@ -0,0 +1,27 @@ +(ns status-im.components.jail + (:require-macros [status-im.utils.slurp :refer [slurp]]) + (:require [status-im.components.react :as r] + [status-im.utils.types :as t])) + +(def status-js (slurp "resources/status.js")) + +(def jail + (when (exists? (.-NativeModules r/react)) + (.-Jail (.-NativeModules r/react)))) + +(when jail + (.init jail status-js)) + +(defn parse [chat-id file callback] + (.parse jail chat-id file callback)) + +(defn cljs->json [data] + (.stringify js/JSON (clj->js data))) + +(defn call [chat-id path params callback] + (println :call chat-id (cljs->json path) (cljs->json params)) + (let [cb (fn [r] + (let [r' (t/json->clj r)] + (println r') + (callback r')))] + (.call jail chat-id (cljs->json path) (cljs->json params) cb))) diff --git a/src/status_im/components/main_tabs.cljs b/src/status_im/components/main_tabs.cljs index 16fb5b074d..d59b1d676b 100644 --- a/src/status_im/components/main_tabs.cljs +++ b/src/status_im/components/main_tabs.cljs @@ -1,11 +1,17 @@ (ns status-im.components.main-tabs - (:require-macros [status-im.utils.views :refer [defview]]) + (:require-macros [reagent.ratom :refer [reaction]] + [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [reagent.core :as r] [status-im.components.react :refer [view + animated-view text-input text image - touchable-highlight]] + touchable-highlight + get-dimensions]] + [status-im.components.drawer.view :refer [drawer-view]] + [status-im.components.animation :as anim] [status-im.chats-list.screen :refer [chats-list]] [status-im.discovery.screen :refer [discovery]] [status-im.contacts.screen :refer [contact-list]] @@ -14,33 +20,92 @@ [status-im.components.styles :as common-st] [status-im.i18n :refer [label]])) +(def window-width (:width (get-dimensions "window"))) + (def tab-list [{:view-id :chat-list :title (label :t/chats) :screen chats-list - :icon :icon_tab_chats} + :icon :icon_tab_chats + :index 0} {:view-id :discovery :title (label :t/discovery) :screen discovery - :icon :icon_tab_discovery} + :icon :icon_tab_discovery + :index 1} {:view-id :contact-list :title (label :t/contacts) :screen contact-list - :icon :icon_tab_contacts}]) + :icon :icon_tab_contacts + :index 2}]) -(defn show-view? [current-view view-id] - (let [key-map {:key view-id}] - (if (= current-view view-id) - (merge st/show-tab key-map) - (merge st/hide-tab key-map)))) +(defn animation-logic [{:keys [offsets val tab-id to-tab-id]}] + (fn [_] + (when-let [offsets @offsets] + (let [from-value (:from offsets) + to-value (:to offsets) + to-tab-id @to-tab-id] + (anim/set-value val from-value) + (when to-value + (anim/start + (anim/timing val {:toValue to-value + :duration 300}) + (when (= tab-id to-tab-id) + (fn [arg] + (when (.-finished arg) + (dispatch [:on-navigated-to-tab])))))))))) -(defn tab-view [current-view {:keys [view-id screen]}] - [view (show-view? current-view view-id) +(defn get-tab-index-by-id [id] + (:index (first (filter #(= id (:view-id %)) tab-list)))) + +(defn get-offsets [tab-id from-id to-id] + (let [tab (get-tab-index-by-id tab-id) + from (get-tab-index-by-id from-id) + to (get-tab-index-by-id to-id)] + (if (or (= tab from) (= tab to)) + (cond + (or (nil? from) (= from to)) {:from 0} + (< from to) (if (= tab to) + {:from window-width, :to 0} + {:from 0, :to (- window-width)}) + (< to from) (if (= tab to) + {:from (- window-width), :to 0} + {:from 0, :to window-width})) + {:from (- window-width)}))) + +(defn tab-view-container [tab-id content] + (let [cur-tab-id (subscribe [:get :view-id]) + prev-tab-id (subscribe [:get :prev-tab-view-id]) + offsets (reaction (get-offsets tab-id @prev-tab-id @cur-tab-id)) + anim-value (anim/create-value (- window-width)) + context {:offsets offsets + :val anim-value + :tab-id tab-id + :to-tab-id cur-tab-id} + on-update (animation-logic context)] + (r/create-class + {:component-did-mount + on-update + :component-did-update + on-update + :reagent-render + (fn [tab-id content] + @offsets + [animated-view {:style (st/tab-view-container anim-value)} + content])}))) + +(defn tab-view [{:keys [view-id screen]}] + ^{:key view-id} + [tab-view-container view-id [screen]]) (defview main-tabs [] - [view-id [:get :view-id]] - [view common-st/flex - (doall (map #(tab-view view-id %1) tab-list)) + [view-id [:get :view-id] + tab-animation? [:get :prev-tab-view-id]] + [drawer-view + [view {:style common-st/flex} + [view {:style common-st/flex + :pointerEvents (if tab-animation? :none :auto)} + (doall (map #(tab-view %) tab-list))] [tabs {:selected-view-id view-id - :tab-list tab-list}]]) + :tab-list tab-list}]]]) diff --git a/src/status_im/components/react.cljs b/src/status_im/components/react.cljs index 0ab2d2b755..f931ce4e00 100644 --- a/src/status_im/components/react.cljs +++ b/src/status_im/components/react.cljs @@ -46,6 +46,14 @@ (when-let [picker (get-react-property "Picker")] (adapt-class (.-Item picker)))) +(def pan-responder (.-PanResponder js/React)) +(def animated (.-Animated js/React)) +(def animated-view (r/adapt-react-class (.-View animated))) +(def animated-text (r/adapt-react-class (.-Text animated))) + +(def dimensions (.-Dimensions js/React)) +(defn get-dimensions [name] + (js->clj (.get dimensions name) :keywordize-keys true)) (defn icon ([n] (icon n {})) @@ -68,3 +76,5 @@ (r/as-element component)) (def dismiss-keyboard! (u/require "dismissKeyboard")) +(def device-event-emitter (.-DeviceEventEmitter react)) +(def orientation (u/require "react-native-orientation")) diff --git a/src/status_im/components/styles.cljs b/src/status_im/components/styles.cljs index 70e907c631..aaa82f5a42 100644 --- a/src/status_im/components/styles.cljs +++ b/src/status_im/components/styles.cljs @@ -10,6 +10,7 @@ (def color-black "#000000de") (def color-purple "#a187d5") (def color-gray "#838c93de") +(def color-gray2 "#8f838c93") (def color-white :white) (def color-light-blue "#bbc4cb") (def color-light-blue-transparent "#bbc4cb32") @@ -20,6 +21,7 @@ (def text2-color color-gray) (def text3-color color-blue) (def text4-color color-white) +(def text5-color "#838c938f") (def online-color color-blue) (def new-messages-count-color color-blue-transparent) (def chat-background color-light-gray) @@ -32,7 +34,7 @@ (def toolbar-height 56) (def flex - {:style {:flex 1}}) + {:flex 1}) (def hamburger-icon {:width 16 @@ -98,4 +100,4 @@ (def button-input {:flex 1 :flexDirection :column - :height 50}) \ No newline at end of file + :height 50}) diff --git a/src/status_im/components/tabs/styles.cljs b/src/status_im/components/tabs/styles.cljs index f3571cf3c8..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 @@ -54,17 +54,10 @@ :justifyContent :center :alignItems :center}) -(def show-tab - {:flex 1 - :pointerEvents :auto - :position :absolute - :top 0 - :left 0 - :right 0 - :bottom tab-height}) - -(def hide-tab - {:opacity 0 - :pointerEvents :none - :overflow :hidden}) - +(defn tab-view-container [offset-x] + {:position :absolute + :top 0 + :left 0 + :right 0 + :bottom 0 + :transform [{:translateX offset-x}]}) diff --git a/src/status_im/components/tabs/tab.cljs b/src/status_im/components/tabs/tab.cljs index e2f56af6b5..9ee44dcc44 100644 --- a/src/status_im/components/tabs/tab.cljs +++ b/src/status_im/components/tabs/tab.cljs @@ -10,9 +10,9 @@ [status-im.components.tabs.styles :as st])) (defview tab [{:keys [view-id title icon selected-view-id]}] - [touchable-highlight {:style st/tab - :onPress #(dispatch [:navigate-to - view-id])} + [touchable-highlight {:style st/tab + :disabled (= view-id selected-view-id) + :onPress #(dispatch [:navigate-to-tab view-id])} [view {:style st/tab-container} [image {:source {:uri icon} :style st/tab-icon}] 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/constants.cljs b/src/status_im/constants.cljs index a684182899..41460b6cc9 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -2,7 +2,7 @@ (def ethereum-rpc-url "http://localhost:8545") -(def server-address "http://rpc0.status.im:20000/") +(def server-address "http://api.status.im/") ;; (def server-address "http://10.0.3.2:3000/") ;; (def server-address "http://localhost:3000/") @@ -10,3 +10,10 @@ (def content-type-command "command") (def content-type-command-request "command-request") (def content-type-status "status") + +(def max-chat-name-length 20) + +(def response-input-hiding-duration 100) +(def response-suggesstion-resize-duration 100) + +(def default-number-of-messages 5) diff --git a/src/status_im/contacts/handlers.cljs b/src/status_im/contacts/handlers.cljs index ac11cf33a2..f61bed64d5 100644 --- a/src/status_im/contacts/handlers.cljs +++ b/src/status_im/contacts/handlers.cljs @@ -1,5 +1,6 @@ (ns status-im.contacts.handlers - (:require [re-frame.core :refer [register-handler after dispatch]] + (:require [re-frame.core :refer [after dispatch]] + [status-im.utils.handlers :refer [register-handler]] [status-im.models.contacts :as contacts] [status-im.utils.crypt :refer [encrypt]] [clojure.string :as s] @@ -96,7 +97,6 @@ (remove #(identities (:whisper-identity %))) (map #(vector (:whisper-identity %) %)) (into {}))] - (println new-contacts') (-> db (update :contacts merge new-contacts') (assoc :new-contacts (vals new-contacts'))))) diff --git a/src/status_im/contacts/screen.cljs b/src/status_im/contacts/screen.cljs index a763c2ea50..97342f9f01 100644 --- a/src/status_im/contacts/screen.cljs +++ b/src/status_im/contacts/screen.cljs @@ -1,17 +1,19 @@ (ns status-im.contacts.screen (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [reagent.core :as r] [status-im.components.react :refer [view text image touchable-highlight + linear-gradient + scroll-view list-view - list-item]] + list-item] :as react] [status-im.components.action-button :refer [action-button action-button-item]] - [status-im.contacts.views.contact :refer [contact-view]] - [status-im.components.styles :refer [toolbar-background2]] + [status-im.contacts.views.contact :refer [contact-extended-view]] [status-im.components.toolbar :refer [toolbar]] - [status-im.components.drawer.view :refer [drawer-view open-drawer]] + [status-im.components.drawer.view :refer [open-drawer]] [status-im.components.icons.ionicons :refer [icon]] [status-im.components.styles :refer [color-blue hamburger-icon @@ -19,41 +21,79 @@ create-icon toolbar-background2]] [status-im.contacts.styles :as st] - [status-im.utils.listview :as lw] [status-im.i18n :refer [label]])) -(defn render-row [row _ _] - (list-item [contact-view row])) - (defn contact-list-toolbar [] - [toolbar {:nav-action {:image {:source {:uri :icon_hamburger} - :style hamburger-icon} - :handler open-drawer} + [toolbar {:nav-action {:image {:source {:uri :icon_hamburger} + :style hamburger-icon} + :handler open-drawer} :title (label :t/contacts) :background-color toolbar-background2 + :style {:elevation 0} :action {:image {:source {:uri :icon_search} :style icon-search} :handler (fn [])}}]) -(defview contact-list [] - [contacts [:get-contacts]] - [drawer-view - [view st/contacts-list-container - [contact-list-toolbar] - ;; 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}]) - [action-button {:buttonColor color-blue - :offsetY 16 - :offsetX 16} - [action-button-item - {:title (label :t/new-contact) - :buttonColor :#9b59b6 - :onPress #(dispatch [:navigate-to :new-contact])} - [icon {:name :android-create - :style create-icon}]] - ]]]) +(def contacts-limit 10) + +(defn contact-group [contacts contacts-count title group top?] + [view st/contact-group + [view st/contact-group-header + (when-not top? + [linear-gradient {:style st/contact-group-header-gradient-top + :colors st/contact-group-header-gradient-top-colors}]) + [view st/contact-group-header-inner + [text {:style st/contact-group-text} title] + [text {:style st/contact-group-size-text} (str contacts-count)]] + [linear-gradient {:style st/contact-group-header-gradient-bottom + :colors st/contact-group-header-gradient-bottom-colors}]] + ;; todo what if there is no contacts, should we show some information + ;; about this? + [view {:flexDirection :column} + (for [contact contacts] + ;; TODO not imlemented: contact more button handler + ^{:key contact} [contact-extended-view contact nil nil])] + (when (= contacts-limit (count contacts)) + [view st/show-all + [touchable-highlight {:on-press #(dispatch [:show-group-contacts group])} + [text {:style st/show-all-text} (label :show-all)]]])]) + +(defn contact-list [] + (let [contacts (subscribe [:get-contacts-with-limit contacts-limit]) + contcats-count (subscribe [:contacts-count]) + show-toolbar-shadow? (r/atom false)] + (fn [] + [view st/contacts-list-container + [contact-list-toolbar] + [view {:style st/toolbar-shadow} + (when @show-toolbar-shadow? + [linear-gradient {:style st/contact-group-header-gradient-bottom + :colors st/contact-group-header-gradient-bottom-colors}])] + (if (pos? @contcats-count) + [scroll-view {:style st/contact-groups + :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] + [contact-group + @contacts + @contcats-count + (label :t/contacs-group-people) + :people false]] + [view st/empty-contact-groups + [react/icon :group_big st/empty-contacts-icon] + [text {:style st/empty-contacts-text} (label :t/no-contacts)]]) + [action-button {:buttonColor color-blue + :offsetY 16 + :offsetX 16} + [action-button-item + {:title (label :t/new-contact) + :buttonColor :#9b59b6 + :onPress #(dispatch [:navigate-to :new-contact])} + [icon {:name :android-create + :style create-icon}]]]]))) diff --git a/src/status_im/contacts/styles.cljs b/src/status_im/contacts/styles.cljs index 4b64148fc9..4da8803d96 100644 --- a/src/status_im/contacts/styles.cljs +++ b/src/status_im/contacts/styles.cljs @@ -1,72 +1,158 @@ (ns status-im.contacts.styles (:require [status-im.components.styles :refer [font + font-medium title-font text1-color + text2-color + text3-color + text5-color color-white toolbar-background2 - online-color]])) - - + online-color + color-gray2]])) (def contacts-list-container {:flex 1 :backgroundColor :white}) +(def toolbar-shadow + {:height 2 + :backgroundColor toolbar-background2}) + +(def contact-groups + {:flex 1 + :backgroundColor toolbar-background2}) + +(def empty-contact-groups + (merge contact-groups + {:align-items :center + :padding-top 150})) + +(def empty-contacts-icon + {:height 62 + :width 62}) + +(def empty-contacts-text + {:margin-top 12 + :font-size 16 + :color color-gray2}) + (def contacts-list {:backgroundColor :white}) -(def contact-photo-container - {:borderRadius 50}) +(def contact-group + {:flexDirection :column}) -(def photo-image - {:borderRadius 50 - :width 40 - :height 40}) +(def contact-group-header + {:flexDirection :column + :backgroundColor toolbar-background2}) -(def online-container - {:position :absolute - :top 24 - :left 24 - :width 20 - :height 20 - :borderRadius 50 - :backgroundColor online-color - :borderWidth 2 - :borderColor color-white}) +(def contact-group-header-inner + {:flexDirection :row + :alignItems :center + :height 48 + :backgroundColor toolbar-background2}) -(def online-dot - {:position :absolute - :top 6 - :width 4 - :height 4 - :borderRadius 50 +(def contact-group-text + {:flex 1 + :marginLeft 16 + :fontSize 14 + :fontFamily font-medium + :color text5-color}) + +(def contact-group-size-text + {:marginRight 14 + :fontSize 12 + :fontFamily font + :color text2-color}) + +(def contact-group-header-gradient-top + {:flexDirection :row + :height 3 + :backgroundColor toolbar-background2}) + +(def contact-group-header-gradient-top-colors + ["rgba(24, 52, 76, 0.165)" + "rgba(24, 52, 76, 0.03)" + "rgba(24, 52, 76, 0.01)"]) + +(def contact-group-header-gradient-bottom + {:flexDirection :row + :height 2 + :backgroundColor toolbar-background2}) + +(def contact-group-header-gradient-bottom-colors + ["rgba(24, 52, 76, 0.01)" + "rgba(24, 52, 76, 0.05)"]) + +(def contact-group-header-height (+ (:height contact-group-header-inner) + (:height contact-group-header-gradient-bottom))) + +(def show-all + {:flexDirection :row + :alignItems :center + :height 56 :backgroundColor color-white}) -(def online-dot-left - (assoc online-dot :left 3)) - -(def online-dot-right - (assoc online-dot :left 9)) +(def show-all-text + {:marginLeft 72 + :fontSize 14 + :fontFamily font-medium + :color text3-color + ;; ios only: + :letterSpacing 0.5}) (def contact-container - {:flexDirection :row - :height 56}) + {:flexDirection :row + :backgroundColor color-white}) -(def photo-container - {:marginTop 8 - :marginLeft 16 - :width 44 - :height 44}) +(def letter-container + {:paddingTop 11 + :paddingLeft 20 + :width 56}) -(def name-container - {:justifyContent :center}) +(def letter-text + {:fontSize 24 + :fontFamily font + :color text3-color}) + +(def contact-photo-container + {:marginTop 4 + :marginLeft 12}) + +(def contact-inner-container + {:flex 1 + :flexDirection :row + :height 56 + :backgroundColor color-white}) + +(def info-container + {:flex 1 + :flexDirection :column + :marginLeft 12 + :justifyContent :center}) (def name-text - {:marginLeft 16 - :fontSize 16 + {:fontSize 16 :fontFamily font :color text1-color}) +(def info-text + {:marginTop 1 + :fontSize 12 + :fontFamily font + :color text2-color}) + +(def more-btn + {:width 56 + :height 56 + :alignItems :center + :justifyContent :center}) + +(def more-btn-icon + {:width 4 + :height 16}) + ; new contact (def contact-form-container @@ -82,4 +168,4 @@ (def form-container {:marginLeft 16 - :margin-top 50}) \ No newline at end of file + :margin-top 50}) diff --git a/src/status_im/contacts/subs.cljs b/src/status_im/contacts/subs.cljs index 76803f74dd..969518e1dd 100644 --- a/src/status_im/contacts/subs.cljs +++ b/src/status_im/contacts/subs.cljs @@ -1,28 +1,60 @@ (ns status-im.contacts.subs (:require-macros [reagent.ratom :refer [reaction]]) - (:require [re-frame.core :refer [register-sub]])) + (:require [re-frame.core :refer [register-sub subscribe]])) (register-sub :get-contacts (fn [db _] (let [contacts (reaction (:contacts @db))] (reaction (vals @contacts))))) +(defn sort-contacts [contacts] + (sort-by :name #(compare (clojure.string/lower-case %1) + (clojure.string/lower-case %2)) (vals contacts))) + (register-sub :all-contacts (fn [db _] (let [contacts (reaction (:contacts @db))] - (reaction (sort-by :name (vals @contacts)))))) + (reaction (sort-contacts @contacts))))) + +(register-sub :get-contacts-with-limit + (fn [_ [_ limit]] + (let [contacts (subscribe [:all-contacts])] + (reaction (take limit @contacts))))) + +(register-sub :contacts-count + (fn [_ _] + (let [contacts (subscribe [:all-contacts])] + (reaction (count @contacts))))) + +(defn get-contact-letter [contact] + (when-let [letter (first (:name contact))] + (clojure.string/upper-case letter))) + +(register-sub :contacts-with-letters + (fn [db _] + (let [contacts (reaction (:contacts @db))] + (reaction + (let [ordered (sort-contacts @contacts)] + (reduce (fn [prev cur] + (let [prev-letter (get-contact-letter (last prev)) + cur-letter (get-contact-letter cur)] + (conj prev + (if (not= prev-letter cur-letter) + (assoc cur :letter cur-letter) + cur)))) + [] ordered)))))) (defn contacts-by-chat [fn db chat-id] (let [chat (reaction (get-in @db [:chats chat-id])) contacts (reaction (:contacts @db))] (reaction - (when @chat - (let [current-participants (->> @chat - :contacts - (map :identity) - set)] - (fn #(current-participants (:whisper-identity %)) - (vals @contacts))))))) + (when @chat + (let [current-participants (->> @chat + :contacts + (map :identity) + set)] + (fn #(current-participants (:whisper-identity %)) + (vals @contacts))))))) (defn contacts-by-current-chat [fn db] (let [current-chat-id (:current-chat-id @db)] @@ -33,6 +65,10 @@ (let [identity (:contact-identity @db)] (reaction (get-in @db [:contacts identity]))))) +(register-sub :contact-by-identity + (fn [db [_ identity]] + (reaction (get-in @db [:contacts identity])))) + (register-sub :all-new-contacts (fn [db _] (contacts-by-current-chat remove db))) @@ -47,8 +83,8 @@ 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)))))))) + (when @chat + (if (:group-chat @chat) + ;; TODO return group chat icon + nil + (:photo-path (first @contacts)))))))) diff --git a/src/status_im/contacts/views/contact.cljs b/src/status_im/contacts/views/contact.cljs index f253789990..b63ea9c31e 100644 --- a/src/status_im/contacts/views/contact.cljs +++ b/src/status_im/contacts/views/contact.cljs @@ -1,16 +1,35 @@ (ns status-im.contacts.views.contact (:require-macros [status-im.utils.views :refer [defview]]) - (:require [status-im.components.react :refer [view touchable-highlight]] + (:require [status-im.components.react :refer [view text icon touchable-highlight]] [re-frame.core :refer [dispatch subscribe]] + [status-im.contacts.styles :as st] [status-im.contacts.views.contact-inner :refer [contact-inner-view]])) +(defn letter-view [letter] + [view st/letter-container + (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]))) -(defview contact-view [{:keys [whisper-identity] :as contact}] +(defview contact-with-letter-view [{:keys [whisper-identity letter] :as contact}] [chat [:get-chat whisper-identity]] [touchable-highlight {:onPress (on-press chat whisper-identity)} - [view {} [contact-inner-view contact]]]) + [view st/contact-container + [letter-view letter] + [contact-inner-view contact]]]) + +(defview contact-extended-view [{:keys [whisper-identity] :as contact} info more-click-handler] + [chat [:get-chat whisper-identity]] + [touchable-highlight + {:onPress (on-press chat whisper-identity)} + [view st/contact-container + [contact-inner-view contact info] + [touchable-highlight + {:on-press more-click-handler} + [view st/more-btn + [icon :more-vertical st/more-btn-icon]]]]]) diff --git a/src/status_im/contacts/views/contact_inner.cljs b/src/status_im/contacts/views/contact_inner.cljs index 05502d793b..4c27d5e1a1 100644 --- a/src/status_im/contacts/views/contact_inner.cljs +++ b/src/status_im/contacts/views/contact_inner.cljs @@ -1,31 +1,26 @@ (ns status-im.contacts.views.contact-inner (:require [clojure.string :as s] [status-im.components.react :refer [view image text]] - [status-im.resources :as res] + [status-im.components.chat-icon.screen :refer [contact-icon-contacts-tab]] [status-im.contacts.styles :as st] [status-im.i18n :refer [label]])) -(defn contact-photo [{:keys [photo-path]}] +(defn contact-photo [contact] [view st/contact-photo-container - [image {:source (if (s/blank? photo-path) - res/user-no-photo - {:uri photo-path}) - :style st/photo-image}]]) + [contact-icon-contacts-tab contact]]) -(defn contact-online [{:keys [online]}] - (when online - [view st/online-container - [view st/online-dot-left] - [view st/online-dot-right]])) - -(defn contact-inner-view [{:keys [name photo-path online]}] - [view st/contact-container - [view st/photo-container - [contact-photo {:photo-path photo-path}] - [contact-online {:online online}]] - [view st/name-container - [text {:style st/name-text} - (if (pos? (count name)) - name - ;; todo is this correct behaviour? - (label :t/no-name))]]]) +(defn contact-inner-view + ([contact] + (contact-inner-view contact nil)) + ([{:keys [name] :as contact} info] + [view st/contact-inner-container + [contact-photo contact] + [view st/info-container + [text {:style st/name-text} + (if (pos? (count (:name contact))) + name + ;; todo is this correct behaviour? + (label :t/no-name))] + (when info + [text {:style st/info-text} + info])]])) diff --git a/src/status_im/contacts/views/contact_list.cljs b/src/status_im/contacts/views/contact_list.cljs new file mode 100644 index 0000000000..9a346e3829 --- /dev/null +++ b/src/status_im/contacts/views/contact_list.cljs @@ -0,0 +1,46 @@ +(ns status-im.contacts.views.contact-list + (:require-macros [status-im.utils.views :refer [defview]]) + (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [status-im.components.react :refer [view text + image + touchable-highlight + list-view + list-item]] + [status-im.contacts.views.contact :refer [contact-with-letter-view]] + [status-im.components.toolbar :refer [toolbar]] + [status-im.components.drawer.view :refer [drawer-view open-drawer]] + [status-im.components.icons.ionicons :refer [icon]] + [status-im.components.styles :refer [color-blue + hamburger-icon + icon-search + create-icon + toolbar-background1]] + [status-im.contacts.styles :as st] + [status-im.utils.listview :as lw] + [status-im.i18n :refer [label]])) + +(defn render-row [row _ _] + (list-item [contact-with-letter-view row])) + +(defview contact-list-toolbar [] + [group [:get :contacts-group]] + [toolbar {:title (label (if (= group :dapps) + :t/contacs-group-dapps + :t/contacs-group-people)) + :background-color toolbar-background1 + :action {:image {:source {:uri :icon_search} + :style icon-search} + :handler (fn [])}}]) + +(defview contact-list [] + [contacts [:contacts-with-letters]] + [drawer-view + [view st/contacts-list-container + [contact-list-toolbar] + ;; 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}])]]) diff --git a/src/status_im/contacts/views/new_contact.cljs b/src/status_im/contacts/views/new_contact.cljs index 469ed6b914..a45de304a6 100644 --- a/src/status_im/contacts/views/new_contact.cljs +++ b/src/status_im/contacts/views/new_contact.cljs @@ -7,8 +7,8 @@ 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 color-white icon-search @@ -56,23 +56,21 @@ (defview new-contact [] [{:keys [name whisper-identity phone-number] :as new-contact} [:get :new-contact]] - [drawer-view - [view st/contact-form-container - [linear-gradient {:colors ["rgba(182, 116, 241, 1)" "rgba(107, 147, 231, 1)" "rgba(43, 171, 238, 1)"] - :start [0, 0] - :end [0.5, 1] - :locations [0, 0.8 ,1] - :style st/gradient-background}] + [view st/contact-form-container + [linear-gradient {:colors ["rgba(182, 116, 241, 1)" "rgba(107, 147, 231, 1)" "rgba(43, 171, 238, 1)"] + :start [0, 0] + :end [0.5, 1] + :locations [0, 0.8, 1] + :style st/gradient-background}] - [toolbar {:background-color :transparent - :nav-action {:image {:source {:uri :icon_back_white} - :style icon-back} - :handler #(dispatch [:navigate-back])} - :custom-content toolbar-title - :action {:image {:source {:uri :icon_add} - :style icon-search} - :handler #(dispatch [:add-new-contact new-contact])}}] - [view st/form-container - [contact-whisper-id-input whisper-identity] - [contact-name-input name] - ]]]) + [toolbar {:background-color :transparent + :nav-action {:image {:source {:uri :icon_back_white} + :style icon-back} + :handler #(dispatch [:navigate-back])} + :custom-content toolbar-title + :action {:image {:source {:uri :icon_add} + :style icon-search} + :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 283a09a1a9..08f3e8abf0 100644 --- a/src/status_im/db.cljs +++ b/src/status_im/db.cljs @@ -1,5 +1,7 @@ (ns status-im.db - (:require [schema.core :as s :include-macros true])) + (:require [schema.core :as s :include-macros true] + [status-im.components.react :refer [animated]] + [status-im.components.animation :as anim])) ;; schema of app-db (def schema {:greeting s/Str}) @@ -34,7 +36,10 @@ :address "" :whisper-identity "" :phone-number ""} - :disable-group-creation false}) + :disable-group-creation false + :animations {:to-response-height 0.1 + ;; todo clear this + :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/discovery/handlers.cljs b/src/status_im/discovery/handlers.cljs index 2b227acefa..2f062260bc 100644 --- a/src/status_im/discovery/handlers.cljs +++ b/src/status_im/discovery/handlers.cljs @@ -1,5 +1,6 @@ (ns status-im.discovery.handlers - (:require [re-frame.core :refer [register-handler after dispatch enrich]] + (:require [re-frame.core :refer [after dispatch enrich]] + [status-im.utils.handlers :refer [register-handler]] [status-im.protocol.api :as api] [status-im.navigation.handlers :as nav] [status-im.discovery.model :as discoveries] diff --git a/src/status_im/discovery/styles.cljs b/src/status_im/discovery/styles.cljs index dd998b352d..540dc04a03 100644 --- a/src/status_im/discovery/styles.cljs +++ b/src/status_im/discovery/styles.cljs @@ -2,6 +2,7 @@ (:require [status-im.components.styles :refer [font title-font color-white + color-gray2 chat-background online-color selected-message-color @@ -49,7 +50,7 @@ :elevation 0}) (def discovery-subtitle - {:color "#8f838c93" + {:color color-gray2 :fontFamily "sans-serif-medium" :fontSize 14}) diff --git a/src/status_im/group_settings/handlers.cljs b/src/status_im/group_settings/handlers.cljs index 82eab18d7b..6f5e94d558 100644 --- a/src/status_im/group_settings/handlers.cljs +++ b/src/status_im/group_settings/handlers.cljs @@ -1,6 +1,6 @@ (ns status-im.group-settings.handlers - (:require [re-frame.core :refer [register-handler debug dispatch after - enrich]] + (:require [re-frame.core :refer [debug dispatch after enrich]] + [status-im.utils.handlers :refer [register-handler]] [status-im.persistence.realm :as r] [status-im.chat.handlers :refer [delete-messages!]] [status-im.protocol.api :as api] diff --git a/src/status_im/group_settings/screen.cljs b/src/status_im/group_settings/screen.cljs index 346b783912..a35bdbcf53 100644 --- a/src/status_im/group_settings/screen.cljs +++ b/src/status_im/group_settings/screen.cljs @@ -146,7 +146,9 @@ (defview chat-name [] [name [:chat :name] new-name [:get :new-chat-name] - focused? [:get ::name-input-focused]] + validation-messages [:new-chat-name-validation-messages] + focused? [:get ::name-input-focused] + valid? [:new-chat-name-valid?]] [view [text {:style st/chat-name-text} (label :t/chat-name)] [view (st/chat-name-value-container focused?) @@ -157,12 +159,14 @@ :on-blur blur} name] (if (or focused? (not= name new-name)) - [touchable-highlight {:style st/chat-name-btn-edit-container + [touchable-highlight {:style (st/chat-name-btn-edit-container valid?) :on-press save} [view [icon :ok-purple st/add-members-icon]]] - [touchable-highlight {:style st/chat-name-btn-edit-container + [touchable-highlight {:style (st/chat-name-btn-edit-container true) :on-press focus} - [text {:style st/chat-name-btn-edit-text} (label :t/edit)]])]]) + [text {:style st/chat-name-btn-edit-text} (label :t/edit)]])] + (when (pos? (count validation-messages)) + [text {:style st/chat-name-validation-message} (first validation-messages)])]) (defview group-settings [] [show-color-picker [:group-settings :show-color-picker]] diff --git a/src/status_im/group_settings/styles/group_settings.cljs b/src/status_im/group_settings/styles/group_settings.cljs index 91d407c604..9950bb2f9c 100644 --- a/src/status_im/group_settings/styles/group_settings.cljs +++ b/src/status_im/group_settings/styles/group_settings.cljs @@ -91,9 +91,15 @@ :fontFamily font :color text1-color}) -(def chat-name-btn-edit-container +(def chat-name-validation-message + {:marginTop 8 + :marginLeft 16 + :color :red}) + +(defn chat-name-btn-edit-container [enabled?] {:padding 16 - :justifyContent :center}) + :justifyContent :center + :opacity (if enabled? 1 0.3)}) (def chat-name-btn-edit-text {:marginTop -1 diff --git a/src/status_im/group_settings/styles/member.cljs b/src/status_im/group_settings/styles/member.cljs deleted file mode 100644 index e5ef3f9f58..0000000000 --- a/src/status_im/group_settings/styles/member.cljs +++ /dev/null @@ -1,78 +0,0 @@ -(ns status-im.group-settings.styles.member - (:require [status-im.components.styles :refer [font - title-font - text1-color - text2-color - color-white - online-color]])) - -(def contact-photo-container - {:borderRadius 50}) - -(def 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 contact-container - {:flexDirection :row - :height 56}) - -(def photo-container - {:marginTop 8 - :marginLeft 16 - :width 44 - :height 44}) - -(def info-container - {:flex 1 - :flexDirection :column - :marginLeft 16 - :justifyContent :center}) - -(def name-text - {:marginTop -2 - :fontSize 16 - :fontFamily font - :color text1-color}) - -(def role-text - {:marginTop 1 - :fontSize 12 - :fontFamily font - :color text2-color}) - -(def more-btn - {:width 56 - :height 56 - :alignItems :center - :justifyContent :center }) - -(def more-btn-icon - {:width 4 - :height 16}) diff --git a/src/status_im/group_settings/subs.cljs b/src/status_im/group_settings/subs.cljs index f2b6ac1db3..c1bb17da4d 100644 --- a/src/status_im/group_settings/subs.cljs +++ b/src/status_im/group_settings/subs.cljs @@ -1,6 +1,7 @@ (ns status-im.group-settings.subs (:require-macros [reagent.ratom :refer [reaction]]) - (:require [re-frame.core :refer [register-sub]])) + (:require [re-frame.core :refer [register-sub]] + [status-im.constants :refer [max-chat-name-length]])) (register-sub :selected-participant (fn [db _] @@ -11,3 +12,20 @@ (register-sub :group-settings (fn [db [_ k]] (reaction (get-in @db [:group-settings k])))) + +(defn get-chat-name-validation-messages [chat-name] + (filter some? + (list (when (zero? (count chat-name)) + "Chat name can't be empty") + (when (< max-chat-name-length (count chat-name)) + "Chat name is too long")))) + +(register-sub :new-chat-name-validation-messages + (fn [db [_]] + (let [chat-name (reaction (:new-chat-name @db))] + (reaction (get-chat-name-validation-messages @chat-name))))) + +(register-sub :new-chat-name-valid? + (fn [db [_]] + (let [chat-name (reaction (:new-chat-name @db))] + (reaction (zero? (count (get-chat-name-validation-messages @chat-name))))))) diff --git a/src/status_im/group_settings/views/member.cljs b/src/status_im/group_settings/views/member.cljs index c5292daa36..e73d46cc2a 100644 --- a/src/status_im/group_settings/views/member.cljs +++ b/src/status_im/group_settings/views/member.cljs @@ -1,44 +1,9 @@ (ns status-im.group-settings.views.member - (:require [clojure.string :as s] - [re-frame.core :refer [subscribe dispatch dispatch-sync]] - [status-im.components.react :refer [view - image - text - icon - touchable-highlight]] - [status-im.resources :as res] - [status-im.group-settings.styles.member :as st] + (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [status-im.contacts.views.contact :refer [contact-extended-view]] [status-im.i18n :refer [label]])) -(defn contact-photo [{:keys [photo-path]}] - [view st/contact-photo-container - [image {:source (if (s/blank? photo-path) - res/user-no-photo - {:uri photo-path}) - :style st/photo-image}]]) - -(defn contact-online [{:keys [online]}] - (when online - [view st/online-container - [view st/online-dot-left] - [view st/online-dot-right]])) - -(defn member-view [{:keys [whisper-identity name photo-path online role]}] - [view st/contact-container - [view st/photo-container - [contact-photo {:photo-path photo-path}] - [contact-online {:online online}]] - [view st/info-container - [text {:style st/name-text} - (if (pos? (count name)) - name - ;; todo is this correct behaviour? - (label :t/no-name))] - ;; TODO implement :role property for group chat contact - (when role - [text {:style st/role-text} - role])] - [touchable-highlight - {:on-press #(dispatch [:set :selected-participants #{whisper-identity}])} - [view st/more-btn - [icon :more-vertical st/more-btn-icon]]]]) +(defn member-view [{:keys [whisper-identity role] :as contact}] + ;; TODO implement :role property for group chat contact + [contact-extended-view contact role + #(dispatch [:set :selected-participants #{whisper-identity}])]) diff --git a/src/status_im/handlers.cljs b/src/status_im/handlers.cljs index 5d43f80031..1c656f5514 100644 --- a/src/status_im/handlers.cljs +++ b/src/status_im/handlers.cljs @@ -1,24 +1,26 @@ (ns status-im.handlers (:require - [re-frame.core :refer [register-handler after dispatch debug]] + [re-frame.core :refer [after dispatch debug]] [schema.core :as s :include-macros true] [status-im.db :refer [app-db schema]] [status-im.persistence.simple-kv-store :as kv] [status-im.protocol.state.storage :as storage] - [status-im.models.commands :refer [set-commands]] - [status-im.chat.suggestions :refer [load-commands]] [status-im.utils.logging :as log] [status-im.utils.crypt :refer [gen-random-bytes]] - [status-im.utils.handlers :as u] + [status-im.utils.handlers :refer [register-handler] :as u] status-im.chat.handlers + status-im.chat.handlers.animation status-im.group-settings.handlers status-im.navigation.handlers status-im.contacts.handlers status-im.discovery.handlers status-im.new-group.handlers status-im.participants.handlers + status-im.commands.handlers.loading + status-im.commands.handlers.jail status-im.qr-scanner.handlers - status-im.protocol.handlers)) + status-im.protocol.handlers + status-im.chat.handlers.requests)) ;; -- Middleware ------------------------------------------------------------ ;; @@ -39,16 +41,16 @@ (defn set-el [db [_ k v]] (assoc db k v)) -(register-handler :set - debug - set-el) +(register-handler :set set-el) (defn set-in [db [_ path v]] (assoc-in db path v)) -(register-handler :set-in - debug - set-in) +(register-handler :set-in set-in) + +(register-handler :set-animation + (fn [db [_ k v]] + (assoc-in db [:animations k] v))) (register-handler :initialize-db (fn [_ _] @@ -76,17 +78,6 @@ (fn [_ _] (log/debug "crypt initialized")))) -(register-handler :load-commands - (u/side-effect! - (fn [_ [action]] - (log/debug action) - (load-commands)))) - -(register-handler :set-commands - (fn [db [action commands]] - (log/debug action commands) - (set-commands db commands))) - ;; -- User data -------------------------------------------------------------- (register-handler :load-user-phone-number (fn [db [_]] diff --git a/src/status_im/handlers/content_suggestions.cljs b/src/status_im/handlers/content_suggestions.cljs index f4457f931f..c3928b3303 100644 --- a/src/status_im/handlers/content_suggestions.cljs +++ b/src/status_im/handlers/content_suggestions.cljs @@ -4,19 +4,22 @@ [status-im.utils.logging :as log] [clojure.string :as s])) +;; TODO stub data? (def suggestions - {:phone [{:value "89171111111" + {:phone [{:header "Phone number formats"} + {:value "89171111111" :description "Number format 1"} - {:value "+79171111111" + {:value "+79171111111" :description "Number format 2"} - {:value "9171111111" + {:value "9171111111" :description "Number format 3"}]}) -(defn get-content-suggestions [db command text] +(defn get-content-suggestions [command text] (or (when command - (when-let [command-suggestions ((:command command) suggestions)] + (when-let [command-suggestions ((keyword (:name command)) suggestions)] (filterv (fn [s] - (and (.startsWith (:value s) (or text "")) - (not= (:value s) text))) + (or (:header s) + (and (.startsWith (:value s) (or text "")) + (not= (:value s) text)))) command-suggestions))) [])) diff --git a/src/status_im/models/commands.cljs b/src/status_im/models/commands.cljs index a9ba2ffd26..1ebb70dccb 100644 --- a/src/status_im/models/commands.cljs +++ b/src/status_im/models/commands.cljs @@ -3,75 +3,18 @@ [clojure.walk :refer [stringify-keys keywordize-keys]] [re-frame.core :refer [subscribe dispatch]] [status-im.db :as db] + [status-im.components.animation :as anim] [status-im.components.styles :refer [color-blue color-dark-mint]] [status-im.i18n :refer [label]])) -;; todo delete -(def commands [{:command :money - :text "!money" - :description (label :t/money-command-description) - :color color-dark-mint - :request-icon {:uri "icon_lock_white"} - :icon {:uri "icon_lock_gray"} - :suggestion true} - {:command :location - :text "!location" - :description (label :t/location-command-description) - :color "#9a5dcf" - :suggestion true} - {:command :phone - :text "!phone" - :description (label :t/phone-command-description) - :color color-dark-mint - :request-text (label :t/phone-request-text) - :suggestion true - :handler #(dispatch [:sign-up %])} - {:command :confirmation-code - :text "!confirmationCode" - :description (label :t/confirmation-code-command-description) - :request-text (label :t/confirmation-code-request-text) - :color color-blue - :request-icon {:uri "icon_lock_white"} - :icon {:uri "icon_lock_gray"} - :suggestion true - :handler #(dispatch [:sign-up-confirm %])} - {:command :send - :text "!send" - :description (label :t/send-command-description) - :color "#9a5dcf" - :suggestion true} - {:command :request - :text "!request" - :description (label :t/request-command-description) - :color "#48ba30" - :suggestion true} - {:command :keypair-password - :text "!keypair-password" - :description (label :t/keypair-password-command-description) - :color color-blue - :request-icon {:uri "icon_lock_white"} - :icon {:uri "icon_lock_gray"} - :suggestion false - :handler #(dispatch [:save-password %])} - {:command :help - :text "!help" - :description (label :t/help-command-description) - :color "#9a5dcf" - :suggestion true}]) +(defn get-commands [{:keys [current-chat-id] :as db}] + (or (get-in db [:chats current-chat-id :commands]) {})) -(defn get-commands [db] - ;; todo: temp. must be '(get db :commands)' - ;; (get db :commands) - commands) - -(defn set-commands [db commands] - (assoc db :commands commands)) - -;; todo delete -(def suggestions (filterv :suggestion commands)) - -(defn get-command [db command-key] - (first (filter #(= command-key (:command %)) (get-commands db)))) +(defn get-command [{:keys [current-chat-id] :as db} command-key] + ((or (->> (get-in db [:chats current-chat-id]) + ((juxt :commands :responses)) + (apply merge)) + {}) command-key)) (defn find-command [commands command-key] (first (filter #(= command-key (:command %)) commands))) @@ -91,9 +34,10 @@ (defn set-response-chat-command [{:keys [current-chat-id] :as db} msg-id command-key] (update-in db [:chats current-chat-id :command-input] merge - {:content nil - :command (get-command db command-key) - :to-msg-id msg-id})) + {:content nil + :command (get-command db command-key) + :parameter-idx 0 + :to-msg-id msg-id})) (defn set-chat-command [db command-key] (set-response-chat-command db nil command-key)) @@ -129,7 +73,7 @@ #(assoc % msg-id handler))) (defn parse-command-msg-content [commands content] - (update content :command #(find-command commands (keyword %)))) + (update content :command #((keyword %) commands))) (defn parse-command-request [commands content] - (update content :command #(find-command commands (keyword %)))) + (update content :command #((keyword %) commands))) 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/models/messages.cljs b/src/status_im/models/messages.cljs index 5c8c17fa5f..38d7342ae3 100644 --- a/src/status_im/models/messages.cljs +++ b/src/status_im/models/messages.cljs @@ -3,11 +3,11 @@ [re-frame.core :refer [dispatch]] [cljs.reader :refer [read-string]] [status-im.utils.random :refer [timestamp]] - [status-im.db :as db] [status-im.utils.logging :as log] [clojure.string :refer [join split]] [clojure.walk :refer [stringify-keys keywordize-keys]] - [status-im.constants :as c])) + [status-im.constants :as c] + [status-im.commands.utils :refer [generate-hiccup]])) (defn- map-to-str [m] @@ -21,7 +21,8 @@ {:outgoing false :to nil :same-author false - :same-direction false}) + :same-direction false + :preview nil}) (defn save-message ;; todo remove chat-id parameter @@ -46,15 +47,19 @@ #{c/content-type-command c/content-type-command-request} type)) -(defn get-messages [chat-id] - (->> (-> (r/get-by-field :msgs :chat-id chat-id) - (r/sorted :timestamp :asc) - (r/collection->map)) - (into '()) - (map (fn [{:keys [content-type] :as message}] - (if (command-type? content-type) - (update message :content str-to-map) - message))))) +(defn get-messages + ([chat-id] (get-messages chat-id 0)) + ([chat-id from] + (->> (-> (r/get-by-field :msgs :chat-id chat-id) + (r/sorted :timestamp :desc) + (r/page from (+ from c/default-number-of-messages)) + (r/collection->map)) + (into '()) + reverse + (keep (fn [{:keys [content-type] :as message}] + (if (command-type? content-type) + (update message :content str-to-map) + message)))))) (defn update-message! [{:keys [msg-id] :as msg}] (log/debug "update-message!" msg) diff --git a/src/status_im/navigation/handlers.cljs b/src/status_im/navigation/handlers.cljs index c47134e429..db33411902 100644 --- a/src/status_im/navigation/handlers.cljs +++ b/src/status_im/navigation/handlers.cljs @@ -1,6 +1,6 @@ (ns status-im.navigation.handlers - (:require [re-frame.core :refer [register-handler dispatch debug enrich - after]])) + (:require [re-frame.core :refer [dispatch debug enrich after]] + [status-im.utils.handlers :refer [register-handler]])) (defn push-view [db view-id] (-> db @@ -43,17 +43,36 @@ (assoc :view-id view-id) (assoc :navigation-stack navigation-stack')))))) +(register-handler :navigate-to-tab + (enrich preload-data!) + (fn [db [_ view-id]] + (-> db + (assoc :prev-tab-view-id (:view-id db)) + (replace-view view-id)))) + +(register-handler :on-navigated-to-tab + (enrich preload-data!) + (fn [db [_]] + (assoc db :prev-tab-view-id nil))) + (register-handler :show-group-new (debug (fn [db _] (-> db (push-view :new-group) - (assoc :new-group #{}))))) + (assoc :new-group #{}) + (assoc :new-chat-name nil))))) (register-handler :show-contacts (fn [db _] (push-view db :contact-list))) +(register-handler :show-group-contacts + (fn [db [_ group]] + (-> db + (assoc :contacts-group group) + (push-view :group-contacts)))) + (defn show-profile [db [_ identity]] (-> db diff --git a/src/status_im/new_group/handlers.cljs b/src/status_im/new_group/handlers.cljs index 0c34111405..3cba0c3db0 100644 --- a/src/status_im/new_group/handlers.cljs +++ b/src/status_im/new_group/handlers.cljs @@ -1,6 +1,7 @@ (ns status-im.new-group.handlers (:require [status-im.protocol.api :as api] - [re-frame.core :refer [register-handler after dispatch debug enrich]] + [re-frame.core :refer [after dispatch debug enrich]] + [status-im.utils.handlers :refer [register-handler]] [status-im.components.styles :refer [default-chat-color]] [status-im.models.chats :as chats] [clojure.string :as s])) diff --git a/src/status_im/new_group/screen.cljs b/src/status_im/new_group/screen.cljs index 4b266486c6..9e81f77b89 100644 --- a/src/status_im/new_group/screen.cljs +++ b/src/status_im/new_group/screen.cljs @@ -3,13 +3,13 @@ (:require [re-frame.core :refer [subscribe dispatch]] [status-im.resources :as res] [status-im.components.react :refer [view - text-input - text - image - icon - touchable-highlight - list-view - list-item]] + text-input + text + image + icon + touchable-highlight + list-view + list-item]] [status-im.components.styles :refer [color-purple]] [status-im.components.toolbar :refer [toolbar]] [status-im.utils.listview :refer [to-datasource]] @@ -19,24 +19,30 @@ (defview new-group-toolbar [] - [group-name [:get ::group-name] - creation-disabled? [:get :disable-group-creation]] - [toolbar - {:title (label :t/new-group-chat) - :action {:image {:source res/v ;; {:uri "icon_search"} - :style st/toolbar-icon} - :handler (when-not creation-disabled? - #(dispatch [:init-group-creation group-name]))}}]) + [group-name [:get :new-chat-name] + creation-disabled? [:get :disable-group-creation] + valid? [:new-chat-name-valid?]] + (let [create-btn-enabled? (and valid? (not creation-disabled?))] + [toolbar + {:title (label :t/new-group-chat) + :action {:image {:source res/v ;; {:uri "icon_search"} + :style (st/toolbar-icon create-btn-enabled?)} + :handler (when create-btn-enabled? + #(dispatch [:init-group-creation group-name]))}}])) (defview group-name-input [] - [group-name [:get ::group-name]] - [text-input - {:underlineColorAndroid color-purple - :style st/group-name-input - :autoFocus true - :placeholder (label :t/group-name) - :onChangeText #(dispatch [:set ::group-name %])} - group-name]) + [group-name [:get :new-chat-name] + validation-messages [:new-chat-name-validation-messages]] + [view + [text-input + {:underlineColorAndroid color-purple + :style st/group-name-input + :autoFocus true + :placeholder (label :t/group-name) + :onChangeText #(dispatch [:set :new-chat-name %])} + group-name] + (when (pos? (count validation-messages)) + [text {:style st/group-name-validation-message} (first validation-messages)])]) (defview new-group [] [contacts [:all-contacts]] diff --git a/src/status_im/new_group/styles.cljs b/src/status_im/new_group/styles.cljs index cd16b8dcc8..3d15673453 100644 --- a/src/status_im/new_group/styles.cljs +++ b/src/status_im/new_group/styles.cljs @@ -7,9 +7,10 @@ text2-color toolbar-background1]])) -(def toolbar-icon - {:width 20 - :height 18}) +(defn toolbar-icon [enabled?] + {:width 20 + :height 18 + :opacity (if enabled? 1 0.3)}) (def new-group-container {:flex 1 @@ -33,6 +34,9 @@ :fontFamily font :color text1-color}) +(def group-name-validation-message + {:color :red}) + (def members-text {:marginTop 24 :marginBottom 16 diff --git a/src/status_im/participants/handlers.cljs b/src/status_im/participants/handlers.cljs index fe0e5581c0..a6fb532355 100644 --- a/src/status_im/participants/handlers.cljs +++ b/src/status_im/participants/handlers.cljs @@ -1,6 +1,7 @@ (ns status-im.participants.handlers (:require [status-im.navigation.handlers :as nav] - [re-frame.core :refer [register-handler debug]])) + [re-frame.core :refer [debug]] + [status-im.utils.handlers :refer [register-handler]])) (defmethod nav/preload-data! :add-participants [db _] diff --git a/src/status_im/persistence/realm.cljs b/src/status_im/persistence/realm.cljs index 5d304828fe..e0ee29a606 100644 --- a/src/status_im/persistence/realm.cljs +++ b/src/status_im/persistence/realm.cljs @@ -1,9 +1,9 @@ (ns status-im.persistence.realm (:require [cljs.reader :refer [read-string]] [status-im.components.styles :refer [default-chat-color]] - [status-im.utils.logging :as log] [status-im.utils.types :refer [to-string]] - [status-im.utils.utils :as u]) + [status-im.utils.utils :as u] + [clojure.string :as str]) (:refer-clojure :exclude [exists?])) (def opts {:schema [{:name :contacts @@ -15,6 +15,13 @@ :optional true} :photo-path {:type "string" :optinal true}}} + {:name :requests + :properties {:message-id :string + :chat-id :string + :type :string + :status {:type :string + :default "open"} + :added :date}} {:name :kv-store :primaryKey :key :properties {:key "string" @@ -34,7 +41,9 @@ :delivery-status {:type "string" :optional true} :same-author "bool" - :same-direction "bool"}} + :same-direction "bool" + :preview {:type :string + :optional true}}} {:name :chat-contact :properties {:identity "string" :is-in-chat {:type "bool" @@ -51,7 +60,15 @@ :timestamp "int" :contacts {:type "list" :objectType "chat-contact"} + :dapp-url {:type :string + :optional true} + :dapp-hash {:type :int + :optional true} :last-msg-id "string"}} + {:name :commands + :primaryKey :chat-id + :properties {:chat-id "string" + :file "string"}} {:name :tag :primaryKey :name :properties {:name "string" @@ -94,12 +111,20 @@ ([schema-name obj update?] (.create realm (to-string schema-name) (clj->js obj) update?))) +(defn create-object + [schema-name obj] + (write (fn [] (create schema-name obj true)))) + +(defn and-q [queries] + (str/join " and " queries)) + (defmulti to-query (fn [schema-name operator field value] operator)) (defmethod to-query :eq [schema-name operator field value] (let [value (to-string value) - query (str (name field) "=" (if (= "string" (field-type schema-name field)) + query (str (name field) "=" (if (= "string" (name (field-type + schema-name field))) (str "\"" value "\"") value))] query)) @@ -112,6 +137,12 @@ (let [q (to-query schema-name :eq field value)] (.filtered (.objects realm (name schema-name)) q))) +(defn get-by-fields [schema-name fields] + (let [queries (map (fn [[k v]] + (to-query schema-name :eq k v)) + fields)] + (.filtered (.objects realm (name schema-name)) (and-q queries)))) + (defn get-all [schema-name] (.objects realm (to-string schema-name))) @@ -158,3 +189,6 @@ (defn collection->map [collection] (-> (.map collection (fn [object _ _] object)) (js->clj :keywordize-keys true))) + +(defn get-one-by-field [schema-name field value] + (single-cljs (get-by-field schema-name field value))) diff --git a/src/status_im/protocol/handlers.cljs b/src/status_im/protocol/handlers.cljs index a9b94e5541..e6e8c8699c 100644 --- a/src/status_im/protocol/handlers.cljs +++ b/src/status_im/protocol/handlers.cljs @@ -4,7 +4,8 @@ (:require [status-im.utils.handlers :as u] [status-im.utils.logging :as log] [status-im.protocol.api :as api] - [re-frame.core :refer [register-handler dispatch debug]] + [re-frame.core :refer [dispatch debug]] + [status-im.utils.handlers :refer [register-handler]] [status-im.models.contacts :as contacts] [status-im.protocol.api :refer [init-protocol]] [status-im.protocol.protocol-handler :refer [make-handler]] diff --git a/src/status_im/qr_scanner/handlers.cljs b/src/status_im/qr_scanner/handlers.cljs index be05d7fb65..8827ea847a 100644 --- a/src/status_im/qr_scanner/handlers.cljs +++ b/src/status_im/qr_scanner/handlers.cljs @@ -1,5 +1,6 @@ (ns status-im.qr-scanner.handlers - (:require [re-frame.core :refer [register-handler after dispatch debug enrich]] + (:require [re-frame.core :refer [after dispatch debug enrich]] + [status-im.utils.handlers :refer [register-handler]] [status-im.navigation.handlers :as nav] [status-im.utils.handlers :as u])) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 972c57887c..f31dc4ebe2 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -15,3 +15,7 @@ (register-sub :get-in (fn [db [_ path]] (reaction (get-in @db path)))) + +(register-sub :animations + (fn [db [_ k]] + (reaction (get-in @db [:animations k])))) diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index 37fac291a0..792dc4ebd5 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -72,6 +72,10 @@ :contacts "Contacts" :no-name "Noname" :new-contact "New Contact" + :show-all "SHOW ALL" + :contacs-group-dapps "Dapps" + :contacs-group-people "People" + :no-contacts "No contacts yet" ;group-settings :remove "Remove" @@ -92,7 +96,7 @@ :location-command-description "Send location" :phone-command-description "Send phone number" :phone-request-text "Phone number request" - :confirmation-code-coomand-description "Send confirmation code" + :confirmation-code-command-description "Send confirmation code" :confirmation-code-request-text "Confirmation code request" :send-command-description "Send location" :request-command-description "Send request" @@ -127,6 +131,6 @@ :login "Login" ;users - :add-account "Add account" + :add-account "Add account" - }) \ No newline at end of file + }) diff --git a/src/status_im/utils/crypt.cljs b/src/status_im/utils/crypt.cljs index 0b3f2dd7d0..9179e7dfc0 100644 --- a/src/status_im/utils/crypt.cljs +++ b/src/status_im/utils/crypt.cljs @@ -20,7 +20,7 @@ (byteArrayToHex (.digest sha-256))) (defn gen-random-bytes [length cb] - #_(.randomBytes random-bytes length (fn [& [err buf]] + (.randomBytes random-bytes length (fn [& [err buf]] (if err (cb {:error err}) (cb {:buffer buf}))))) diff --git a/src/status_im/utils/handlers.cljs b/src/status_im/utils/handlers.cljs index f14ce02a79..083196375e 100644 --- a/src/status_im/utils/handlers.cljs +++ b/src/status_im/utils/handlers.cljs @@ -1,4 +1,5 @@ -(ns status-im.utils.handlers) +(ns status-im.utils.handlers + (:require [re-frame.core :refer [after dispatch debug] :as re-core])) (defn side-effect! "Middleware for handlers that will not affect db." @@ -6,3 +7,8 @@ (fn [db params] (handler db params) db)) + +(defn register-handler + ([name handler] (register-handler name nil handler)) + ([name middleware handler] + (re-core/register-handler name [#_debug middleware] handler))) 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))))) + diff --git a/src/status_im/utils/listview.cljs b/src/status_im/utils/listview.cljs index dcd63da8bd..d9ab791710 100644 --- a/src/status_im/utils/listview.cljs +++ b/src/status_im/utils/listview.cljs @@ -7,3 +7,12 @@ (defn to-datasource [items] (clone-with-rows (data-source {:rowHasChanged not=}) items)) + +(defn clone-with-rows-inverted [ds rows] + (let [rows (reduce (fn [ac el] (.push ac el) ac) + (clj->js []) (reverse rows)) + row-ids (.reverse (.map rows (fn [_ index] index)))] + (.cloneWithRows ds rows row-ids))) + +(defn to-datasource-inverted [items] + (clone-with-rows-inverted (data-source {:rowHasChanged not=}) items)) diff --git a/src/status_im/utils/slurp.clj b/src/status_im/utils/slurp.clj new file mode 100644 index 0000000000..194b1384b5 --- /dev/null +++ b/src/status_im/utils/slurp.clj @@ -0,0 +1,5 @@ +(ns status-im.utils.slurp + (:refer-clojure :exclude [slurp])) + +(defmacro slurp [file] + (clojure.core/slurp file)) diff --git a/src/status_im/utils/types.cljs b/src/status_im/utils/types.cljs index 93c942df77..d30d09804b 100644 --- a/src/status_im/utils/types.cljs +++ b/src/status_im/utils/types.cljs @@ -11,5 +11,6 @@ (defn clj->json [data] (.stringify js/JSON (clj->js data))) -(defn json->clj [data] - (js->clj (.parse js/JSON data) :keywordize-keys true)) +(defn json->clj [json] + (when-not (= json "undefined") + (js->clj (.parse js/JSON json) :keywordize-keys true))) diff --git a/src/status_im/utils/utils.cljs b/src/status_im/utils/utils.cljs index c20a6c1e2a..b317dd0301 100644 --- a/src/status_im/utils/utils.cljs +++ b/src/status_im/utils/utils.cljs @@ -41,10 +41,8 @@ (toast (str error)))))))) (defn http-get - ([action on-success on-error] - (-> (.fetch js/window - (str const/server-address action) - (clj->js {:method "GET"})) + ([url on-success on-error] + (-> (.fetch js/window url (clj->js {:method "GET"})) (.then (fn [response] (log response) (.text response))) diff --git a/test/cljs/status_im/test/commands/handlers.cljs b/test/cljs/status_im/test/commands/handlers.cljs new file mode 100644 index 0000000000..cf356be4e2 --- /dev/null +++ b/test/cljs/status_im/test/commands/handlers.cljs @@ -0,0 +1,20 @@ +(ns status-im.test.commands.handlers + (:require [cljs.test :refer-macros [deftest is]] + [status-im.commands.handlers.loading :as h])) + +(deftest test-validate-hash + (let [file "some-js" + db (-> {} + (assoc-in [:chats :user :dapp-hash] -731917028) + (assoc-in [:chats :user2 :dapp-hash] 123))] + (is (::h/valid-hash (h/validate-hash db [:user file]))) + (is (not (::h/valid-hash (h/validate-hash db [:user2 file])))) + (is (not (::h/valid-hash (h/validate-hash db [:user3 file])))))) + +(deftest test-add-commands + (let [obj {:commands {:test {:name "name" + :description "desc"}} + :responses {:test-r {:name "r" + :description "desc-r"}}} + db (h/add-commands {} [:user nil obj])] + (is (= obj (select-keys (get-in db [:chats :user]) [:commands :responses]))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 14bda7de00..fef2b15ecf 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -1,5 +1,7 @@ (ns status-im.test.runner (:require [doo.runner :refer-macros [doo-tests]] - [status-im.test.handlers])) + [status-im.test.handlers] + [status-im.test.commands.handlers])) -(doo-tests 'status-im.test.handlers) +(doo-tests 'status-im.test.handlers + 'status-im.test.commands.handlers)