diff --git a/android/app/src/main/java/com/expensify/chat/MainApplication.kt b/android/app/src/main/java/com/expensify/chat/MainApplication.kt index 26a28d9955a0..2cc8b7780253 100644 --- a/android/app/src/main/java/com/expensify/chat/MainApplication.kt +++ b/android/app/src/main/java/com/expensify/chat/MainApplication.kt @@ -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 @@ -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()) diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index 8eff32dedf76..b950921a0cd5 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -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; @@ -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; @@ -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; @@ -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 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 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)); diff --git a/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerModule.java b/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerModule.java new file mode 100644 index 000000000000..fdb6d0ba3b97 --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerModule.java @@ -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); + } +} diff --git a/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerPackage.java b/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerPackage.java new file mode 100644 index 000000000000..d28f75592d93 --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerPackage.java @@ -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 createViewManagers(@NonNull ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new ShortcutManagerModule(reactContext)); + return modules; + } +} diff --git a/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerUtils.java b/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerUtils.java new file mode 100644 index 000000000000..5947faaa67c4 --- /dev/null +++ b/android/app/src/main/java/com/expensify/chat/shortcutManagerModule/ShortcutManagerUtils.java @@ -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); + } + +} diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 19e80e80c59e..768062717d4b 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -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 */; }; @@ -89,7 +91,9 @@ 083353EA2B5AB22900C603C0 /* success.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = success.mp3; path = ../assets/sounds/success.mp3; sourceTree = ""; }; 0CDA8E33287DD650004ECBEC /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = NewExpensify/AppDelegate.mm; sourceTree = ""; }; 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = NewExpensify/Images.xcassets; sourceTree = ""; }; - 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = NewExpensify/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = NewExpensify/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTShortcutManagerModule.h; sourceTree = ""; }; + 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTShortcutManagerModule.m; sourceTree = ""; }; 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 0F5E534E263B73D5004CA14F /* EnvironmentChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnvironmentChecker.h; sourceTree = ""; }; 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnvironmentChecker.m; sourceTree = ""; }; @@ -279,6 +283,8 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */, + 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */, 499B0DA92BE2A1C000CABFB0 /* PrivacyInfo.xcprivacy */, 374FB8D528A133A7000D84EF /* OriginImageRequestHandler.h */, 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */, @@ -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 */, @@ -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 */, diff --git a/ios/RCTShortcutManagerModule.h b/ios/RCTShortcutManagerModule.h new file mode 100644 index 000000000000..5d596d5e7a5e --- /dev/null +++ b/ios/RCTShortcutManagerModule.h @@ -0,0 +1,4 @@ +// RCTShortcutManagerModule.h +#import +@interface RCTShortcutManagerModule : NSObject +@end diff --git a/ios/RCTShortcutManagerModule.m b/ios/RCTShortcutManagerModule.m new file mode 100644 index 000000000000..bab19019a967 --- /dev/null +++ b/ios/RCTShortcutManagerModule.m @@ -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 diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 334317ec5d0a..448365c1cd1d 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -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'; @@ -139,6 +140,7 @@ const deregister: Deregister = () => { Airship.removeAllListeners(EventType.PushReceived); Airship.removeAllListeners(EventType.NotificationResponse); ForegroundNotifications.disableForegroundNotifications(); + ShortcutManager.removeAllDynamicShortcuts(); }; /** diff --git a/src/libs/ShortcutManager/index.ts b/src/libs/ShortcutManager/index.ts new file mode 100644 index 000000000000..cc748caaa0de --- /dev/null +++ b/src/libs/ShortcutManager/index.ts @@ -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); diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index 7e3f299ad938..40c5b72ce1e4 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -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 = { @@ -42,6 +43,7 @@ declare module 'react-native' { StartupTimer: StartupTimer; RNTextInputReset: RNTextInputResetModule; EnvironmentChecker: EnvironmentCheckerModule; + ShortcutManager: ShortcutManagerModule; } namespace Animated {