Skip to content

Commit

Permalink
Merge pull request #47626 from software-mansion-labs/conversation-sty…
Browse files Browse the repository at this point in the history
…le-notifications

Add conversation styling and shortcuts for Android 🤖
  • Loading branch information
arosiclair authored Sep 16, 2024
2 parents 9b25c13 + 729bc4a commit df070b7
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.database.CursorWindow
import android.os.Process
import androidx.multidex.MultiDexApplication
import com.expensify.chat.bootsplash.BootSplashPackage
import com.expensify.chat.shortcutManagerModule.ShortcutManagerPackage
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
Expand All @@ -29,6 +30,7 @@ class MainApplication : MultiDexApplication(), ReactApplication {
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage());
add(ShortcutManagerPackage())
add(BootSplashPackage())
add(ExpensifyAppPackage())
add(RNTextInputResetPackage())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
Expand All @@ -30,10 +31,13 @@
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.versionedparcelable.ParcelUtils;

import com.expensify.chat.R;
import com.expensify.chat.shortcutManagerModule.ShortcutManagerUtils;
import com.urbanairship.AirshipConfigOptions;
import com.urbanairship.json.JsonMap;
import com.urbanairship.json.JsonValue;
Expand All @@ -47,6 +51,7 @@
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
Expand Down Expand Up @@ -205,44 +210,47 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil

// Use the formatted alert message from the backend. Otherwise fallback on the message in the Onyx data.
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("");
String roomName = payload.get("roomName") == null ? "" : payload.get("roomName").getString("");

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

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

ShortcutManagerUtils.addDynamicShortcut(context, reportID, name, accountID, personIcon, person);

// Create latest received message object
long createdTimeInMillis = getMessageTimeInMillis(messageData.get("created").getString(""));
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) {
// Create the messaging style notification builder for this notification, associating it with the person who sent the report comment
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person)
.setGroupConversation(true)
.setConversationTitle(conversationName);
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person);

// Add all conversation messages to the notification, including the last one we just received.
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 all conversation messages to the notification, including the last one we just received.
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);
// add the last one message we just received.
messages.add(newMessage);

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

builder.setStyle(messagingStyle);
// 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 (!roomName.isEmpty()) {
// Create the messaging style notification builder for this notification, associating it with the person who sent the report comment
messagingStyle
.setGroupConversation(true)
.setConversationTitle(roomName);
}
builder.setStyle(messagingStyle);
builder.setShortcutId(accountID);

// save reportID and person info for future merging
builder.addExtras(createMessageExtrasBundle(reportID, person));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.expensify.chat.shortcutManagerModule;

import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.core.app.Person;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import java.util.Collections;

import com.expensify.chat.customairshipextender.CustomNotificationProvider;

