Skip to content

Commit

Permalink
Merge branch 'main' into andrew-live-markdown-latest
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewGable committed Feb 9, 2024
2 parents 7b316e7 + c3555ed commit 8571ccf
Show file tree
Hide file tree
Showing 126 changed files with 1,982 additions and 1,230 deletions.
2 changes: 1 addition & 1 deletion .storybook/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ module.exports = ({config}) => {
config.resolve.alias = {
'react-native-config': 'react-web-config',
'react-native$': 'react-native-web',
'@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.js'),
'@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'),
'@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'),

// Module alias support for storybook files, coping from `webpack.common.js`
Expand Down
19 changes: 0 additions & 19 deletions __mocks__/@react-native-community/netinfo.js

This file was deleted.

31 changes: 31 additions & 0 deletions __mocks__/@react-native-community/netinfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {NetInfoCellularGeneration, NetInfoStateType} from '@react-native-community/netinfo';
import type {addEventListener, configure, fetch, NetInfoState, refresh, useNetInfo} from '@react-native-community/netinfo';

const defaultState: NetInfoState = {
type: NetInfoStateType.cellular,
isConnected: true,
isInternetReachable: true,
details: {
isConnectionExpensive: true,
cellularGeneration: NetInfoCellularGeneration['3g'],
carrier: 'T-Mobile',
},
};

type NetInfoMock = {
configure: typeof configure;
fetch: typeof fetch;
refresh: typeof refresh;
addEventListener: typeof addEventListener;
useNetInfo: typeof useNetInfo;
};

const netInfoMock: NetInfoMock = {
configure: () => {},
fetch: () => Promise.resolve(defaultState),
refresh: () => Promise.resolve(defaultState),
addEventListener: () => () => {},
useNetInfo: () => defaultState,
};

export default netInfoMock;
3 changes: 0 additions & 3 deletions __mocks__/react-native-device-info.js

This file was deleted.

6 changes: 6 additions & 0 deletions __mocks__/react-native-device-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type DeviceInfoModule from 'react-native-device-info';
import MockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock';

const DeviceInfo: typeof DeviceInfoModule = MockDeviceInfo;

export default DeviceInfo;
3 changes: 0 additions & 3 deletions __mocks__/react-native-localize.js

This file was deleted.

3 changes: 3 additions & 0 deletions __mocks__/react-native-localize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import mockRNLocalize from 'react-native-localize/mock';

module.exports = mockRNLocalize;
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
versionCode 1001043900
versionName "1.4.39-0"
versionCode 1001043901
versionName "1.4.39-1"
}

flavorDimensions "default"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,5 @@ public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship)

CustomNotificationProvider notificationProvider = new CustomNotificationProvider(context, airship.getAirshipConfigOptions());
pushManager.setNotificationProvider(notificationProvider);

NotificationListener notificationListener = airship.getPushManager().getNotificationListener();
pushManager.setNotificationListener(new CustomNotificationListener(notificationListener, notificationProvider));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
import static androidx.core.app.NotificationCompat.PRIORITY_MAX;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
Expand All @@ -15,6 +16,8 @@
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
Expand All @@ -26,6 +29,7 @@
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import androidx.core.graphics.drawable.IconCompat;
import androidx.versionedparcelable.ParcelUtils;

import com.urbanairship.AirshipConfigOptions;
import com.urbanairship.json.JsonMap;
Expand All @@ -40,18 +44,14 @@
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import com.expensify.chat.customairshipextender.NotificationCache.NotificationData;
import com.expensify.chat.customairshipextender.NotificationCache.NotificationMessage;

