Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conversation styling and shortcuts for Android 🤖 #47626

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading