diff --git a/.circleci/config.yml b/.circleci/config.yml index 206b41d8d..7d612de47 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -197,7 +197,8 @@ jobs: npx jetify cd android if [[ $KEYSTORE ]]; then - ./gradlew bundleRelease + # TODO: enable app bundle again + ./gradlew assembleRelease else ./gradlew assembleDebug fi diff --git a/README.md b/README.md index 0fa994229..f399817d3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Rocket.Chat React Native Mobile +# Rocket.Chat Mobile [![Project Dependencies](https://david-dm.org/RocketChat/Rocket.Chat.ReactNative.svg)](https://david-dm.org/RocketChat/Rocket.Chat.ReactNative) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/bb15e2392a71473ea59d3f634f35c54e)](https://www.codacy.com/app/RocketChat/Rocket.Chat.ReactNative?utm_source=github.com&utm_medium=referral&utm_content=RocketChat/Rocket.Chat.ReactNative&utm_campaign=badger) @@ -8,6 +8,16 @@ **Supported Server Versions:** 0.70.0+ ## Download + +### Official apps + + Download on Google Play + + + Download on App Store + + +### Experimental apps Download on Google Play @@ -19,11 +29,17 @@ ### TestFlight -You can signup to our TestFlight builds by acessing this link: https://testflight.apple.com/join/7I3dLCNT. +You can signup to our TestFlight builds by accessing these links: + +- Official: https://testflight.apple.com/join/3gcYeoMr +- Experimental: https://testflight.apple.com/join/7I3dLCNT. ### Google Play beta -You can subscribe to Google Play Beta program and download latest versions: https://play.google.com/store/apps/details?id=chat.rocket.reactnative +You can subscribe to Google Play Beta program and download latest versions: + +- Official: https://play.google.com/store/apps/details?id=chat.rocket.android +- Experimental: https://play.google.com/store/apps/details?id=chat.rocket.reactnative ## Reporting an Issue @@ -57,13 +73,8 @@ If you don't need multiple servers, there is a branch `single-server` just for t Readme will guide you on how to config. ## Current priorities -1) Jitsi integration -2) Notification Preferences -3) Two-way authentication -4) Bugsnag -5) Optional Analytics -6) Typescript -7) Prettier +1) Omnichannel support +2) E2E encryption ## Features | Feature | Status | @@ -71,6 +82,7 @@ Readme will guide you on how to config. | Jitsi Integration | ✅ | | Federation (Directory) | ✅ | | Discussions | ❌ | +| Omnichannel | ❌ | | Threads | ✅ | | Record Audio | ✅ | | Record Video | ✅ | @@ -83,18 +95,18 @@ Readme will guide you on how to config. | Grouped messages | ✅ | | Mark room as read | ✅ | | Mark room as unread | ✅ | -| Tablet Support | ❌ | +| Tablet Support | ✅ | | Read receipt | ✅ | | Broadbast Channel | ✅ | | Authentication via SAML | ✅ | | Authentication via CAS | ✅ | | Custom Fields on Signup | ✅ | | Report message | ✅ | -| Theming | ❌ | +| Theming | ✅ | | Settings -> Review the App | ❌ | | Settings -> Default Browser | ❌ | | Admin panel | ✅ | -| Reply message from notification | ❌ | +| Reply message from notification | ✅ | | Unread counter banner on message list | ✅ | | E2E Encryption | ❌ | | Join a Protected Room | ❌ | @@ -106,7 +118,7 @@ Readme will guide you on how to config. | Accessibility (Medium) | ❌ | | Accessibility (Advanced) | ❌ | | Authentication via Meteor | ❌ | -| Authentication via Wordpress | ❌ | +| Authentication via Wordpress | ✅ | | Authentication via Custom OAuth | ✅ | | Add user to the room | ✅ | | Send message | ✅ | diff --git a/__mocks__/react-native-splash-screen.js b/__mocks__/react-native-bootsplash.js similarity index 100% rename from __mocks__/react-native-splash-screen.js rename to __mocks__/react-native-bootsplash.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index b6bc2022d..b7e07750c 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -213,7 +213,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -455,7 +455,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -697,7 +697,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -917,7 +917,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -1011,7 +1011,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -1105,7 +1105,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -1325,7 +1325,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -1441,7 +1441,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -1701,7 +1701,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -1939,7 +1939,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -2181,7 +2181,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -2452,7 +2452,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -2704,7 +2704,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -2745,7 +2745,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -3005,7 +3005,7 @@ exports[`Storyshots Message list 1`] = ` @@ -3025,7 +3025,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -3083,7 +3083,7 @@ exports[`Storyshots Message list 1`] = ` @@ -3103,7 +3103,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -3165,7 +3165,7 @@ exports[`Storyshots Message list 1`] = ` @@ -3185,7 +3185,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -3453,7 +3453,7 @@ exports[`Storyshots Message list 1`] = ` @@ -3473,7 +3473,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -3533,7 +3533,7 @@ exports[`Storyshots Message list 1`] = ` @@ -3553,7 +3553,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -3817,7 +3817,7 @@ exports[`Storyshots Message list 1`] = ` @@ -3837,7 +3837,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -3953,7 +3953,7 @@ exports[`Storyshots Message list 1`] = ` @@ -3973,7 +3973,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -4219,7 +4219,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -4461,7 +4461,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -4703,7 +4703,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -5108,7 +5108,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -5355,7 +5355,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -5602,7 +5602,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -5976,7 +5976,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -6224,7 +6224,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -6503,7 +6503,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -6803,7 +6803,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -7045,7 +7045,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -7627,7 +7627,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -8650,7 +8650,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -8870,7 +8870,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -9090,7 +9090,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -9310,7 +9310,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -9552,7 +9552,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -9834,7 +9834,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -9973,7 +9973,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -10193,7 +10193,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -10458,7 +10458,7 @@ exports[`Storyshots Message list 1`] = ` Array [ undefined, Object { - "color": "#0d0e12", + "color": "#2f343d", }, ] } @@ -10718,46 +10718,66 @@ exports[`Storyshots Message list 1`] = ` style={ Array [ Object { - "overflow": "hidden", + "borderRadius": 4, + "borderWidth": 1, + "minHeight": 200, + "width": "100%", + }, + Object { + "borderColor": "#e1e5e8", }, - Array [ - Object { - "borderRadius": 4, - "borderWidth": 1, - "minHeight": 200, - "width": "100%", - }, - Object { - "borderColor": "#e1e5e8", - }, - ], ] } > - + > + + - + > + + - Thread with emoji :) 😂 + Thread with emoji🙂 😂 + + + + + + + android:windowSoftInputMode="adjustResize" + android:exported="true"> - - @@ -40,6 +47,15 @@ + + + > notificationMessages = new HashMap>(); + public static String KEY_REPLY = "KEY_REPLY"; + public static String NOTIFICATION_ID = "NOTIFICATION_ID"; + + public static void clearMessages(int notId) { + notificationMessages.remove(Integer.toString(notId)); + } + + @Override + public void onReceived() throws InvalidNotificationException { + final Bundle bundle = mNotificationProps.asBundle(); + + String notId = bundle.getString("notId"); + String message = bundle.getString("message"); + + if (notificationMessages.get(notId) == null) { + notificationMessages.put(notId, new ArrayList()); + } + notificationMessages.get(notId).add(message); + + super.postNotification(notId != null ? Integer.parseInt(notId) : 1); + + notifyReceivedToJS(); + } + + @Override + public void onOpened() { + Bundle bundle = mNotificationProps.asBundle(); + final String notId = bundle.getString("notId"); + notificationMessages.remove(notId); + digestNotification(); } @Override protected Notification.Builder getNotificationBuilder(PendingIntent intent) { - final Resources res = mContext.getResources(); - String packageName = mContext.getPackageName(); + final Notification.Builder notification = new Notification.Builder(mContext); Bundle bundle = mNotificationProps.asBundle(); - int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); - int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName); String title = bundle.getString("title"); String message = bundle.getString("message"); + String notId = bundle.getString("notId"); - String CHANNEL_ID = "rocketchatrn_channel_01"; - String CHANNEL_NAME = "All"; - - final Notification.Builder notification = new Notification.Builder(mContext) - .setSmallIcon(smallIconResId) + notification .setContentIntent(intent) .setContentTitle(title) .setContentText(message) - .setStyle(new Notification.BigTextStyle().bigText(message)) .setPriority(Notification.PRIORITY_HIGH) .setDefaults(Notification.DEFAULT_ALL) .setAutoCancel(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - notification.setColor(mContext.getColor(R.color.notification_text)); - } + Integer notificationId = notId != null ? Integer.parseInt(notId) : 1; + notificationChannel(notification); + notificationIcons(notification, bundle); + notificationStyle(notification, notificationId, bundle); + notificationReply(notification, notificationId, bundle); + notificationDismiss(notification, notificationId); + return notification; + } + + private void notifyReceivedToJS() { + mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); + } + + private Bitmap getAvatar(String uri) { + try { + return Glide.with(mContext) + .asBitmap() + .apply(RequestOptions.bitmapTransform(new RoundedCorners(10))) + .load(uri) + .submit(100, 100) + .get(); + } catch (final ExecutionException | InterruptedException e) { + return null; + } + } + + private void notificationIcons(Notification.Builder notification, Bundle bundle) { + final Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + + int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); + + Gson gson = new Gson(); + Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class); + + notification + .setSmallIcon(smallIconResId) + .setLargeIcon(getAvatar(ejson.getAvatarUri())); + } + + private void notificationChannel(Notification.Builder notification) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String CHANNEL_ID = "rocketchatrn_channel_01"; + String CHANNEL_NAME = "All"; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT); @@ -63,10 +146,65 @@ public class CustomPushNotification extends PushNotification { notification.setChannelId(CHANNEL_ID); } - - Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); - notification.setLargeIcon(largeIconBitmap); - - return notification; } + + private void notificationStyle(Notification.Builder notification, int notId, Bundle bundle) { + Notification.InboxStyle messageStyle = new Notification.InboxStyle(); + List messages = notificationMessages.get(Integer.toString(notId)); + if (messages != null) { + for (int i = 0; i < messages.size(); i++) { + messageStyle.addLine(messages.get(i)); + } + String summary = bundle.getString("summaryText"); + messageStyle.setSummaryText(summary.replace("%n%", Integer.toString(messages.size()))); + notification.setNumber(messages.size()); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + notification.setColor(mContext.getColor(R.color.notification_text)); + } + + notification.setStyle(messageStyle); + } + + private void notificationReply(Notification.Builder notification, int notificationId, Bundle bundle) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return; + } + String label = "Reply"; + + final Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); + + Intent replyIntent = new Intent(mContext, ReplyBroadcast.class); + replyIntent.setAction(KEY_REPLY); + replyIntent.putExtra("pushNotification", bundle); + + PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) + .setLabel(label) + .build(); + + CharSequence title = label; + Notification.Action replyAction = new Notification.Action.Builder(smallIconResId, title, replyPendingIntent) + .addRemoteInput(remoteInput) + .setAllowGeneratedReplies(true) + .build(); + + notification + .setShowWhen(true) + .addAction(replyAction); + } + + private void notificationDismiss(Notification.Builder notification, int notificationId) { + Intent intent = new Intent(mContext, DismissNotification.class); + intent.putExtra(NOTIFICATION_ID, notificationId); + + PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, 0); + + notification.setDeleteIntent(dismissPendingIntent); + } + } diff --git a/android/app/src/main/java/chat/rocket/reactnative/DismissNotification.java b/android/app/src/main/java/chat/rocket/reactnative/DismissNotification.java new file mode 100644 index 000000000..32524a35f --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/DismissNotification.java @@ -0,0 +1,13 @@ +package chat.rocket.reactnative; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class DismissNotification extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + int notId = intent.getExtras().getInt(CustomPushNotification.NOTIFICATION_ID); + CustomPushNotification.clearMessages(notId); + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/Ejson.java b/android/app/src/main/java/chat/rocket/reactnative/Ejson.java new file mode 100644 index 000000000..591fda93a --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/Ejson.java @@ -0,0 +1,42 @@ +package chat.rocket.reactnative; + +import android.content.SharedPreferences; + +import chat.rocket.userdefaults.RNUserDefaultsModule; + +public class Ejson { + String host; + String rid; + String type; + Sender sender; + + private String TOKEN_KEY = "reactnativemeteor_usertoken-"; + private SharedPreferences sharedPreferences = RNUserDefaultsModule.getPreferences(CustomPushNotification.reactApplicationContext); + + public String getAvatarUri() { + if (type == null || !type.equals("d")) { + return null; + } + return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId(); + } + + public String token() { + return sharedPreferences.getString(TOKEN_KEY.concat(userId()), ""); + } + + public String userId() { + return sharedPreferences.getString(TOKEN_KEY.concat(serverURL()), ""); + } + + public String serverURL() { + String url = this.host; + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + return url; + } + + private class Sender { + String username; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java index 078f14142..f408e11cd 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java +++ b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.java @@ -5,16 +5,16 @@ import com.facebook.react.ReactRootView; import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; import android.os.Bundle; import com.facebook.react.ReactFragmentActivity; -import org.devio.rn.splashscreen.SplashScreen; import android.content.Intent; import android.content.res.Configuration; +import com.zoontek.rnbootsplash.RNBootSplash; public class MainActivity extends ReactFragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { - SplashScreen.show(this); - super.onCreate(null); + super.onCreate(savedInstanceState); + RNBootSplash.show(R.drawable.launch_screen, MainActivity.this); } /** diff --git a/android/app/src/main/java/chat/rocket/reactnative/ReplyBroadcast.java b/android/app/src/main/java/chat/rocket/reactnative/ReplyBroadcast.java new file mode 100644 index 000000000..81c62aeb0 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/ReplyBroadcast.java @@ -0,0 +1,157 @@ +package chat.rocket.reactnative; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.RemoteInput; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import java.io.IOException; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.HashMap; +import java.util.Map; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import chat.rocket.userdefaults.RNUserDefaultsModule; +import com.wix.reactnativenotifications.core.NotificationIntentAdapter; + +public class ReplyBroadcast extends BroadcastReceiver { + private Context mContext; + private Bundle bundle; + private NotificationManager notificationManager; + + @Override + public void onReceive(Context context, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + final CharSequence message = getReplyMessage(intent); + if (message == null) { + return; + } + + mContext = context; + bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + String notId = bundle.getString("notId"); + + Gson gson = new Gson(); + Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class); + + replyToMessage(ejson, Integer.parseInt(notId), message); + } + } + + protected void replyToMessage(final Ejson ejson, final int notId, final CharSequence message) { + String serverURL = ejson.serverURL(); + String rid = ejson.rid; + + if (serverURL == null || rid == null) { + return; + } + + final OkHttpClient client = new OkHttpClient(); + final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + String json = buildMessage(rid, message.toString()); + + CustomPushNotification.clearMessages(notId); + + RequestBody body = RequestBody.create(JSON, json); + Request request = new Request.Builder() + .header("x-auth-token", ejson.token()) + .header("x-user-id", ejson.userId()) + .url(String.format("%s/api/v1/chat.sendMessage", serverURL)) + .post(body) + .build(); + + client.newCall(request).enqueue(new okhttp3.Callback() { + @Override + public void onFailure(Call call, IOException e) { + Log.i("RCNotification", String.format("Reply FAILED exception %s", e.getMessage())); + onReplyFailed(notificationManager, notId); + } + + @Override + public void onResponse(Call call, final Response response) throws IOException { + if (response.isSuccessful()) { + Log.d("RCNotification", "Reply SUCCESS"); + onReplySuccess(notificationManager, notId); + } else { + Log.i("RCNotification", String.format("Reply FAILED status %s BODY %s", response.code(), response.body().string())); + onReplyFailed(notificationManager, notId); + } + } + }); + } + + private String getMessageId() { + final String ALPHA_NUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + int count = 17; + StringBuilder builder = new StringBuilder(); + while (count-- != 0) { + int character = (int)(Math.random()*ALPHA_NUMERIC_STRING.length()); + builder.append(ALPHA_NUMERIC_STRING.charAt(character)); + } + return builder.toString(); + } + + protected String buildMessage(String rid, String message) { + Gson gsonBuilder = new GsonBuilder().create(); + + Map msgMap = new HashMap(); + msgMap.put("_id", getMessageId()); + msgMap.put("rid", rid); + msgMap.put("msg", message); + msgMap.put("tmid", null); + + Map msg = new HashMap(); + msg.put("message", msgMap); + + String json = gsonBuilder.toJson(msg); + + return json; + } + + protected void onReplyFailed(NotificationManager notificationManager, int notId) { + String CHANNEL_ID = "CHANNEL_ID_REPLY_FAILED"; + + final Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); + + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW); + notificationManager.createNotificationChannel(channel); + Notification notification = + new Notification.Builder(mContext, CHANNEL_ID) + .setContentTitle("Failed to reply message.") + .setSmallIcon(smallIconResId) + .build(); + + notificationManager.notify(notId, notification); + } + + protected void onReplySuccess(NotificationManager notificationManager, int notId) { + notificationManager.cancel(notId); + } + + private CharSequence getReplyMessage(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(CustomPushNotification.KEY_REPLY); + } + return null; + } +} diff --git a/android/app/src/main/res/drawable-hdpi/icon_wordpress.png b/android/app/src/main/res/drawable-hdpi/icon_wordpress.png new file mode 100644 index 000000000..4dbc11e67 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_wordpress.png differ diff --git a/android/app/src/main/res/drawable-hdpi/launch_screen.png b/android/app/src/main/res/drawable-hdpi/launch_screen.png deleted file mode 100644 index bc960ac72..000000000 Binary files a/android/app/src/main/res/drawable-hdpi/launch_screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_wordpress.png b/android/app/src/main/res/drawable-mdpi/icon_wordpress.png new file mode 100644 index 000000000..cc24baf40 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_wordpress.png differ diff --git a/android/app/src/main/res/drawable-mdpi/launch_screen.png b/android/app/src/main/res/drawable-mdpi/launch_screen.png deleted file mode 100644 index 1c11678c8..000000000 Binary files a/android/app/src/main/res/drawable-mdpi/launch_screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_wordpress.png b/android/app/src/main/res/drawable-xhdpi/icon_wordpress.png new file mode 100644 index 000000000..e3457517b Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_wordpress.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/launch_screen.png b/android/app/src/main/res/drawable-xhdpi/launch_screen.png deleted file mode 100644 index 38cbcb666..000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/launch_screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_wordpress.png b/android/app/src/main/res/drawable-xxhdpi/icon_wordpress.png new file mode 100644 index 000000000..82b896631 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_wordpress.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/launch_screen.png b/android/app/src/main/res/drawable-xxhdpi/launch_screen.png deleted file mode 100644 index 3b36d489c..000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/launch_screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_wordpress.png b/android/app/src/main/res/drawable-xxxhdpi/icon_wordpress.png new file mode 100644 index 000000000..6d7f42995 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_wordpress.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/launch_screen.png b/android/app/src/main/res/drawable-xxxhdpi/launch_screen.png deleted file mode 100644 index 040a3ec38..000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/launch_screen.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 000000000..ad69ba207 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/launch_screen.xml b/android/app/src/main/res/drawable/launch_screen.xml new file mode 100644 index 000000000..a85f4c9fe --- /dev/null +++ b/android/app/src/main/res/drawable/launch_screen.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/launch_screen_bitmap.xml b/android/app/src/main/res/drawable/launch_screen_bitmap.xml deleted file mode 100644 index de474fb1f..000000000 --- a/android/app/src/main/res/drawable/launch_screen_bitmap.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/layout/launch_screen.xml b/android/app/src/main/res/layout/launch_screen.xml deleted file mode 100644 index 97c4ac273..000000000 --- a/android/app/src/main/res/layout/launch_screen.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 000000000..1e24e3159 --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #000000 + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index effdcbed1..8ba17e337 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ #660B0B0B - #FFFFFF + #eeeff1 #CC3333 \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index fdfc9173c..50861d8e0 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,7 +1,8 @@ + + diff --git a/android/build.gradle b/android/build.gradle index 62a22d81d..aaef31e10 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -55,16 +55,16 @@ allprojects { } } -// subprojects { subproject -> -// afterEvaluate { -// if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) { -// android { -// compileSdkVersion 28 -// buildToolsVersion "28.0.3" -// defaultConfig { -// targetSdkVersion 28 -// } -// } -// } -// } -// } +subprojects { subproject -> + afterEvaluate { + if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) { + android { + compileSdkVersion 28 + buildToolsVersion "28.0.3" + defaultConfig { + targetSdkVersion 28 + } + } + } + } +} diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index ced5b25ef..15718b171 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -54,3 +54,11 @@ export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT'; export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS'; export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS'; export const USERS_TYPING = createRequestTypes('USERS_TYPING', ['ADD', 'REMOVE', 'CLEAR']); +export const INVITE_LINKS = createRequestTypes('INVITE_LINKS', [ + 'SET_TOKEN', + 'SET_PARAMS', + 'SET_INVITE', + 'CREATE', + 'CLEAR', + ...defaultTypes +]); diff --git a/app/actions/inviteLinks.js b/app/actions/inviteLinks.js new file mode 100644 index 000000000..b456603cf --- /dev/null +++ b/app/actions/inviteLinks.js @@ -0,0 +1,55 @@ +import * as types from './actionsTypes'; + +export function inviteLinksSetToken(token) { + return { + type: types.INVITE_LINKS.SET_TOKEN, + token + }; +} + +export function inviteLinksRequest(token) { + return { + type: types.INVITE_LINKS.REQUEST, + token + }; +} + +export function inviteLinksSuccess() { + return { + type: types.INVITE_LINKS.SUCCESS + }; +} + +export function inviteLinksFailure() { + return { + type: types.INVITE_LINKS.FAILURE + }; +} + +export function inviteLinksClear() { + return { + type: types.INVITE_LINKS.CLEAR + }; +} + + +export function inviteLinksCreate(rid) { + return { + type: types.INVITE_LINKS.CREATE, + rid + }; +} + +export function inviteLinksSetParams(params) { + return { + type: types.INVITE_LINKS.SET_PARAMS, + params + }; +} + +export function inviteLinksSetInvite(invite) { + return { + type: types.INVITE_LINKS.SET_INVITE, + invite + }; +} diff --git a/app/constants/settings.js b/app/constants/settings.js index d4697a709..74c6234f6 100644 --- a/app/constants/settings.js +++ b/app/constants/settings.js @@ -59,6 +59,9 @@ export default { Message_TimeFormat: { type: 'valueAsString' }, + Message_TimeAndDateFormat: { + type: 'valueAsString' + }, Site_Name: { type: 'valueAsString' }, diff --git a/app/containers/EmojiPicker/EmojiCategory.js b/app/containers/EmojiPicker/EmojiCategory.js index e7a71f513..22382f1e3 100644 --- a/app/containers/EmojiPicker/EmojiCategory.js +++ b/app/containers/EmojiPicker/EmojiCategory.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Text, TouchableOpacity, FlatList } from 'react-native'; -import { shortnameToUnicode } from 'emoji-toolkit'; import { responsive } from 'react-native-responsive-ui'; +import shortnameToUnicode from '../../utils/shortnameToUnicode'; import styles from './styles'; import CustomEmoji from './CustomEmoji'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 817b0f602..65a973715 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -2,7 +2,6 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import ScrollableTabView from 'react-native-scrollable-tab-view'; -import { shortnameToUnicode } from 'emoji-toolkit'; import equal from 'deep-equal'; import { connect } from 'react-redux'; import orderBy from 'lodash/orderBy'; @@ -15,6 +14,7 @@ import categories from './categories'; import database from '../../lib/database'; import { emojisByCategory } from '../../emojis'; import protectedFunction from '../../lib/methods/helpers/protectedFunction'; +import shortnameToUnicode from '../../utils/shortnameToUnicode'; import log from '../../utils/log'; import { themes } from '../../constants/colors'; import { withTheme } from '../../theme'; diff --git a/app/containers/FileModal.js b/app/containers/FileModal.js deleted file mode 100644 index 2943afa4f..000000000 --- a/app/containers/FileModal.js +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useState } from 'react'; -import { - View, Text, TouchableWithoutFeedback, StyleSheet, SafeAreaView -} from 'react-native'; -import FastImage from 'react-native-fast-image'; -import PropTypes from 'prop-types'; -import Modal from 'react-native-modal'; -import ImageViewer from 'react-native-image-zoom-viewer'; -import { Video } from 'expo-av'; - -import sharedStyles from '../views/Styles'; -import { formatAttachmentUrl } from '../lib/utils'; -import ActivityIndicator from './ActivityIndicator'; -import { themes } from '../constants/colors'; -import { withTheme } from '../theme'; - -const styles = StyleSheet.create({ - safeArea: { - flex: 1 - }, - modal: { - margin: 0 - }, - titleContainer: { - width: '100%', - alignItems: 'center', - marginVertical: 10 - }, - title: { - textAlign: 'center', - fontSize: 16, - ...sharedStyles.textSemibold - }, - description: { - textAlign: 'center', - fontSize: 14, - ...sharedStyles.textMedium - }, - video: { - flex: 1 - } -}); - -const ModalContent = React.memo(({ - attachment, onClose, user, baseUrl, theme -}) => { - if (attachment && attachment.image_url) { - const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl); - return ( - - - - {attachment.title} - {attachment.description ? {attachment.description} : null} - - - null} - renderImage={props => } - loadingRender={() => } - /> - - ); - } - if (attachment && attachment.video_url) { - const [loading, setLoading] = useState(true); - const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl); - return ( - <> -