public class CustomNotificationProvider extends ReactNotificationProvider {
// Resize icons to 100 dp x 100 dp
private static final int MAX_ICON_SIZE_DPS = 100;
Expand All @@ -73,6 +73,13 @@ public class CustomNotificationProvider extends ReactNotificationProvider {
private static final String PAYLOAD_KEY = "payload";
private static final String ONYX_DATA_KEY = "onyxData";

// Notification extras keys
public static final String EXTRAS_REPORT_ID_KEY = "reportID";
public static final String EXTRAS_AVATAR_KEY = "avatar";
public static final String EXTRAS_NAME_KEY = "name";
public static final String EXTRAS_ACCOUNT_ID_KEY = "accountID";


private final ExecutorService executorService = Executors.newCachedThreadPool();

public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) {
Expand Down Expand Up @@ -158,7 +165,7 @@ public Bitmap getCroppedBitmap(Bitmap bitmap) {

/**
* Applies the message style to the notification builder. It also takes advantage of the
* notification cache to build conversations style notifications.
* android notification API to build conversations style notifications.
*
* @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.
Expand All @@ -170,10 +177,9 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
return;
}

// Retrieve and check for cached notifications
NotificationData notificationData = NotificationCache.getNotificationData(reportID);
boolean hasExistingNotification = notificationData.messages.size() >= 1;

// Retrieve and check for existing notifications
StatusBarNotification existingReportNotification = getActiveNotificationByReportId(context, reportID);
boolean hasExistingNotification = existingReportNotification != null;
try {
JsonMap reportMap = payload.get(ONYX_DATA_KEY).getList().get(1).getMap().get("value").getMap();
String reportId = reportMap.keySet().iterator().next();
Expand All @@ -187,31 +193,15 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
String message = alert != null ? alert : messageData.get("message").getList().get(0).getMap().get("text").getString();
String conversationName = payload.get("roomName") == null ? "" : payload.get("roomName").getString("");

// Retrieve or create the Person object who sent the latest report comment
Person person = notificationData.getPerson(accountID);
Bitmap personIcon = notificationData.getIcon(accountID);

if (personIcon == null) {
personIcon = fetchIcon(context, avatar);
}
// create the Person object who sent the latest report comment
Bitmap personIcon = fetchIcon(context, avatar);
builder.setLargeIcon(personIcon);

// Persist the person and icon to the notification cache
if (person == null) {
IconCompat iconCompat = IconCompat.createWithBitmap(personIcon);
person = new Person.Builder()
.setIcon(iconCompat)
.setKey(accountID)
.setName(name)
.build();

notificationData.putPerson(accountID, name, personIcon);
}
Person person = createMessagePersonObject(IconCompat.createWithBitmap(personIcon), accountID, name);

// Despite not using conversation style for the initial notification from each chat, we need to cache it to enable conversation style for future notifications
// Create latest received message object
long createdTimeInMillis = getMessageTimeInMillis(messageData.get("created").getString(""));
notificationData.messages.add(new NotificationMessage(accountID, message, createdTimeInMillis));

NotificationCompat.MessagingStyle.Message newMessage = new NotificationCompat.MessagingStyle.Message(message, createdTimeInMillis, person);

// Conversational styling should be applied to groups chats, rooms, and any 1:1 chats with more than one notification (ensuring the large profile image is always shown)
if (!conversationName.isEmpty() || hasExistingNotification) {
Expand All @@ -220,30 +210,77 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
.setGroupConversation(true)
.setConversationTitle(conversationName);


// Add all conversation messages to the notification, including the last one we just received.
for (NotificationMessage cachedMessage : notificationData.messages) {
messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, notificationData.getPerson(cachedMessage.accountID));
List<NotificationCompat.MessagingStyle.Message> messages;
if (hasExistingNotification) {
NotificationCompat.MessagingStyle previousStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(existingReportNotification.getNotification());
messages = previousStyle != null ? previousStyle.getMessages() : new ArrayList<>(List.of(recreatePreviousMessage(existingReportNotification)));
} else {
messages = new ArrayList<>();
}

// add the last one message we just received.
messages.add(newMessage);

for (NotificationCompat.MessagingStyle.Message activeMessage : messages) {
messagingStyle.addMessage(activeMessage);
}

builder.setStyle(messagingStyle);
}

// save reportID and person info for future merging
builder.addExtras(createMessageExtrasBundle(reportID, 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 (notificationData.prevNotificationID != -1) {
NotificationManagerCompat.from(context).cancel(notificationData.prevNotificationID);
if (hasExistingNotification) {
int previousNotificationID = existingReportNotification.getId();
NotificationManagerCompat.from(context).cancel(previousNotificationID);
}

} catch (Exception e) {
e.printStackTrace();
}
}

private Person createMessagePersonObject (IconCompat icon, String key, String name) {
return new Person.Builder().setIcon(icon).setKey(key).setName(name).build();
}

private NotificationCompat.MessagingStyle.Message recreatePreviousMessage (StatusBarNotification statusBarNotification) {
// Get previous message
Notification previousNotification = statusBarNotification.getNotification();
String previousMessage = previousNotification.extras.getString("android.text");
long time = statusBarNotification.getNotification().when;
// Recreate Person object
IconCompat avatarBitmap = ParcelUtils.getVersionedParcelable(previousNotification.extras, EXTRAS_AVATAR_KEY);
String previousName = previousNotification.extras.getString(EXTRAS_NAME_KEY);
String previousAccountID = previousNotification.extras.getString(EXTRAS_ACCOUNT_ID_KEY);
Person previousPerson = createMessagePersonObject(avatarBitmap, previousAccountID, previousName);

return new NotificationCompat.MessagingStyle.Message(previousMessage, time, previousPerson);
}

// Store the new notification ID so we can replace the notification if this conversation
// receives more messages
notificationData.prevNotificationID = notificationID;
private Bundle createMessageExtrasBundle(long reportID, Person person) {
Bundle extrasBundle = new Bundle();
extrasBundle.putLong(EXTRAS_REPORT_ID_KEY, reportID);
ParcelUtils.putVersionedParcelable(extrasBundle, EXTRAS_AVATAR_KEY, person.getIcon());
extrasBundle.putString(EXTRAS_ACCOUNT_ID_KEY, person.getKey());
extrasBundle.putString(EXTRAS_NAME_KEY, person.getName().toString());

NotificationCache.setNotificationData(reportID, notificationData);
return extrasBundle;
}

private StatusBarNotification getActiveNotificationByReportId(@NonNull Context context, long reportId) {
List<StatusBarNotification> notifications = NotificationManagerCompat.from(context).getActiveNotifications();
for (StatusBarNotification currentNotification : notifications) {
long associatedReportId = currentNotification.getNotification().extras.getLong("reportID", -1);
if (associatedReportId == reportId) return currentNotification;
}
return null;
}
/**
* Safely retrieve the message time in milliseconds
*/
Expand All @@ -260,26 +297,6 @@ private long getMessageTimeInMillis(String createdTime) {
return Calendar.getInstance().getTimeInMillis();
}

/**
* 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();
long reportID = payload.get("reportID").getLong(-1);

if (reportID == -1) {
return;
}

NotificationCache.setNotificationData(reportID, null);
} catch (Exception e) {
Log.e(TAG, "Failed to delete conversation cache. SendID=" + message.getSendId(), e);
}
}

private Bitmap fetchIcon(@NonNull Context context, String urlString) {
URL parsedUrl = null;
try {
Expand Down
Loading

0 comments on commit 8571ccf

Please sign in to comment.