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 14 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
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/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import NetworkConnection from '@libs/NetworkConnection';
import * as Pusher from '@libs/Pusher/pusher';
import * as ReportUtils from '@libs/ReportUtils';
import * as SessionUtils from '@libs/SessionUtils';
import ShortcutManager from '@libs/ShortcutManager';
import Timers from '@libs/Timers';
import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import {KEYS_TO_PRESERVE, openApp} from '@userActions/App';
Expand Down Expand Up @@ -205,6 +206,7 @@ function hasAuthToken(): boolean {
function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, killHybridApp = true) {
Log.info('Redirecting to Sign In because signOut() was called');
hideContextMenu(false);
ShortcutManager.removeAllDynamicShortcuts();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would probably be better to put this in PushNotification.deregister. Can we do a quick test to make sure it still works correctly from there?

if (!isAnonymousUser()) {
// In the HybridApp, we want the Old Dot to handle the sign out process
if (NativeModules.HybridAppModule && killHybridApp) {
Expand Down
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