From 23d166146e7b4586d84a18fb07769a1ec301f606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Tue, 27 Jul 2021 18:32:18 +0200 Subject: [PATCH 001/181] Start using the messaging style for the notifications --- android/app/src/main/AndroidManifest.xml | 3 + .../expensify/chat/CustomAirshipExtender.java | 15 ++ .../chat/CustomNotificationProvider.java | 249 ++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java create mode 100644 android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6976c0f8c828..1ee7203fb56e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -59,5 +59,8 @@ + + diff --git a/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java b/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java new file mode 100644 index 000000000000..76f024c1c749 --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java @@ -0,0 +1,15 @@ +package com.expensify.chat; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.urbanairship.UAirship; +import com.urbanairship.reactnative.AirshipExtender; + +public class CustomAirshipExtender implements AirshipExtender { + @Override + public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship) { + airship.getPushManager().setNotificationProvider(new CustomNotificationProvider(context, airship.getAirshipConfigOptions())); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java new file mode 100644 index 000000000000..0200a10cf2ea --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -0,0 +1,249 @@ +package com.expensify.chat; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.service.notification.StatusBarNotification; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.WindowManager; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; +import androidx.core.app.Person; +import androidx.core.graphics.drawable.IconCompat; + +import com.urbanairship.AirshipConfigOptions; +import com.urbanairship.json.JsonException; +import com.urbanairship.json.JsonList; +import com.urbanairship.json.JsonMap; +import com.urbanairship.json.JsonValue; +import com.urbanairship.push.PushMessage; +import com.urbanairship.push.notifications.NotificationArguments; +import com.urbanairship.reactnative.ReactNotificationProvider; +import com.urbanairship.util.ImageUtils; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class CustomNotificationProvider extends ReactNotificationProvider { + + // Resize icons to 100 dp x 100 dp + private static final int MAX_ICON_SIZE_DPS = 100; + + // Max wait time to resolve an icon. We have ~10 seconds to a little less + // to ensure the notification builds. + private static final int MAX_ICON_FETCH_WAIT_TIME_SECONDS = 8; + + // Fallback drawable ID. 0 to not use a fallback ID. + private static final int FALLBACK_ICON_ID = 0; + + // Logging + private static final String TAG = "NotificationProvider"; + + // Conversation JSON keys + private static final String CONVERSATION_KEY = "conversation"; + private static final String CONVERSATION_OWNER_KEY = "owner"; + private static final String CONVERSATION_TITLE_KEY = "title"; + private static final String PEOPLE_KEY = "people"; + private static final String PERSON_ID_KEY = "id"; + private static final String PERSON_ICON_KEY = "icon"; + private static final String PERSON_NAME_KEY = "name"; + private static final String MESSAGES_KEY = "messages"; + private static final String MESSAGE_TEXT_KEY = "text"; + private static final String MESSAGE_TIME_KEY = "time"; + private static final String MESSAGE_PERSON_KEY = "person"; + + // Expensify Conversation JSON keys + private static final String PAYLOAD_KEY = "payload"; + private static final String TYPE_KEY = "type"; + private static final String REPORT_COMMENT_TYPE = "reportComment"; + + private final ExecutorService executorService = Executors.newCachedThreadPool(); + + public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { + super(context, configOptions); + } + + @NonNull + @Override + protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @NonNull NotificationCompat.Builder builder, @NonNull NotificationArguments arguments) { + super.onExtendBuilder(context, builder, arguments); + PushMessage message = arguments.getMessage(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return builder; + } + + if (message.containsKey(CONVERSATION_KEY)) { + applyMessageStyle(message, builder); + } + + if (message.containsKey(PAYLOAD_KEY)) { + try { + JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); + + if (REPORT_COMMENT_TYPE.equals(payload.get(TYPE_KEY).getString())) { + applyExpensifyMessageStyle(builder, payload); + } + } catch (Exception e) { + Log.e(TAG, "Failed to parse conversation", e); + } + } + + return builder; + } + + private void applyExpensifyMessageStyle(NotificationCompat.Builder builder, JsonMap payload) { + int reportID = payload.get("reportID").getInt(-1); + JsonMap reportAction = payload.get("reportAction").getMap(); + String name = reportAction.get("person").getList().get(0).getMap().get("text").getString(); + String avatar = reportAction.get("avatar").getString(); + String accountID = Integer.toString(reportAction.get("actorAccountID").getInt(-1)); + + String message = reportAction.get("message").getList().get(0).getMap().get("text").getString(); + long time = reportAction.get("timestamp").getLong(0); + + IconCompat iconCompat = fetchIcon(avatar, FALLBACK_ICON_ID); + Person person = new Person.Builder() + .setIcon(iconCompat) + .setKey(accountID) + .setName(name) + .build(); + + NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person) + .setGroupConversation(false) + .setConversationTitle("Chat with " + name); + + messagingStyle.addMessage(message, time, person); + + builder.setStyle(messagingStyle); + } + + + private void applyMessageStyle(PushMessage message, NotificationCompat.Builder builder) { + JsonMap conversation = null; + try { + conversation = JsonValue.parseString(message.getExtra(CONVERSATION_KEY)).optMap(); + } catch (JsonException e) { + Log.e(TAG, "Failed to parse conversation", e); + } + + if (conversation == null) { + return; + } + + Map people = resolvePeople(conversation.opt(PEOPLE_KEY).optList()); + if (people.isEmpty()) { + Log.e(TAG, "Missing people."); + return; + } + + String ownerKey = conversation.get(CONVERSATION_OWNER_KEY).getString(); + if (!people.containsKey(ownerKey)) { + Log.e(TAG, "Missing owner."); + return; + } + + NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(people.get(ownerKey)) + .setGroupConversation(people.size() > 2) + .setConversationTitle(conversation.opt(CONVERSATION_TITLE_KEY).optString()); + + for (JsonValue messageJson : conversation.opt(MESSAGES_KEY).optList()) { + String personKey = messageJson.optMap().opt(MESSAGE_PERSON_KEY).getString(); + String text = messageJson.optMap().opt(MESSAGE_TEXT_KEY).getString(); + long time = messageJson.optMap().opt(MESSAGE_TIME_KEY).getLong(0); + + if (people.containsKey(personKey) && text != null && time > 0) { + messagingStyle.addMessage(text, time, people.get(personKey)); + } + } + + builder.setStyle(messagingStyle); + } + + private Map resolvePeople(JsonList peopleJson) { + Map people = Collections.synchronizedMap(new HashMap<>()); + CountDownLatch countDownLatch = new CountDownLatch(peopleJson.size()); + + for (JsonValue personJson : peopleJson) { + executorService.execute(() -> { + String id = personJson.optMap().opt(PERSON_ID_KEY).optString(); + String name = personJson.optMap().opt(PERSON_NAME_KEY).optString(); + String icon = personJson.optMap().opt(PERSON_ICON_KEY).optString(); + + if (id != null) { + IconCompat iconCompat = fetchIcon(icon, FALLBACK_ICON_ID); + Person person = new Person.Builder() + .setIcon(iconCompat) + .setKey(id) + .setName(name) + .build(); + + people.put(id, person); + } + + countDownLatch.countDown(); + }); + } + + try { + countDownLatch.await(); + } catch (InterruptedException e) { + Log.e(TAG, "Failed to resolve people", e); + Thread.currentThread().interrupt(); + } + + return people; + } + + @NonNull + private IconCompat fetchIcon(@NonNull String urlString, @DrawableRes int fallbackId) { + // TODO: Add disk LRU cache + + URL parsedUrl = null; + try { + parsedUrl = urlString == null ? null : new URL(urlString); + } catch (MalformedURLException e) { + Log.e(TAG, "Failed to resolve URL " + urlString, e); + } + + if (parsedUrl != null) { + WindowManager window = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics dm = new DisplayMetrics(); + window.getDefaultDisplay().getMetrics(dm); + + final int reqWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MAX_ICON_SIZE_DPS, dm); + final int reqHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MAX_ICON_SIZE_DPS, dm); + + final URL url = parsedUrl; + Future future = executorService.submit(() -> ImageUtils.fetchScaledBitmap(context, url, reqWidth, reqHeight)); + + try { + Bitmap bitmap = future.get(MAX_ICON_FETCH_WAIT_TIME_SECONDS, TimeUnit.SECONDS); + return IconCompat.createWithBitmap(bitmap); + } catch (InterruptedException e) { + Log.e(TAG,"Failed to fetch icon", e); + Thread.currentThread().interrupt(); + } catch (Exception e) { + Log.e(TAG,"Failed to fetch icon", e); + future.cancel(true); + } + } + + return fallbackId == 0 ? null : IconCompat.createWithResource(context, fallbackId); + } +} From c3fc83e3cd3a891ea69b810b128dc85c64f6c6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Wed, 28 Jul 2021 12:35:59 +0200 Subject: [PATCH 002/181] Cache conversations to update update conversations --- .../chat/CustomNotificationProvider.java | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 0200a10cf2ea..248d65269df3 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -15,6 +15,7 @@ import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.graphics.drawable.IconCompat; @@ -30,6 +31,7 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -73,6 +75,7 @@ public class CustomNotificationProvider extends ReactNotificationProvider { private static final String REPORT_COMMENT_TYPE = "reportComment"; private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final HashMap cache = new HashMap<>(); public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { super(context, configOptions); @@ -97,7 +100,7 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); if (REPORT_COMMENT_TYPE.equals(payload.get(TYPE_KEY).getString())) { - applyExpensifyMessageStyle(builder, payload); + applyExpensifyMessageStyle(builder, payload, arguments); } } catch (Exception e) { Log.e(TAG, "Failed to parse conversation", e); @@ -107,8 +110,10 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ return builder; } - private void applyExpensifyMessageStyle(NotificationCompat.Builder builder, JsonMap payload) { + private void applyExpensifyMessageStyle(NotificationCompat.Builder builder, JsonMap payload, NotificationArguments arguments) { int reportID = payload.get("reportID").getInt(-1); + NotificationCache notificationCache = findOrCreateNotificationCache(reportID); + JsonMap reportAction = payload.get("reportAction").getMap(); String name = reportAction.get("person").getList().get(0).getMap().get("text").getString(); String avatar = reportAction.get("avatar").getString(); @@ -124,13 +129,37 @@ private void applyExpensifyMessageStyle(NotificationCompat.Builder builder, Json .setName(name) .build(); + if (!notificationCache.people.containsKey(accountID)) { + notificationCache.people.put(accountID, person); + } + + notificationCache.messages.add(new NotificationCache.Message(person, message, time)); + NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person) - .setGroupConversation(false) + .setGroupConversation(notificationCache.people.size() > 2) .setConversationTitle("Chat with " + name); - messagingStyle.addMessage(message, time, person); + for (NotificationCache.Message cachedMessage : notificationCache.messages) { + messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, cachedMessage.person); + } + + if (notificationCache.prevNotificationId != -1) { + NotificationManagerCompat.from(context).cancel(notificationCache.prevNotificationId); + } builder.setStyle(messagingStyle); + notificationCache.prevNotificationId = arguments.getNotificationId(); + } + + private NotificationCache findOrCreateNotificationCache(int reportID) { + NotificationCache notificationCache = cache.get(reportID); + + if (notificationCache == null) { + notificationCache = new NotificationCache(); + cache.put(reportID, notificationCache); + } + + return notificationCache; } @@ -246,4 +275,22 @@ private IconCompat fetchIcon(@NonNull String urlString, @DrawableRes int fallbac return fallbackId == 0 ? null : IconCompat.createWithResource(context, fallbackId); } + + private static class NotificationCache { + public Map people = new HashMap<>(); + public ArrayList messages = new ArrayList<>(); + public int prevNotificationId = -1; + + public static class Message { + public Person person; + public String text; + public long time; + + Message(Person person, String text, long time) { + this.person = person; + this.text = text; + this.time = time; + } + } + } } From 259c99303d8df1cb2eca1cc856931eb259417d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Wed, 28 Jul 2021 14:03:06 +0200 Subject: [PATCH 003/181] Clear cache on notification dismiss --- .../expensify/chat/CustomAirshipExtender.java | 10 +- .../chat/CustomNotificationListener.java | 48 ++++++++ .../chat/CustomNotificationProvider.java | 111 ++---------------- 3 files changed, 68 insertions(+), 101 deletions(-) create mode 100644 android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java diff --git a/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java b/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java index 76f024c1c749..32fa546102cd 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java +++ b/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java @@ -5,11 +5,19 @@ import androidx.annotation.NonNull; import com.urbanairship.UAirship; +import com.urbanairship.push.NotificationListener; +import com.urbanairship.push.PushManager; import com.urbanairship.reactnative.AirshipExtender; public class CustomAirshipExtender implements AirshipExtender { @Override public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship) { - airship.getPushManager().setNotificationProvider(new CustomNotificationProvider(context, airship.getAirshipConfigOptions())); + PushManager pushManager = airship.getPushManager(); + + CustomNotificationProvider notificationProvider = new CustomNotificationProvider(context, airship.getAirshipConfigOptions()); + pushManager.setNotificationProvider(notificationProvider); + + NotificationListener notificationListener = airship.getPushManager().getNotificationListener(); + pushManager.setNotificationListener(new CustomNotificationListener(notificationListener, notificationProvider)); } } \ No newline at end of file diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java new file mode 100644 index 000000000000..7d00e74b542c --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java @@ -0,0 +1,48 @@ +package com.expensify.chat; + +import androidx.annotation.NonNull; + +import com.urbanairship.push.NotificationActionButtonInfo; +import com.urbanairship.push.NotificationInfo; +import com.urbanairship.push.NotificationListener; +import com.urbanairship.push.PushMessage; + +import org.jetbrains.annotations.NotNull; + +public class CustomNotificationListener implements NotificationListener { + private final NotificationListener parent; + private final CustomNotificationProvider provider; + + CustomNotificationListener(NotificationListener parent, CustomNotificationProvider provider) { + this.parent = parent; + this.provider = provider; + } + + @Override + public void onNotificationPosted(@NonNull @NotNull NotificationInfo notificationInfo) { + parent.onNotificationPosted(notificationInfo); + } + + @Override + public boolean onNotificationOpened(@NonNull @NotNull NotificationInfo notificationInfo) { + return parent.onNotificationOpened(notificationInfo); + } + + @Override + public boolean onNotificationForegroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { + return parent.onNotificationForegroundAction(notificationInfo, actionButtonInfo); + } + + @Override + public void onNotificationBackgroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { + parent.onNotificationBackgroundAction(notificationInfo, actionButtonInfo); + } + + @Override + public void onNotificationDismissed(@NonNull @NotNull NotificationInfo notificationInfo) { + parent.onNotificationDismissed(notificationInfo); + + PushMessage message = notificationInfo.getMessage(); + provider.onDismissNotification(message); + } +} diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 248d65269df3..db2f8f711d5b 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -1,11 +1,8 @@ package com.expensify.chat; -import android.app.Notification; -import android.app.NotificationManager; import android.content.Context; import android.graphics.Bitmap; import android.os.Build; -import android.service.notification.StatusBarNotification; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; @@ -13,15 +10,12 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.graphics.drawable.IconCompat; import com.urbanairship.AirshipConfigOptions; -import com.urbanairship.json.JsonException; -import com.urbanairship.json.JsonList; import com.urbanairship.json.JsonMap; import com.urbanairship.json.JsonValue; import com.urbanairship.push.PushMessage; @@ -32,10 +26,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -57,25 +49,12 @@ public class CustomNotificationProvider extends ReactNotificationProvider { private static final String TAG = "NotificationProvider"; // Conversation JSON keys - private static final String CONVERSATION_KEY = "conversation"; - private static final String CONVERSATION_OWNER_KEY = "owner"; - private static final String CONVERSATION_TITLE_KEY = "title"; - private static final String PEOPLE_KEY = "people"; - private static final String PERSON_ID_KEY = "id"; - private static final String PERSON_ICON_KEY = "icon"; - private static final String PERSON_NAME_KEY = "name"; - private static final String MESSAGES_KEY = "messages"; - private static final String MESSAGE_TEXT_KEY = "text"; - private static final String MESSAGE_TIME_KEY = "time"; - private static final String MESSAGE_PERSON_KEY = "person"; - - // Expensify Conversation JSON keys private static final String PAYLOAD_KEY = "payload"; private static final String TYPE_KEY = "type"; private static final String REPORT_COMMENT_TYPE = "reportComment"; private final ExecutorService executorService = Executors.newCachedThreadPool(); - private final HashMap cache = new HashMap<>(); + public final HashMap cache = new HashMap<>(); public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { super(context, configOptions); @@ -91,16 +70,12 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ return builder; } - if (message.containsKey(CONVERSATION_KEY)) { - applyMessageStyle(message, builder); - } - if (message.containsKey(PAYLOAD_KEY)) { try { JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); if (REPORT_COMMENT_TYPE.equals(payload.get(TYPE_KEY).getString())) { - applyExpensifyMessageStyle(builder, payload, arguments); + applyMessageStyle(builder, payload, arguments); } } catch (Exception e) { Log.e(TAG, "Failed to parse conversation", e); @@ -110,15 +85,13 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ return builder; } - private void applyExpensifyMessageStyle(NotificationCompat.Builder builder, JsonMap payload, NotificationArguments arguments) { + private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap payload, NotificationArguments arguments) { int reportID = payload.get("reportID").getInt(-1); NotificationCache notificationCache = findOrCreateNotificationCache(reportID); - JsonMap reportAction = payload.get("reportAction").getMap(); String name = reportAction.get("person").getList().get(0).getMap().get("text").getString(); String avatar = reportAction.get("avatar").getString(); String accountID = Integer.toString(reportAction.get("actorAccountID").getInt(-1)); - String message = reportAction.get("message").getList().get(0).getMap().get("text").getString(); long time = reportAction.get("timestamp").getLong(0); @@ -162,81 +135,19 @@ private NotificationCache findOrCreateNotificationCache(int reportID) { return notificationCache; } - - private void applyMessageStyle(PushMessage message, NotificationCompat.Builder builder) { - JsonMap conversation = null; + public void onDismissNotification(PushMessage message) { try { - conversation = JsonValue.parseString(message.getExtra(CONVERSATION_KEY)).optMap(); - } catch (JsonException e) { - Log.e(TAG, "Failed to parse conversation", e); - } - - if (conversation == null) { - return; - } - - Map people = resolvePeople(conversation.opt(PEOPLE_KEY).optList()); - if (people.isEmpty()) { - Log.e(TAG, "Missing people."); - return; - } - - String ownerKey = conversation.get(CONVERSATION_OWNER_KEY).getString(); - if (!people.containsKey(ownerKey)) { - Log.e(TAG, "Missing owner."); - return; - } + JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); + int reportID = payload.get("reportID").getInt(-1); - NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(people.get(ownerKey)) - .setGroupConversation(people.size() > 2) - .setConversationTitle(conversation.opt(CONVERSATION_TITLE_KEY).optString()); - - for (JsonValue messageJson : conversation.opt(MESSAGES_KEY).optList()) { - String personKey = messageJson.optMap().opt(MESSAGE_PERSON_KEY).getString(); - String text = messageJson.optMap().opt(MESSAGE_TEXT_KEY).getString(); - long time = messageJson.optMap().opt(MESSAGE_TIME_KEY).getLong(0); - - if (people.containsKey(personKey) && text != null && time > 0) { - messagingStyle.addMessage(text, time, people.get(personKey)); + if (reportID == -1) { + return; } - } - - builder.setStyle(messagingStyle); - } - private Map resolvePeople(JsonList peopleJson) { - Map people = Collections.synchronizedMap(new HashMap<>()); - CountDownLatch countDownLatch = new CountDownLatch(peopleJson.size()); - - for (JsonValue personJson : peopleJson) { - executorService.execute(() -> { - String id = personJson.optMap().opt(PERSON_ID_KEY).optString(); - String name = personJson.optMap().opt(PERSON_NAME_KEY).optString(); - String icon = personJson.optMap().opt(PERSON_ICON_KEY).optString(); - - if (id != null) { - IconCompat iconCompat = fetchIcon(icon, FALLBACK_ICON_ID); - Person person = new Person.Builder() - .setIcon(iconCompat) - .setKey(id) - .setName(name) - .build(); - - people.put(id, person); - } - - countDownLatch.countDown(); - }); - } - - try { - countDownLatch.await(); - } catch (InterruptedException e) { - Log.e(TAG, "Failed to resolve people", e); - Thread.currentThread().interrupt(); + cache.remove(reportID); + } catch (Exception e) { + Log.e(TAG, "Failed to delete conversation cache"); } - - return people; } @NonNull From c00d7d9b6239ba27d347266f0e05a2cf8d4b7758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Wed, 28 Jul 2021 16:41:05 +0200 Subject: [PATCH 004/181] Add some comments --- .../chat/CustomNotificationListener.java | 3 ++ .../chat/CustomNotificationProvider.java | 34 +++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java index 7d00e74b542c..73d8a1031654 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java @@ -9,6 +9,9 @@ import org.jetbrains.annotations.NotNull; +/** + * Allows us to clear the notification cache when the user dismisses a notification. + */ public class CustomNotificationListener implements NotificationListener { private final NotificationListener parent; private final CustomNotificationProvider provider; diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index db2f8f711d5b..35b56b629dd9 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -74,8 +74,9 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ try { JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); + // Apply message style only for report comments if (REPORT_COMMENT_TYPE.equals(payload.get(TYPE_KEY).getString())) { - applyMessageStyle(builder, payload, arguments); + applyMessageStyle(builder, payload, arguments.getNotificationId()); } } catch (Exception e) { Log.e(TAG, "Failed to parse conversation", e); @@ -85,7 +86,15 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ return builder; } - private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap payload, NotificationArguments arguments) { + /** + * Applies the message style to the notification builder. It also takes advantage of the + * notification cache to build conversations. + * + * @param builder Notification builder that will receive the message style + * @param payload Notification payload, which contains all the data we need to build the notifications. + * @param notificationID Current notification ID + */ + private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap payload, int notificationID) { int reportID = payload.get("reportID").getInt(-1); NotificationCache notificationCache = findOrCreateNotificationCache(reportID); JsonMap reportAction = payload.get("reportAction").getMap(); @@ -116,14 +125,22 @@ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap paylo messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, cachedMessage.person); } - if (notificationCache.prevNotificationId != -1) { - NotificationManagerCompat.from(context).cancel(notificationCache.prevNotificationId); + if (notificationCache.prevNotificationID != -1) { + NotificationManagerCompat.from(context).cancel(notificationCache.prevNotificationID); } builder.setStyle(messagingStyle); - notificationCache.prevNotificationId = arguments.getNotificationId(); + notificationCache.prevNotificationID = notificationID; } + /** + * Check if we are showing a notification related to a reportID. + * If not, create a new NotificationCache so we can build a conversation notification + * as the messages come. + * + * @param reportID Report ID. + * @return Notification Cache. + */ private NotificationCache findOrCreateNotificationCache(int reportID) { NotificationCache notificationCache = cache.get(reportID); @@ -135,6 +152,11 @@ private NotificationCache findOrCreateNotificationCache(int reportID) { return notificationCache; } + /** + * Remove the notification data from the cache when the user dismisses the notification. + * + * @param message Push notification's message + */ public void onDismissNotification(PushMessage message) { try { JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); @@ -190,7 +212,7 @@ private IconCompat fetchIcon(@NonNull String urlString, @DrawableRes int fallbac private static class NotificationCache { public Map people = new HashMap<>(); public ArrayList messages = new ArrayList<>(); - public int prevNotificationId = -1; + public int prevNotificationID = -1; public static class Message { public Person person; From 7534047346bd62d4f64b72aae12f984796c4ab79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 2 Aug 2021 11:01:52 +0200 Subject: [PATCH 005/181] Use room name as the conversation title --- .../com/expensify/chat/CustomNotificationProvider.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 35b56b629dd9..1c14180d6339 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -103,6 +103,11 @@ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap paylo String accountID = Integer.toString(reportAction.get("actorAccountID").getInt(-1)); String message = reportAction.get("message").getList().get(0).getMap().get("text").getString(); long time = reportAction.get("timestamp").getLong(0); + String roomName = payload.get("roomName") == null ? "" : payload.get("roomName").getString(""); + String conversationTitle = "Chat with " + name; + if (!roomName.isEmpty()) { + conversationTitle = "#" + roomName; + } IconCompat iconCompat = fetchIcon(avatar, FALLBACK_ICON_ID); Person person = new Person.Builder() @@ -118,8 +123,8 @@ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap paylo notificationCache.messages.add(new NotificationCache.Message(person, message, time)); NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person) - .setGroupConversation(notificationCache.people.size() > 2) - .setConversationTitle("Chat with " + name); + .setGroupConversation(notificationCache.people.size() > 2 || !roomName.isEmpty()) + .setConversationTitle(conversationTitle); for (NotificationCache.Message cachedMessage : notificationCache.messages) { messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, cachedMessage.person); From 9aa6a0d738f4a7532dd62bd56baf1dcb92f12b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 12:28:17 +0200 Subject: [PATCH 006/181] No newlines between imports --- .../java/com/expensify/chat/CustomNotificationProvider.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 1c14180d6339..da8257dbab78 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -7,14 +7,12 @@ import android.util.Log; import android.util.TypedValue; import android.view.WindowManager; - import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.graphics.drawable.IconCompat; - import com.urbanairship.AirshipConfigOptions; import com.urbanairship.json.JsonMap; import com.urbanairship.json.JsonValue; @@ -22,7 +20,6 @@ import com.urbanairship.push.notifications.NotificationArguments; import com.urbanairship.reactnative.ReactNotificationProvider; import com.urbanairship.util.ImageUtils; - import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; From f8bbdb6e11093d49af685a6f0ed91555cb7428c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 13:02:44 +0200 Subject: [PATCH 007/181] No newlines between imports --- .../src/main/java/com/expensify/chat/CustomAirshipExtender.java | 2 -- .../java/com/expensify/chat/CustomNotificationListener.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java b/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java index 32fa546102cd..dd09a934fc79 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java +++ b/android/app/src/main/java/com/expensify/chat/CustomAirshipExtender.java @@ -1,9 +1,7 @@ package com.expensify.chat; import android.content.Context; - import androidx.annotation.NonNull; - import com.urbanairship.UAirship; import com.urbanairship.push.NotificationListener; import com.urbanairship.push.PushManager; diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java index 73d8a1031654..d6fc1f9e1a35 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationListener.java @@ -1,12 +1,10 @@ package com.expensify.chat; import androidx.annotation.NonNull; - import com.urbanairship.push.NotificationActionButtonInfo; import com.urbanairship.push.NotificationInfo; import com.urbanairship.push.NotificationListener; import com.urbanairship.push.PushMessage; - import org.jetbrains.annotations.NotNull; /** From 640b3be9cdc0de4f8de9d4b429770fea17bb118f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 13:07:54 +0200 Subject: [PATCH 008/181] Remove check for Date: Mon, 9 Aug 2021 13:16:15 +0200 Subject: [PATCH 009/181] Early return if reportID == -1 --- .../java/com/expensify/chat/CustomNotificationProvider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index d52d402b49e5..76ad327d554a 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -88,6 +88,10 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ */ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap payload, int notificationID) { int reportID = payload.get("reportID").getInt(-1); + if (reportID == -1) { + return; + } + NotificationCache notificationCache = findOrCreateNotificationCache(reportID); JsonMap reportAction = payload.get("reportAction").getMap(); String name = reportAction.get("person").getList().get(0).getMap().get("text").getString(); From 474203d6ca7dcf65cfb58d9fc4e30b9aa6a1b909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 13:22:55 +0200 Subject: [PATCH 010/181] Retrieve person from cache instead of creating a new one every time. --- .../com/expensify/chat/CustomNotificationProvider.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 76ad327d554a..99a3e14c5af5 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -105,14 +105,15 @@ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap paylo conversationTitle = "#" + roomName; } - IconCompat iconCompat = fetchIcon(avatar, FALLBACK_ICON_ID); - Person person = new Person.Builder() + Person person = notificationCache.people.get(accountID); + if (person == null) { + IconCompat iconCompat = fetchIcon(avatar, FALLBACK_ICON_ID); + person = new Person.Builder() .setIcon(iconCompat) .setKey(accountID) .setName(name) .build(); - if (!notificationCache.people.containsKey(accountID)) { notificationCache.people.put(accountID, person); } From a68b7441ac9a90806177ffc43d5a83cb8edaaa8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 16:24:45 +0200 Subject: [PATCH 011/181] Improve flow comments --- .../expensify/chat/CustomNotificationProvider.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 99a3e14c5af5..36200c3c4c1c 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -105,6 +105,7 @@ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap paylo conversationTitle = "#" + roomName; } + // Retrieve or create the Person object who sent the latest report comment Person person = notificationCache.people.get(accountID); if (person == null) { IconCompat iconCompat = fetchIcon(avatar, FALLBACK_ICON_ID); @@ -117,21 +118,34 @@ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap paylo notificationCache.people.put(accountID, person); } + // Store the latest report comment in the local conversation history notificationCache.messages.add(new NotificationCache.Message(person, message, time)); + // Create the messaging style notification builder for this notification. + // Associate the notification with the person who sent the report comment. + // If this conversation has 2 participants or more and there's no room name, we should mark + // it as a group conversation. + // Also set the conversation title. NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person) .setGroupConversation(notificationCache.people.size() > 2 || !roomName.isEmpty()) .setConversationTitle(conversationTitle); + // Add all conversation messages to the notification, including the last one we just received. for (NotificationCache.Message cachedMessage : notificationCache.messages) { messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, cachedMessage.person); } + // Clear the previous notification associated to this conversation so it looks like we are + // replacing them with this new one we just built. if (notificationCache.prevNotificationID != -1) { NotificationManagerCompat.from(context).cancel(notificationCache.prevNotificationID); } + // Apply the messaging style to the notification builder builder.setStyle(messagingStyle); + + // Store the new notification ID so we can replace the notification if this conversation + // receives more messages notificationCache.prevNotificationID = notificationID; } From c105f86ae84cba52d95817cb8429aa92bfe7f3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 16:28:50 +0200 Subject: [PATCH 012/181] Log the Push notification SendID --- .../java/com/expensify/chat/CustomNotificationProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 36200c3c4c1c..78ded2c3c344 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -71,7 +71,7 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @ applyMessageStyle(builder, payload, arguments.getNotificationId()); } } catch (Exception e) { - Log.e(TAG, "Failed to parse conversation", e); + Log.e(TAG, "Failed to parse conversation. SendID=" + message.getSendId(), e); } } From 48a52184dd234a66fa930e70bdb13f44474bac77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 16:30:05 +0200 Subject: [PATCH 013/181] Improve Log.e message and error --- .../java/com/expensify/chat/CustomNotificationProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 78ded2c3c344..906914969a9b 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -184,7 +184,7 @@ public void onDismissNotification(PushMessage message) { cache.remove(reportID); } catch (Exception e) { - Log.e(TAG, "Failed to delete conversation cache"); + Log.e(TAG, "Failed to delete conversation cache. SendID=" + message.getSendId(), e); } } From 6b61b147a53dd4a17d6768541020a268a9c94e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 16:34:03 +0200 Subject: [PATCH 014/181] Remove FALLBACK_ICON_ID as we don't have a fallback icon --- .../expensify/chat/CustomNotificationProvider.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 906914969a9b..d32e2975944b 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -6,7 +6,6 @@ import android.util.Log; import android.util.TypedValue; import android.view.WindowManager; -import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; @@ -38,9 +37,6 @@ public class CustomNotificationProvider extends ReactNotificationProvider { // to ensure the notification builds. private static final int MAX_ICON_FETCH_WAIT_TIME_SECONDS = 8; - // Fallback drawable ID. 0 to not use a fallback ID. - private static final int FALLBACK_ICON_ID = 0; - // Logging private static final String TAG = "NotificationProvider"; @@ -108,7 +104,7 @@ private void applyMessageStyle(NotificationCompat.Builder builder, JsonMap paylo // Retrieve or create the Person object who sent the latest report comment Person person = notificationCache.people.get(accountID); if (person == null) { - IconCompat iconCompat = fetchIcon(avatar, FALLBACK_ICON_ID); + IconCompat iconCompat = fetchIcon(avatar); person = new Person.Builder() .setIcon(iconCompat) .setKey(accountID) @@ -188,8 +184,7 @@ public void onDismissNotification(PushMessage message) { } } - @NonNull - private IconCompat fetchIcon(@NonNull String urlString, @DrawableRes int fallbackId) { + private IconCompat fetchIcon(String urlString) { // TODO: Add disk LRU cache URL parsedUrl = null; @@ -222,7 +217,7 @@ private IconCompat fetchIcon(@NonNull String urlString, @DrawableRes int fallbac } } - return fallbackId == 0 ? null : IconCompat.createWithResource(context, fallbackId); + return null; } private static class NotificationCache { From fb98d879edc6cc7f261ac8ad35abb29caf9c5ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Mon, 9 Aug 2021 16:35:46 +0200 Subject: [PATCH 015/181] Return early instead of wrap everything in an if block --- .../chat/CustomNotificationProvider.java | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index d32e2975944b..83ba99bd9705 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -194,27 +194,29 @@ private IconCompat fetchIcon(String urlString) { Log.e(TAG, "Failed to resolve URL " + urlString, e); } - if (parsedUrl != null) { - WindowManager window = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - DisplayMetrics dm = new DisplayMetrics(); - window.getDefaultDisplay().getMetrics(dm); + if (parsedUrl == null) { + return null; + } - final int reqWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MAX_ICON_SIZE_DPS, dm); - final int reqHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MAX_ICON_SIZE_DPS, dm); + WindowManager window = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics dm = new DisplayMetrics(); + window.getDefaultDisplay().getMetrics(dm); - final URL url = parsedUrl; - Future future = executorService.submit(() -> ImageUtils.fetchScaledBitmap(context, url, reqWidth, reqHeight)); + final int reqWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MAX_ICON_SIZE_DPS, dm); + final int reqHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MAX_ICON_SIZE_DPS, dm); - try { - Bitmap bitmap = future.get(MAX_ICON_FETCH_WAIT_TIME_SECONDS, TimeUnit.SECONDS); - return IconCompat.createWithBitmap(bitmap); - } catch (InterruptedException e) { - Log.e(TAG,"Failed to fetch icon", e); - Thread.currentThread().interrupt(); - } catch (Exception e) { - Log.e(TAG,"Failed to fetch icon", e); - future.cancel(true); - } + final URL url = parsedUrl; + Future future = executorService.submit(() -> ImageUtils.fetchScaledBitmap(context, url, reqWidth, reqHeight)); + + try { + Bitmap bitmap = future.get(MAX_ICON_FETCH_WAIT_TIME_SECONDS, TimeUnit.SECONDS); + return IconCompat.createWithBitmap(bitmap); + } catch (InterruptedException e) { + Log.e(TAG,"Failed to fetch icon", e); + Thread.currentThread().interrupt(); + } catch (Exception e) { + Log.e(TAG,"Failed to fetch icon", e); + future.cancel(true); } return null; From 0c938c247accf95f36d3be0a620e3c1dbad99f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horus=20Lugo=20L=C3=B3pez?= Date: Wed, 11 Aug 2021 10:20:32 +0200 Subject: [PATCH 016/181] Remove TODO --- .../java/com/expensify/chat/CustomNotificationProvider.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java index 83ba99bd9705..d3efa5bc6196 100644 --- a/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/CustomNotificationProvider.java @@ -185,8 +185,6 @@ public void onDismissNotification(PushMessage message) { } private IconCompat fetchIcon(String urlString) { - // TODO: Add disk LRU cache - URL parsedUrl = null; try { parsedUrl = urlString == null ? null : new URL(urlString); From 12a5884f053abb85a94f3dd21f0b09f9247ccca6 Mon Sep 17 00:00:00 2001 From: Nikki Wines Date: Thu, 2 Sep 2021 17:14:24 -0700 Subject: [PATCH 017/181] use message.text not message.html for updated report actions --- src/libs/actions/Report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ee9cae52188b..e52932f83059 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -525,7 +525,7 @@ function updateReportActionMessage(reportID, sequenceNumber, message) { // If this is the most recent message, update the lastMessageText in the report object as well if (sequenceNumber === reportMaxSequenceNumbers[reportID]) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - lastMessageText: message.html, + lastMessageText: message.text, }); } } From dcfb429699ed6f9a5082997935e6b3c1b98292f5 Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Tue, 7 Sep 2021 15:30:07 +0100 Subject: [PATCH 018/181] create test with desired format --- tests/unit/GithubUtilsTest.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/unit/GithubUtilsTest.js b/tests/unit/GithubUtilsTest.js index 5ef465413091..75983a517ae3 100644 --- a/tests/unit/GithubUtilsTest.js +++ b/tests/unit/GithubUtilsTest.js @@ -287,11 +287,22 @@ describe('GithubUtils', () => { // eslint-disable-next-line max-len const baseExpectedOutput = `**Release Version:** \`${tag}\`\r\n**Compare Changes:** https://github.com/Expensify/App/compare/production...staging\r\n\r\n**This release contains changes from the following pull requests:**\r\n`; - const openCheckbox = '- [ ]'; - const closedCheckbox = '- [x]'; - + const openCheckbox = ' - [ ]'; + const closedCheckbox = ' - [x]'; + const QA = ' QA' + const accessibility = ' Accessibility' const ccApplauseLeads = '\r\ncc @Expensify/applauseleads\r\n'; + test('new formatting', () => { + githubUtils.generateStagingDeployCashBody(tag, basePRList) + .then((issueBody) => { + expect(issueBody).toBe( + `${baseExpectedOutput}\r\n + - ${basePRList[3]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}\r\n\r\n- ${basePRList[0]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}\r\n\r\n- ${basePRList[1]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}\r\n${ccApplauseLeads}` + ); + }) + }); + test('Test no verified PRs', () => ( githubUtils.generateStagingDeployCashBody(tag, basePRList) .then((issueBody) => { From 821c37126852129ad3ce24d003407ecb10160ef4 Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Tue, 7 Sep 2021 16:38:27 +0100 Subject: [PATCH 019/181] implement new issue formatting, to match test --- .github/libs/GithubUtils.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index e3ecb66c2e5a..07c332aced18 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -223,10 +223,11 @@ class GithubUtils { // PR list if (!_.isEmpty(sortedPRList)) { - issueBody += '\r\n**This release contains changes from the following pull requests:**\r\n'; + issueBody += '\r\n**This release contains changes from the following pull requests:**'; _.each(sortedPRList, (URL) => { - issueBody += _.contains(verifiedPRList, URL) ? '- [x]' : '- [ ]'; - issueBody += ` ${URL}\r\n`; + issueBody += `\r\n\r\n- ${URL}`; + issueBody += _.contains(verifiedPRList, URL) ? '\r\n - [x] QA' : '\r\n - [ ] QA'; + issueBody += '\r\n - [ ] Accessibility'; }); } @@ -234,12 +235,13 @@ class GithubUtils { if (!_.isEmpty(deployBlockers)) { issueBody += '\r\n**Deploy Blockers:**\r\n'; _.each(sortedDeployBlockers, (URL) => { - issueBody += _.contains(resolvedDeployBlockers, URL) ? '- [x]' : '- [ ]'; - issueBody += ` ${URL}\r\n`; + issueBody += `\r\n\r\n- ${URL}`; + issueBody += _.contains(resolvedDeployBlockers, URL) ? '\r\n - [x] QA' : '\r\n - [ ] QA'; + issueBody += '\r\n - [ ] Accessibility'; }); } - issueBody += '\r\ncc @Expensify/applauseleads\r\n'; + issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n'; return issueBody; }) .catch(err => console.warn( From 90112c509b72f8eed746c7ff75d6568922a2c8ce Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Tue, 7 Sep 2021 16:38:55 +0100 Subject: [PATCH 020/181] update first 3 tests using the new formatting, replace example test --- tests/unit/GithubUtilsTest.js | 41 +++++++++++++++-------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/tests/unit/GithubUtilsTest.js b/tests/unit/GithubUtilsTest.js index 75983a517ae3..24d944fd5626 100644 --- a/tests/unit/GithubUtilsTest.js +++ b/tests/unit/GithubUtilsTest.js @@ -289,42 +289,37 @@ describe('GithubUtils', () => { const baseExpectedOutput = `**Release Version:** \`${tag}\`\r\n**Compare Changes:** https://github.com/Expensify/App/compare/production...staging\r\n\r\n**This release contains changes from the following pull requests:**\r\n`; const openCheckbox = ' - [ ]'; const closedCheckbox = ' - [x]'; + const listStart = '- ' const QA = ' QA' const accessibility = ' Accessibility' - const ccApplauseLeads = '\r\ncc @Expensify/applauseleads\r\n'; + const ccApplauseLeads = 'cc @Expensify/applauseleads\r\n'; + const deployBlockerHeader = '\r\n**Deploy Blockers:**\r\n'; - test('new formatting', () => { - githubUtils.generateStagingDeployCashBody(tag, basePRList) - .then((issueBody) => { - expect(issueBody).toBe( - `${baseExpectedOutput}\r\n - - ${basePRList[3]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}\r\n\r\n- ${basePRList[0]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}\r\n\r\n- ${basePRList[1]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}\r\n${ccApplauseLeads}` - ); - }) - }); - - test('Test no verified PRs', () => ( - githubUtils.generateStagingDeployCashBody(tag, basePRList) - .then((issueBody) => { - // eslint-disable-next-line max-len - expect(issueBody).toBe(`${baseExpectedOutput}${openCheckbox} ${basePRList[3]}\r\n${openCheckbox} ${basePRList[0]}\r\n${openCheckbox} ${basePRList[1]}\r\n${ccApplauseLeads}`); - }) - )); + // Valid output which will be reused in the deploy blocker tests + const allVerifiedExpectedOutput = `${baseExpectedOutput}` + + `\r\n${listStart}${basePRList[3]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + + `\r\n\r\n${listStart}${basePRList[0]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + + `\r\n\r\n${listStart}${basePRList[1]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}`; test('Test some verified PRs', () => ( githubUtils.generateStagingDeployCashBody(tag, basePRList, [basePRList[0]]) .then((issueBody) => { - // eslint-disable-next-line max-len - expect(issueBody).toBe(`${baseExpectedOutput}${openCheckbox} ${basePRList[3]}\r\n${closedCheckbox} ${basePRList[0]}\r\n${openCheckbox} ${basePRList[1]}\r\n${ccApplauseLeads}`); + expect(issueBody).toBe( + `${baseExpectedOutput}` + + `\r\n${listStart}${basePRList[3]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + + `\r\n\r\n${listStart}${basePRList[0]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + + `\r\n\r\n${listStart}${basePRList[1]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + + `\r\n\r\n${ccApplauseLeads}` + ); }) )); - // eslint-disable-next-line max-len - const allVerifiedExpectedOutput = `${baseExpectedOutput}${closedCheckbox} ${basePRList[3]}\r\n${closedCheckbox} ${basePRList[0]}\r\n${closedCheckbox} ${basePRList[1]}\r\n`; test('Test all verified PRs', () => ( githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList) .then((issueBody) => { - expect(issueBody).toBe(`${allVerifiedExpectedOutput}${ccApplauseLeads}`); + expect(issueBody).toBe( + `${allVerifiedExpectedOutput}\r\n\r\n${ccApplauseLeads}` + ); }) )); From c006880a0f97fc400864a31061954557a2c72276 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 8 Sep 2021 13:38:33 +0530 Subject: [PATCH 021/181] fix: navigation for exisiting report screen --- src/libs/Navigation/CustomActions.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/CustomActions.js b/src/libs/Navigation/CustomActions.js index 5f8762600acf..00569b0c9f9e 100644 --- a/src/libs/Navigation/CustomActions.js +++ b/src/libs/Navigation/CustomActions.js @@ -24,7 +24,19 @@ function pushDrawerRoute(screenName, params, navigationRef) { if (activeReportID === params.reportID) { if (state.type !== 'drawer') { - navigationRef.current.dispatch(StackActions.pop()); + navigationRef.current.dispatch(() => { + // If there are multiple routes then we can pop back to the first route + if (state.routes.length > 1) { + return StackActions.popToTop(); + } + + // Otherwise, we are already on the last page of a modal so just do nothing here as goBack() will navigate us + // back to the screen we were on before we opened the modal. + return StackActions.pop(0); + }); + if (navigationRef.current.canGoBack()) { + navigationRef.current.goBack(); + } } return DrawerActions.closeDrawer(); } From e1fa3f5ebce7f94756411053b601098242d46a45 Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Wed, 8 Sep 2021 14:39:39 +0100 Subject: [PATCH 022/181] modify new formatting of deploy blocker header --- .github/libs/GithubUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index 07c332aced18..996e9c474485 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -233,7 +233,7 @@ class GithubUtils { // Deploy blockers if (!_.isEmpty(deployBlockers)) { - issueBody += '\r\n**Deploy Blockers:**\r\n'; + issueBody += '\r\n\r\n\r\n**Deploy Blockers:**'; _.each(sortedDeployBlockers, (URL) => { issueBody += `\r\n\r\n- ${URL}`; issueBody += _.contains(resolvedDeployBlockers, URL) ? '\r\n - [x] QA' : '\r\n - [ ] QA'; From 2833a202d2966e7c7e9689125311a0a609bc47ec Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Wed, 8 Sep 2021 14:40:00 +0100 Subject: [PATCH 023/181] resolve remaining tests, improve test formatting --- tests/unit/GithubUtilsTest.js | 66 +++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/tests/unit/GithubUtilsTest.js b/tests/unit/GithubUtilsTest.js index 24d944fd5626..71e5dfd92c02 100644 --- a/tests/unit/GithubUtilsTest.js +++ b/tests/unit/GithubUtilsTest.js @@ -286,30 +286,46 @@ describe('GithubUtils', () => { ]; // eslint-disable-next-line max-len - const baseExpectedOutput = `**Release Version:** \`${tag}\`\r\n**Compare Changes:** https://github.com/Expensify/App/compare/production...staging\r\n\r\n**This release contains changes from the following pull requests:**\r\n`; + const baseExpectedOutput = `**Release Version:** \`${tag}\`\r\n**Compare Changes:** https://github.com/Expensify/App/compare/production...staging\r\n\r\n**This release contains changes from the following pull requests:**`; const openCheckbox = ' - [ ]'; const closedCheckbox = ' - [x]'; const listStart = '- ' const QA = ' QA' const accessibility = ' Accessibility' const ccApplauseLeads = 'cc @Expensify/applauseleads\r\n'; - const deployBlockerHeader = '\r\n**Deploy Blockers:**\r\n'; + const deployBlockerHeader = '\r\n**Deploy Blockers:**'; + const lineBreak = '\r\n' + const lineBreakDouble = '\r\n\r\n' // Valid output which will be reused in the deploy blocker tests const allVerifiedExpectedOutput = `${baseExpectedOutput}` + - `\r\n${listStart}${basePRList[3]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + - `\r\n\r\n${listStart}${basePRList[0]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + - `\r\n\r\n${listStart}${basePRList[1]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}`; + `${lineBreakDouble}${listStart}${basePRList[3]}${lineBreak}${closedCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${basePRList[0]}${lineBreak}${closedCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${closedCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + + test('Test no verified PRs', () => ( + githubUtils.generateStagingDeployCashBody(tag, basePRList) + .then((issueBody) => { + expect(issueBody).toBe( + `${baseExpectedOutput}` + + `${lineBreakDouble}${listStart}${basePRList[3]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${basePRList[0]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${ccApplauseLeads}` + ); + }) + )); test('Test some verified PRs', () => ( githubUtils.generateStagingDeployCashBody(tag, basePRList, [basePRList[0]]) .then((issueBody) => { expect(issueBody).toBe( `${baseExpectedOutput}` + - `\r\n${listStart}${basePRList[3]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + - `\r\n\r\n${listStart}${basePRList[0]}\r\n${closedCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + - `\r\n\r\n${listStart}${basePRList[1]}\r\n${openCheckbox}${QA}\r\n${openCheckbox}${accessibility}` + - `\r\n\r\n${ccApplauseLeads}` + `${lineBreakDouble}${listStart}${basePRList[3]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${basePRList[0]}${lineBreak}${closedCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${ccApplauseLeads}` ); }) )); @@ -318,35 +334,47 @@ describe('GithubUtils', () => { githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList) .then((issueBody) => { expect(issueBody).toBe( - `${allVerifiedExpectedOutput}\r\n\r\n${ccApplauseLeads}` + `${allVerifiedExpectedOutput}${lineBreakDouble}${ccApplauseLeads}` ); }) )); - const deployBlockerHeader = '\r\n**Deploy Blockers:**\r\n'; test('Test no resolved deploy blockers', () => ( githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList) .then((issueBody) => { - // eslint-disable-next-line max-len - expect(issueBody).toBe(`${allVerifiedExpectedOutput}${deployBlockerHeader}${openCheckbox} ${baseDeployBlockerList[0]}\r\n${openCheckbox} ${baseDeployBlockerList[1]}\r\n${ccApplauseLeads}`); + expect(issueBody).toBe( + `${allVerifiedExpectedOutput}` + + `${lineBreakDouble}${deployBlockerHeader}` + + `${lineBreakDouble}${listStart}${baseDeployBlockerList[0]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${baseDeployBlockerList[1]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${ccApplauseLeads}` + ); }) )); test('Test some resolved deploy blockers', () => ( - // eslint-disable-next-line max-len githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, [baseDeployBlockerList[0]]) .then((issueBody) => { - // eslint-disable-next-line max-len - expect(issueBody).toBe(`${allVerifiedExpectedOutput}${deployBlockerHeader}${closedCheckbox} ${baseDeployBlockerList[0]}\r\n${openCheckbox} ${baseDeployBlockerList[1]}\r\n${ccApplauseLeads}`); + expect(issueBody).toBe( + `${allVerifiedExpectedOutput}` + + `${lineBreakDouble}${deployBlockerHeader}` + + `${lineBreakDouble}${listStart}${baseDeployBlockerList[0]}${lineBreak}${closedCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${baseDeployBlockerList[1]}${lineBreak}${openCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${ccApplauseLeads}` + ); }) )); test('Test all resolved deploy blockers', () => ( - // eslint-disable-next-line max-len githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, baseDeployBlockerList) .then((issueBody) => { - // eslint-disable-next-line max-len - expect(issueBody).toBe(`${allVerifiedExpectedOutput}${deployBlockerHeader}${closedCheckbox} ${baseDeployBlockerList[0]}\r\n${closedCheckbox} ${baseDeployBlockerList[1]}\r\n${ccApplauseLeads}`); + expect(issueBody).toBe( + `${allVerifiedExpectedOutput}` + + `${lineBreakDouble}${deployBlockerHeader}` + + `${lineBreakDouble}${listStart}${baseDeployBlockerList[0]}${lineBreak}${closedCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${listStart}${baseDeployBlockerList[1]}${lineBreak}${closedCheckbox}${QA}${lineBreak}${openCheckbox}${accessibility}` + + `${lineBreakDouble}${ccApplauseLeads}` + ); }) )); }); From e6befa3e18668aad2581a41d9be033f5f481b65d Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Wed, 8 Sep 2021 17:23:59 +0100 Subject: [PATCH 024/181] attempt to preserve the accessibility status --- .../createOrUpdateStagingDeploy.js | 6 ++++-- .github/libs/GithubUtils.js | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js b/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js index 10b2decd2b81..f24d9aacb21d 100644 --- a/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js +++ b/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js @@ -101,8 +101,9 @@ const run = function () { // Since this is the second argument to _.union, // it will appear later in the array than any duplicate. // Since it is later in the array, it will be truncated by _.unique, - // and the original value of isVerified will be preserved. + // and the original value of isVerified and isAccessible will be preserved. isVerified: false, + isAccessible: false, }))), false, item => item.number, @@ -123,7 +124,8 @@ const run = function () { return GithubUtils.generateStagingDeployCashBody( tag, _.pluck(PRList, 'url'), - _.pluck(_.where(PRList, {isVerified: true}), 'url'), + _.pluck(_.where(PRList, {isVerified: true,}), 'url'), + _.pluck(_.where(PRList, {isAccessible: true,}), 'url'), _.pluck(deployBlockers, 'url'), _.pluck(_.where(deployBlockers, {isResolved: true}), 'url'), ); diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index 996e9c474485..32b901e446a9 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -189,6 +189,7 @@ class GithubUtils { * @param {String} tag * @param {Array} PRList - The list of PR URLs which are included in this StagingDeployCash * @param {Array} [verifiedPRList] - The list of PR URLs which have passed QA. + * @param {Array} [accessabilityPRList] - The list of PR URLs which have passed the accessability check. * @param {Array} [deployBlockers] - The list of DeployBlocker URLs. * @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved. * @returns {Promise} @@ -197,6 +198,7 @@ class GithubUtils { tag, PRList, verifiedPRList = [], + accessabilityPRList = [], deployBlockers = [], resolvedDeployBlockers = [], ) { @@ -227,7 +229,7 @@ class GithubUtils { _.each(sortedPRList, (URL) => { issueBody += `\r\n\r\n- ${URL}`; issueBody += _.contains(verifiedPRList, URL) ? '\r\n - [x] QA' : '\r\n - [ ] QA'; - issueBody += '\r\n - [ ] Accessibility'; + issueBody += _.contains(accessabilityPRList, URL) ? '\r\n - [x] Accessibility' : '\r\n - [ ] Accessibility'; }); } @@ -237,7 +239,7 @@ class GithubUtils { _.each(sortedDeployBlockers, (URL) => { issueBody += `\r\n\r\n- ${URL}`; issueBody += _.contains(resolvedDeployBlockers, URL) ? '\r\n - [x] QA' : '\r\n - [ ] QA'; - issueBody += '\r\n - [ ] Accessibility'; + issueBody += _.contains(accessabilityPRList, URL) ? '\r\n - [x] Accessibility' : '\r\n - [ ] Accessibility'; }); } From df0ae389d875058800631ca8041825e3e462e1e1 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 8 Sep 2021 23:29:48 +0530 Subject: [PATCH 025/181] fix: no result found issue --- src/pages/NewChatPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 00075f1d5d0f..860dea2b0799 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -129,7 +129,7 @@ class NewChatPage extends Component { render() { const sections = this.getSections(); const headerMessage = getHeaderMessage( - this.state.personalDetails.length !== 0, + (this.state.recentReports.length + this.state.personalDetails.length) !== 0, Boolean(this.state.userToInvite), this.state.searchValue, ); From 3a3bbd2cebb061c7c55ff4926706f5ebaec6360f Mon Sep 17 00:00:00 2001 From: Santhoshkumar Sellavel Date: Wed, 15 Sep 2021 01:09:42 +0530 Subject: [PATCH 026/181] IOU Participant page search validation --- src/libs/OptionsListUtils.js | 2 +- .../steps/IOUParticipantsPage/IOUParticipantsRequest.js | 8 +++++++- .../iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 1ad67c2236b8..bc0d142dfc91 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -719,7 +719,7 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma return translate(preferredLocale, 'messages.noPhoneNumber'); } - if (!hasSelectableOptions && !hasUserToInvite) { + if (searchValue && !hasSelectableOptions && !hasUserToInvite) { if (/^\d+$/.test(searchValue)) { return translate(preferredLocale, 'messages.noPhoneNumber'); } diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js index ead69898fdad..d48a4eb87237 100755 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import {getNewChatOptions, isCurrentUser} from '../../../../libs/OptionsListUtils'; +import {getHeaderMessage, getNewChatOptions, isCurrentUser} from '../../../../libs/OptionsListUtils'; import OptionsSelector from '../../../../components/OptionsSelector'; import ONYXKEYS from '../../../../ONYXKEYS'; import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; @@ -115,6 +115,11 @@ class IOUParticipantsRequest extends Component { render() { const sections = this.getSections(); + const headerMessage = getHeaderMessage( + this.state.personalDetails.length !== 0, + Boolean(this.state.userToInvite), + this.state.searchValue, + ); return ( @@ -230,6 +235,7 @@ class IOUParticipantsSplit extends Component { personalDetails, }); }} + headerMessage={headerMessage} disableArrowKeysActions hideAdditionalOptionStates forceTextUnreadStyle From 28533f6073f0ea59fa4b0904ab877fd11bb50b58 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 15 Sep 2021 03:33:22 +0530 Subject: [PATCH 027/181] refactor --- src/libs/Navigation/CustomActions.js | 51 +++++++++++++++++++++------- src/libs/Navigation/Navigation.js | 35 ++----------------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/libs/Navigation/CustomActions.js b/src/libs/Navigation/CustomActions.js index 00569b0c9f9e..7b822acd8d96 100644 --- a/src/libs/Navigation/CustomActions.js +++ b/src/libs/Navigation/CustomActions.js @@ -1,6 +1,42 @@ import {CommonActions, StackActions, DrawerActions} from '@react-navigation/native'; import lodashGet from 'lodash/get'; +/** + * Go back to the Main Drawer + * @param {Object} navigationRef + */ +function navigateBackToDrawer(navigationRef) { + let isLeavingDrawerNavigator = false; + + // This should take us to the first view of the modal's stack navigator + navigationRef.current.dispatch((state) => { + // If this is a nested drawer navigator then we pop the screen and + // prevent calling goBack() as it's default behavior is to toggle open the active drawer + if (state.type === 'drawer') { + isLeavingDrawerNavigator = true; + return StackActions.pop(); + } + + // If there are multiple routes then we can pop back to the first route + if (state.routes.length > 1) { + return StackActions.popToTop(); + } + + // Otherwise, we are already on the last page of a modal so just do nothing here as goBack() will navigate us + // back to the screen we were on before we opened the modal. + return StackActions.pop(0); + }); + + if (isLeavingDrawerNavigator) { + return; + } + + // Navigate back to where we were before we launched the modal + if (navigationRef.current.canGoBack()) { + navigationRef.current.goBack(); + } +} + /** * In order to create the desired browser navigation behavior on web and mobile web we need to replace any * type: 'drawer' routes with a type: 'route' so that when pushing to a report screen we never show the @@ -24,19 +60,7 @@ function pushDrawerRoute(screenName, params, navigationRef) { if (activeReportID === params.reportID) { if (state.type !== 'drawer') { - navigationRef.current.dispatch(() => { - // If there are multiple routes then we can pop back to the first route - if (state.routes.length > 1) { - return StackActions.popToTop(); - } - - // Otherwise, we are already on the last page of a modal so just do nothing here as goBack() will navigate us - // back to the screen we were on before we opened the modal. - return StackActions.pop(0); - }); - if (navigationRef.current.canGoBack()) { - navigationRef.current.goBack(); - } + navigateBackToDrawer(navigationRef); } return DrawerActions.closeDrawer(); } @@ -64,4 +88,5 @@ function pushDrawerRoute(screenName, params, navigationRef) { export default { pushDrawerRoute, + navigateBackToDrawer, }; diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index ff55c65c5e95..f1ac85fb93ac 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -114,39 +114,10 @@ function dismissModal(shouldOpenDrawer = false) { ? shouldOpenDrawer : false; - let isLeavingDrawerNavigator; - - // This should take us to the first view of the modal's stack navigator - navigationRef.current.dispatch((state) => { - // If this is a nested drawer navigator then we pop the screen and - // prevent calling goBack() as it's default behavior is to toggle open the active drawer - if (state.type === 'drawer') { - isLeavingDrawerNavigator = true; - return StackActions.pop(); - } - - // If there are multiple routes then we can pop back to the first route - if (state.routes.length > 1) { - return StackActions.popToTop(); - } - - // Otherwise, we are already on the last page of a modal so just do nothing here as goBack() will navigate us - // back to the screen we were on before we opened the modal. - return StackActions.pop(0); - }); - - if (isLeavingDrawerNavigator) { - return; + CustomActions.navigateBackToDrawer(navigationRef); + if (normalizedShouldOpenDrawer) { + openDrawer(); } - - // Navigate back to where we were before we launched the modal - goBack(shouldOpenDrawer); - - if (!normalizedShouldOpenDrawer) { - return; - } - - openDrawer(); } /** From 0c29d6336c1d6aecebea453e3e8c81c60b8f1da2 Mon Sep 17 00:00:00 2001 From: Prashant Mangukiya Date: Wed, 15 Sep 2021 21:30:12 +0530 Subject: [PATCH 028/181] Cursor focus set after selecting new currency to enter amount --- src/pages/iou/steps/IOUAmountPage.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js index b3b7b8655a87..0fbea5c92975 100755 --- a/src/pages/iou/steps/IOUAmountPage.js +++ b/src/pages/iou/steps/IOUAmountPage.js @@ -76,6 +76,7 @@ class IOUAmountPage extends React.Component { this.updateAmountNumberPad = this.updateAmountNumberPad.bind(this); this.updateAmount = this.updateAmount.bind(this); this.stripCommaFromAmount = this.stripCommaFromAmount.bind(this); + this.focusTextInput = this.focusTextInput.bind(this); this.state = { amount: props.selectedAmount, @@ -83,7 +84,20 @@ class IOUAmountPage extends React.Component { } componentDidMount() { - // Component is not initialized yet due to navigation transitions + this.focusTextInput(); + } + + componentDidUpdate(prevProps) { + if (this.props.iou.selectedCurrencyCode !== prevProps.iou.selectedCurrencyCode) { + this.focusTextInput(); + } + } + + /** + * Focus text input + */ + focusTextInput() { + // Component may not initialized due to navigation transitions // Wait until interactions are complete before trying to focus InteractionManager.runAfterInteractions(() => { // Focus text input From 45ea56f3326c672f1e722fa1a72492daf24b524a Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 15 Sep 2021 23:59:53 +0530 Subject: [PATCH 029/181] fix: lag between language switch on login page --- src/libs/actions/Session.js | 11 +++++++++++ src/pages/signin/ChangeExpensifyLoginLink.js | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Session.js b/src/libs/actions/Session.js index e49e4d9b000c..0546ff3dda38 100644 --- a/src/libs/actions/Session.js +++ b/src/libs/actions/Session.js @@ -298,6 +298,16 @@ function continueSessionFromECom(accountID, validateCode, twoFactorAuthCode) { }); } +/** + * Clear the creadentials and partial sign in session so the user can taken back to first Login step + */ +function clearSignInData() { + Onyx.multiSet({ + [ONYXKEYS.ACCOUNT]: null, + [ONYXKEYS.CREDENTIALS]: null, + }); +} + export { continueSessionFromECom, fetchAccountDetails, @@ -307,4 +317,5 @@ export { reopenAccount, resendValidationLink, resetPassword, + clearSignInData, }; diff --git a/src/pages/signin/ChangeExpensifyLoginLink.js b/src/pages/signin/ChangeExpensifyLoginLink.js index 4fe1741e2f94..a8d3d16be345 100755 --- a/src/pages/signin/ChangeExpensifyLoginLink.js +++ b/src/pages/signin/ChangeExpensifyLoginLink.js @@ -5,11 +5,11 @@ import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; import Text from '../../components/Text'; import styles from '../../styles/styles'; -import redirectToSignIn from '../../libs/actions/SignInRedirect'; import themeColors from '../../styles/themes/default'; import ONYXKEYS from '../../ONYXKEYS'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; +import {clearSignInData} from '../../libs/actions/Session'; const propTypes = { /** The credentials of the logged in person */ @@ -33,7 +33,7 @@ const ChangeExpensifyLoginLink = ({credentials, translate, toLocalPhone}) => ( redirectToSignIn()} + onPress={clearSignInData} underlayColor={themeColors.componentBG} > From d09d16b9e1f5a1bc9ca46d17f70f7835f364ba85 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Thu, 16 Sep 2021 00:00:58 +0530 Subject: [PATCH 030/181] remove unused prop --- src/pages/signin/SignInPage.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index ce47478da5d3..9c49644fb817 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -39,18 +39,11 @@ const propTypes = { twoFactorAuthCode: PropTypes.string, }), - /** The session of the logged in person */ - session: PropTypes.shape({ - /** Error to display when there is a session error returned */ - authToken: PropTypes.string, - }), - ...withLocalizePropTypes, }; const defaultProps = { account: {}, - session: {}, credentials: {}, }; @@ -115,6 +108,5 @@ export default compose( withOnyx({ account: {key: ONYXKEYS.ACCOUNT}, credentials: {key: ONYXKEYS.CREDENTIALS}, - session: {key: ONYXKEYS.SESSION}, }), )(SignInPage); From 2de99d1d24f1599ccf3f721519a5481ea0465307 Mon Sep 17 00:00:00 2001 From: Manan Jadhav Date: Thu, 16 Sep 2021 01:01:00 +0530 Subject: [PATCH 031/181] fix(password-cursor): Added `blurOnSubmit` for PasswordForm --- src/pages/signin/PasswordForm.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index ff2b4cd69950..b40549e0429d 100755 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -92,6 +92,7 @@ class PasswordForm extends React.Component { onSubmitEditing={this.validateAndSubmitForm} autoFocus translateX={-18} + blurOnSubmit={false} /> )} From 28d36a4a038010162bce4b5a3fb3898053831c34 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Fri, 17 Sep 2021 17:19:20 +0530 Subject: [PATCH 032/181] typo --- src/libs/actions/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Session.js b/src/libs/actions/Session.js index 0546ff3dda38..9b9227229adb 100644 --- a/src/libs/actions/Session.js +++ b/src/libs/actions/Session.js @@ -299,7 +299,7 @@ function continueSessionFromECom(accountID, validateCode, twoFactorAuthCode) { } /** - * Clear the creadentials and partial sign in session so the user can taken back to first Login step + * Clear the credentials and partial sign in session so the user can taken back to first Login step */ function clearSignInData() { Onyx.multiSet({ From 2845143b309efe514fa142df57dfe4e871e344a8 Mon Sep 17 00:00:00 2001 From: Santhoshkumar Sellavel Date: Fri, 17 Sep 2021 21:52:42 +0530 Subject: [PATCH 033/181] Added a comment for the searchValue check --- src/libs/OptionsListUtils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index bc0d142dfc91..8c8f598caec3 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -719,6 +719,8 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma return translate(preferredLocale, 'messages.noPhoneNumber'); } + // Without a search value, it would be very confusing to see a search validation message. + // Therefore, this skips the validation when there is no search value. if (searchValue && !hasSelectableOptions && !hasUserToInvite) { if (/^\d+$/.test(searchValue)) { return translate(preferredLocale, 'messages.noPhoneNumber'); From e3083c341b9603cbf1cf93fde0e65d70648f301f Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 21 Sep 2021 01:00:45 +0530 Subject: [PATCH 034/181] fix: styles for ul & ol --- src/styles/styles.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/styles/styles.js b/src/styles/styles.js index 6c263bdbd6cd..f40165792f35 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1055,6 +1055,7 @@ const styles = { lineHeight: 20, marginTop: -2, marginBottom: -2, + maxWidth: '100%', ...whiteSpace.preWrap, ...wordBreak.breakWord, }, @@ -2051,6 +2052,16 @@ const webViewStyles = { a: styles.link, + ul: { + maxWidth: '100%', + flex: 1, + }, + + ol: { + maxWidth: '100%', + flex: 1, + }, + li: { flexShrink: 1, }, From 700669a205bf18a68f179cc278279a29e24f64e1 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Fri, 24 Sep 2021 03:56:00 +0530 Subject: [PATCH 035/181] refactor NewGroupPage --- .../AppNavigator/ModalStackNavigators.js | 6 +- src/libs/OptionsListUtils.js | 34 +---- src/pages/NewGroupPage.js | 132 +++++++++++------- .../IOUParticipantsRequest.js | 2 + tests/unit/OptionsListUtilsTest.js | 6 +- 5 files changed, 92 insertions(+), 88 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 9b47dfced209..02f66d91c26f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import React from 'react'; import {createStackNavigator, CardStyleInterpolators} from '@react-navigation/stack'; import styles from '../../../styles/styles'; -import NewChatPage from '../../../pages/NewChatPage'; import NewGroupPage from '../../../pages/NewGroupPage'; import SearchPage from '../../../pages/SearchPage'; import DetailsPage from '../../../pages/DetailsPage'; @@ -119,12 +118,13 @@ const SearchModalStackNavigator = createModalStackNavigator([{ }]); const NewGroupModalStackNavigator = createModalStackNavigator([{ - Component: NewGroupPage, + // eslint-disable-next-line react/jsx-props-no-spreading + Component: props => , name: 'NewGroup_Root', }]); const NewChatModalStackNavigator = createModalStackNavigator([{ - Component: NewChatPage, + Component: NewGroupPage, name: 'NewChat_Root', }]); diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 1ad67c2236b8..ad22b63d9d1c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -564,35 +564,6 @@ function getSearchOptions( }); } -/** - * Build the options for the New Chat view - * - * @param {Object} reports - * @param {Object} personalDetails - * @param {Array} betas - * @param {String} searchValue - * @param {Array} excludeLogins - * @returns {Object} - */ -function getNewChatOptions( - reports, - personalDetails, - betas = [], - searchValue = '', - excludeLogins = [], - -) { - return getOptions(reports, personalDetails, {}, 0, { - betas, - searchValue, - excludeDefaultRooms: true, - includePersonalDetails: true, - includeRecentReports: true, - maxRecentReportsToShow: 5, - excludeLogins, - }); -} - /** * Build the IOUConfirmation options for showing MyPersonalDetail * @@ -636,14 +607,13 @@ function getIOUConfirmationOptionsFromParticipants( * @param {Array} excludeLogins * @returns {Object} */ -function getNewGroupOptions( +function getNewChatOptions( reports, personalDetails, betas = [], searchValue = '', selectedOptions = [], excludeLogins = [], - ) { return getOptions(reports, personalDetails, {}, 0, { betas, @@ -652,7 +622,6 @@ function getNewGroupOptions( excludeDefaultRooms: true, includeRecentReports: true, includePersonalDetails: true, - includeMultipleParticipantReports: false, maxRecentReportsToShow: 5, excludeLogins, }); @@ -773,7 +742,6 @@ export { isCurrentUser, getSearchOptions, getNewChatOptions, - getNewGroupOptions, getSidebarOptions, getHeaderMessage, getPersonalDetailsForLogins, diff --git a/src/pages/NewGroupPage.js b/src/pages/NewGroupPage.js index 739acf7ade3b..8cf972848e82 100755 --- a/src/pages/NewGroupPage.js +++ b/src/pages/NewGroupPage.js @@ -4,7 +4,7 @@ import {View} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import OptionsSelector from '../components/OptionsSelector'; -import {getNewGroupOptions, getHeaderMessage} from '../libs/OptionsListUtils'; +import {getNewChatOptions, getHeaderMessage} from '../libs/OptionsListUtils'; import ONYXKEYS from '../ONYXKEYS'; import styles from '../styles/styles'; import {fetchOrCreateChatReport} from '../libs/actions/Report'; @@ -19,6 +19,7 @@ import compose from '../libs/compose'; import Button from '../components/Button'; import KeyboardAvoidingView from '../components/KeyboardAvoidingView'; import FixedFooter from '../components/FixedFooter'; +import KeyboardSpacer from '../components/KeyboardSpacer'; const personalDetailsPropTypes = PropTypes.shape({ /** The login of the person (either email or phone number) */ @@ -33,6 +34,9 @@ const personalDetailsPropTypes = PropTypes.shape({ }); const propTypes = { + /** Whether screen is used to create group chat */ + groupChat: PropTypes.bool, + /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -55,23 +59,30 @@ const propTypes = { ...withLocalizePropTypes, }; +const defaultProps = { + groupChat: false, +}; + class NewGroupPage extends Component { constructor(props) { super(props); this.toggleOption = this.toggleOption.bind(this); this.createGroup = this.createGroup.bind(this); + this.whenOptionSelected = this.whenOptionSelected.bind(this); + this.createNewChat = this.createNewChat.bind(this); + const { recentReports, personalDetails, userToInvite, - } = getNewGroupOptions( + } = getNewChatOptions( props.reports, props.personalDetails, props.betas, '', [], - EXCLUDED_GROUP_EMAILS, + this.props.groupChat ? EXCLUDED_GROUP_EMAILS : [], ); this.state = { searchValue: '', @@ -90,15 +101,17 @@ class NewGroupPage extends Component { */ getSections(maxParticipantsReached) { const sections = []; - sections.push({ - title: undefined, - data: this.state.selectedOptions, - shouldShow: true, - indexOffset: 0, - }); + if (this.props.groupChat) { + sections.push({ + title: undefined, + data: this.state.selectedOptions, + shouldShow: true, + indexOffset: 0, + }); - if (maxParticipantsReached) { - return sections; + if (maxParticipantsReached) { + return sections; + } } sections.push({ @@ -163,7 +176,7 @@ class NewGroupPage extends Component { recentReports, personalDetails, userToInvite, - } = getNewGroupOptions( + } = getNewChatOptions( this.props.reports, this.props.personalDetails, this.props.betas, @@ -182,11 +195,30 @@ class NewGroupPage extends Component { }); } + /** + * Creates a new chat with the option + * @param {Object} option + */ + createNewChat(option) { + fetchOrCreateChatReport([ + this.props.session.email, + option.login, + ]); + } + + whenOptionSelected(option) { + if (this.props.groupChat) { + return this.toggleOption(option); + } + + this.createNewChat(option); + } + render() { const maxParticipantsReached = this.state.selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const sections = this.getSections(maxParticipantsReached); const headerMessage = getHeaderMessage( - this.state.personalDetails.length + this.state.recentReports.length !== 0, + (this.state.personalDetails.length + this.state.recentReports.length) !== 0, Boolean(this.state.userToInvite), this.state.searchValue, maxParticipantsReached, @@ -196,48 +228,49 @@ class NewGroupPage extends Component { {({didScreenTransitionEnd}) => ( Navigation.dismissModal(true)} /> {didScreenTransitionEnd && ( <> - - { - const { - recentReports, - personalDetails, - userToInvite, - } = getNewGroupOptions( - this.props.reports, - this.props.personalDetails, - this.props.betas, - searchValue, - [], - EXCLUDED_GROUP_EMAILS, - ); - this.setState({ - searchValue, - userToInvite, - recentReports, - personalDetails, - }); - }} - headerMessage={headerMessage} - disableArrowKeysActions - hideAdditionalOptionStates - forceTextUnreadStyle - shouldFocusOnSelectRow - /> - - {this.state.selectedOptions?.length > 0 && ( + { + const { + recentReports, + personalDetails, + userToInvite, + } = getNewChatOptions( + this.props.reports, + this.props.personalDetails, + this.props.betas, + searchValue, + [], + this.props.groupChat ? EXCLUDED_GROUP_EMAILS : [], + ); + this.setState({ + searchValue, + userToInvite, + recentReports, + personalDetails, + }); + }} + headerMessage={headerMessage} + disableArrowKeysActions + hideAdditionalOptionStates + forceTextUnreadStyle + shouldFocusOnSelectRow + /> + {this.props.groupChat && } + {this.props.groupChat && this.state.selectedOptions?.length > 0 && (