public class ShortcutManagerModule extends ReactContextBaseJavaModule {
private ReactApplicationContext context;

public ShortcutManagerModule(ReactApplicationContext context) {
super(context);
this.context = context;
}

@NonNull
@Override
public String getName() {
return "ShortcutManager";
}

@ReactMethod
public void removeAllDynamicShortcuts() {
ShortcutManagerUtils.removeAllDynamicShortcuts(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.expensify.chat.shortcutManagerModule;

import androidx.annotation.NonNull;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ShortcutManagerPackage implements ReactPackage {

@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ShortcutManagerModule(reactContext));
return modules;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.expensify.chat.shortcutManagerModule;

import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;

import androidx.core.app.Person;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;

import java.util.Collections;

public class ShortcutManagerUtils {
public static void removeAllDynamicShortcuts(Context context) {
ShortcutManagerCompat.removeAllDynamicShortcuts(context);
}

public static void addDynamicShortcut(Context context, long reportID, String name, String accountID, Bitmap personIcon, Person person) {
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("new-expensify://r/" + reportID));

ShortcutInfoCompat shortcutInfo = new ShortcutInfoCompat.Builder(context, accountID)
.setShortLabel(name)
.setLongLabel(name)
.setCategories(Collections.singleton(CATEGORY_MESSAGE))
.setIntent(intent)
.setLongLived(true)
.setPerson(person)
.setIcon(IconCompat.createWithBitmap(personIcon))
.build();
ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo);
}

}
10 changes: 9 additions & 1 deletion ios/NewExpensify.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
0CDA8E35287DD650004ECBEC /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0CDA8E33287DD650004ECBEC /* AppDelegate.mm */; };
0CDA8E37287DD6A0004ECBEC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */; };
0CDA8E38287DD6A0004ECBEC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */; };
0DFC45942C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; };
0DFC45952C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; };
0F5BE0CE252686330097D869 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */; };
0F5E5350263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; };
0F5E5351263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; };
Expand Down Expand Up @@ -89,7 +91,9 @@
083353EA2B5AB22900C603C0 /* success.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = success.mp3; path = ../assets/sounds/success.mp3; sourceTree = "<group>"; };
0CDA8E33287DD650004ECBEC /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = NewExpensify/AppDelegate.mm; sourceTree = "<group>"; };
0CDA8E36287DD6A0004ECBEC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = NewExpensify/Images.xcassets; sourceTree = "<group>"; };
0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = NewExpensify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = NewExpensify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTShortcutManagerModule.h; sourceTree = "<group>"; };
0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTShortcutManagerModule.m; sourceTree = "<group>"; };
0F5BE0CD252686320097D869 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
0F5E534E263B73D5004CA14F /* EnvironmentChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnvironmentChecker.h; sourceTree = "<group>"; };
0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnvironmentChecker.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -279,6 +283,8 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */,
0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */,
499B0DA92BE2A1C000CABFB0 /* PrivacyInfo.xcprivacy */,
374FB8D528A133A7000D84EF /* OriginImageRequestHandler.h */,
374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */,
Expand Down Expand Up @@ -888,6 +894,7 @@
buildActionMask = 2147483647;
files = (
0F5E5351263B73FD004CA14F /* EnvironmentChecker.m in Sources */,
0DFC45952C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */,
0CDA8E35287DD650004ECBEC /* AppDelegate.mm in Sources */,
7041848626A8E47D00E09F4D /* RCTStartupTimer.m in Sources */,
7F5E81F06BCCF61AD02CEA06 /* ExpoModulesProvider.swift in Sources */,
Expand All @@ -899,6 +906,7 @@
buildActionMask = 2147483647;
files = (
18D050E0262400AF000D658B /* BridgingFile.swift in Sources */,
0DFC45942C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */,
0F5E5350263B73FD004CA14F /* EnvironmentChecker.m in Sources */,
374FB8D728A133FE000D84EF /* OriginImageRequestHandler.mm in Sources */,
7041848526A8E47D00E09F4D /* RCTStartupTimer.m in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions ios/RCTShortcutManagerModule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// RCTShortcutManagerModule.h
#import <React/RCTBridgeModule.h>
@interface RCTShortcutManagerModule : NSObject <RCTBridgeModule>
@end
11 changes: 11 additions & 0 deletions ios/RCTShortcutManagerModule.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// RCTCalendarModule.m
// iOS doesn't have dynamic shortcuts like Android, so this module contains noop functions to prevent iOS from crashing
#import "RCTShortcutManagerModule.h"

@implementation RCTShortcutManagerModule

RCT_EXPORT_METHOD(removeAllDynamicShortcuts){}

RCT_EXPORT_MODULE(ShortcutManager);

@end
2 changes: 2 additions & 0 deletions src/libs/Notification/PushNotification/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {PushPayload} from '@ua/react-native-airship';
import Airship, {EventType} from '@ua/react-native-airship';
import Onyx from 'react-native-onyx';
import Log from '@libs/Log';
import ShortcutManager from '@libs/ShortcutManager';
import * as PushNotificationActions from '@userActions/PushNotification';
import ONYXKEYS from '@src/ONYXKEYS';
import ForegroundNotifications from './ForegroundNotifications';
Expand Down Expand Up @@ -139,6 +140,7 @@ const deregister: Deregister = () => {
Airship.removeAllListeners(EventType.PushReceived);
Airship.removeAllListeners(EventType.NotificationResponse);
ForegroundNotifications.disableForegroundNotifications();
ShortcutManager.removeAllDynamicShortcuts();
};

/**
Expand Down
14 changes: 14 additions & 0 deletions src/libs/ShortcutManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {NativeModules} from 'react-native';

type ShortcutManagerModule = {
removeAllDynamicShortcuts: () => void;
};

const {ShortcutManager} = NativeModules;

export type {ShortcutManagerModule};

export default ShortcutManager ||
({
removeAllDynamicShortcuts: () => {},
} as ShortcutManagerModule);
2 changes: 2 additions & 0 deletions src/types/modules/react-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type {TargetedEvent} from 'react-native';
import type {BootSplashModule} from '@libs/BootSplash/types';
import type {EnvironmentCheckerModule} from '@libs/Environment/betaChecker/types';
import type {ShortcutManagerModule} from '@libs/ShortcutManager';
import type StartupTimer from '@libs/StartupTimer/types';

type HybridAppModule = {
Expand Down Expand Up @@ -42,6 +43,7 @@ declare module 'react-native' {
StartupTimer: StartupTimer;
RNTextInputReset: RNTextInputResetModule;
EnvironmentChecker: EnvironmentCheckerModule;
ShortcutManager: ShortcutManagerModule;
}

namespace Animated {
Expand Down

0 comments on commit df070b7

Please sign in to comment.