Skip to content

Commit 7fc3126

Browse files
committed
Merge branch 'main' into collectioneur/transition-tracker-eslint
2 parents a205014 + 4a128f1 commit 7fc3126

27 files changed

Lines changed: 354 additions & 621 deletions

.claude/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@
5252
"mcp__sentry__search_events",
5353
"mcp__sentry__search_issue_events",
5454
"mcp__sentry__search_issues",
55-
"mcp__sentry__whoami"
55+
"mcp__sentry__whoami",
56+
"Bash(agent-device *)",
57+
"Bash(echo \"$(npm root -g)/agent-device/skills/agent-device\")"
5658
]
5759
},
5860
"enabledPlugins": {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
name: agent-device
3+
description: Drive iOS and Android devices for the Expensify App - testing, debugging, performance profiling, bug reproduction, and feature verification. Use when the developer needs to interact with the mobile app on a device.
4+
allowed-tools: Bash(agent-device *) Bash(npm root *)
5+
---
6+
7+
# agent-device
8+
9+
## Pre-flight
10+
11+
`agent-device` CLI version: !`agent-device --version 2>&1 || echo "NOT_INSTALLED"`
12+
13+
Canonical skill reference path (read these files directly for device automation guidance - bootstrap, exploration, verification, debugging): !`echo "$(npm root -g)/agent-device/skills/agent-device"`
14+
15+
> If the version line above shows `NOT_INSTALLED` or a command-not-found error, **STOP** and instruct the developer to install it: `npm install -g agent-device`. All device interaction depends on it.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,8 @@ modules/*/lib/
162162
.playwright-output/
163163
.playwright-mcp/
164164

165+
# agent-device
166+
agent-device-output/
167+
165168
# cspell cache
166169
.cspellcache

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ npm run web
278278
### Browser Testing
279279
Use the `/playwright-app-testing` skill to test and debug the App in a browser. Use this skill after making frontend changes to verify your work, or when the user requests testing.
280280

281+
### Mobile Device Testing
282+
Use the `/agent-device` skill to drive the App on iOS and Android (simulators or real devices) for interactive testing, performance profiling, bug reproduction, and device-specific debugging. Requires `npm install -g agent-device` - the skill's pre-flight check will surface the install instruction if missing.
283+
281284
## Architecture Decisions
282285

283286
### React Native New Architecture

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ For detailed setup instructions for each platform, see the following guides:
4848
* **📱 iOS Development**: [iOS Setup Instructions](contributingGuides/SETUP_IOS.md)
4949
* **🤖 Android Development**: [Android Setup Instructions](contributingGuides/SETUP_ANDROID.md)
5050

51+
**Optional AI-assisted mobile testing:** If you use Claude Code, the [`/agent-device` skill](.claude/skills/agent-device/SKILL.md) drives iOS and Android simulators or devices for interactive testing, debugging, and performance profiling. Requires `npm install -g agent-device`.
52+
5153
## General Troubleshooting
5254
1. If you are having issues with **_Getting Started_**, please reference [React Native's Documentation](https://reactnative.dev/docs/environment-setup)
5355
2. If you are running into CORS errors like (in the browser dev console)

src/components/Reactions/ReactionTooltipContent.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import React, {useMemo} from 'react';
1+
import React from 'react';
22
import {View} from 'react-native';
33
import Text from '@components/Text';
4-
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
54
import useLocalize from '@hooks/useLocalize';
65
import useThemeStyles from '@hooks/useThemeStyles';
6+
import {getLocalizedEmojiName} from '@libs/EmojiUtils';
77
import {getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils';
88

9-
type ReactionTooltipContentProps = Pick<WithCurrentUserPersonalDetailsProps, 'currentUserPersonalDetails'> & {
9+
type ReactionTooltipContentProps = {
1010
/**
1111
* A list of emoji codes to display in the tooltip.
1212
*/
@@ -21,15 +21,18 @@ type ReactionTooltipContentProps = Pick<WithCurrentUserPersonalDetailsProps, 'cu
2121
* A list of account IDs to display in the tooltip.
2222
*/
2323
accountIDs: number[];
24+
25+
/**
26+
* The account ID of the current user.
27+
*/
28+
currentUserAccountID: number;
2429
};
2530

26-
function ReactionTooltipContent({accountIDs, currentUserPersonalDetails, emojiCodes, emojiName}: ReactionTooltipContentProps) {
31+
function ReactionTooltipContent({accountIDs, emojiCodes, emojiName, currentUserAccountID}: ReactionTooltipContentProps) {
2732
const styles = useThemeStyles();
28-
const {translate} = useLocalize();
29-
const users = useMemo(
30-
() => getPersonalDetailsByIDs({accountIDs, currentUserAccountID: currentUserPersonalDetails.accountID, shouldChangeUserDisplayName: true}),
31-
[currentUserPersonalDetails.accountID, accountIDs],
32-
);
33+
const {translate, preferredLocale} = useLocalize();
34+
const users = getPersonalDetailsByIDs({accountIDs, currentUserAccountID, shouldChangeUserDisplayName: true});
35+
const localizedEmojiName = getLocalizedEmojiName(emojiName, preferredLocale);
3336

3437
const namesString = users
3538
.map((user) => user?.displayName)
@@ -51,7 +54,7 @@ function ReactionTooltipContent({accountIDs, currentUserPersonalDetails, emojiCo
5154

5255
<Text style={[styles.mt1, styles.textMicroBold, styles.textReactionSenders, styles.textAlignCenter]}>{namesString}</Text>
5356

54-
<Text style={[styles.textMicro, styles.fontColorReactionLabel]}>{`${translate('emojiReactions.reactedWith')} :${emojiName}:`}</Text>
57+
<Text style={[styles.textMicro, styles.fontColorReactionLabel]}>{`${translate('emojiReactions.reactedWith')} :${localizedEmojiName}:`}</Text>
5558
</View>
5659
);
5760
}

src/components/Reactions/ReportActionItemEmojiReactions.tsx

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,37 @@
11
import sortBy from 'lodash/sortBy';
22
import React, {useContext, useRef} from 'react';
3-
import {View} from 'react-native';
4-
import type {OnyxEntry} from 'react-native-onyx';
3+
// eslint-disable-next-line no-restricted-imports
4+
import {InteractionManager, View} from 'react-native';
5+
import {importEmojiLocale} from '@assets/emojis';
56
import type {Emoji} from '@assets/emojis/types';
67
import OfflineWithFeedback from '@components/OfflineWithFeedback';
78
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
8-
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
9-
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
9+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
10+
import useLocalize from '@hooks/useLocalize';
1011
import useOnyx from '@hooks/useOnyx';
1112
import useThemeStyles from '@hooks/useThemeStyles';
12-
import {getEmojiReactionDetails, getLocalizedEmojiName} from '@libs/EmojiUtils';
13+
import {getEmojiReactionDetails} from '@libs/EmojiUtils';
14+
import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu';
1315
import {ReactionListContext} from '@pages/inbox/ReportScreenContext';
1416
import type {ReactionListAnchor, ReactionListEvent} from '@pages/inbox/ReportScreenContext';
17+
import {toggleEmojiReaction} from '@userActions/Report';
18+
import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session';
1519
import CONST from '@src/CONST';
20+
import {isFullySupportedLocale} from '@src/CONST/LOCALES';
1621
import ONYXKEYS from '@src/ONYXKEYS';
17-
import type {Locale, ReportAction, ReportActionReactions} from '@src/types/onyx';
22+
import type {ReportAction, ReportActionReactions} from '@src/types/onyx';
1823
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
24+
import {getEmptyObject} from '@src/types/utils/EmptyObject';
1925
import AddReactionBubble from './AddReactionBubble';
2026
import EmojiReactionBubble from './EmojiReactionBubble';
2127
import ReactionTooltipContent from './ReactionTooltipContent';
2228

23-
type ReportActionItemEmojiReactionsProps = WithCurrentUserPersonalDetailsProps & {
24-
/** All the emoji reactions for the report action. */
25-
emojiReactions: OnyxEntry<ReportActionReactions>;
26-
27-
/** The user's preferred locale. */
28-
preferredLocale?: OnyxEntry<Locale>;
29-
29+
type ReportActionItemEmojiReactionsProps = {
3030
/** The report action that these reactions are for */
3131
reportAction: ReportAction;
3232

33-
/**
34-
* Function to call when the user presses on an emoji.
35-
* This can also be an emoji the user already reacted with,
36-
* hence this function asks to toggle the reaction by emoji.
37-
*/
38-
toggleReaction: (emoji: Emoji, preferredSkinTone: number, ignoreSkinToneOnCompare?: boolean) => void;
33+
/** The ID of the chat report this action belongs to */
34+
reportID: string | undefined;
3935

4036
/** We disable reacting with emojis on report actions that have errors */
4137
shouldBlockReactions?: boolean;
@@ -77,30 +73,40 @@ type FormattedReaction = {
7773
setIsEmojiPickerActive?: (state: boolean) => void;
7874
};
7975

80-
function ReportActionItemEmojiReactions({
81-
reportAction,
82-
currentUserPersonalDetails,
83-
toggleReaction,
84-
emojiReactions = {},
85-
shouldBlockReactions = false,
86-
preferredLocale = CONST.LOCALES.DEFAULT,
87-
setIsEmojiPickerActive,
88-
}: ReportActionItemEmojiReactionsProps) {
76+
function ReportActionItemEmojiReactions({reportAction, reportID, shouldBlockReactions = false, setIsEmojiPickerActive}: ReportActionItemEmojiReactionsProps) {
8977
const styles = useThemeStyles();
78+
const {preferredLocale} = useLocalize();
79+
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
9080
const reactionListRef = useContext(ReactionListContext);
9181
const popoverReactionListAnchors = useRef<PopoverReactionListAnchors>({});
9282
const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
9383

9484
const reportActionID = reportAction.reportActionID;
85+
const [emojiReactions = getEmptyObject<ReportActionReactions>()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`);
86+
87+
// Prime the locale emoji table when this action has reactions.
88+
// Skip the default locale since getLocalizedEmojiName never reads localeEmojis for it.
89+
if (preferredLocale && preferredLocale !== CONST.LOCALES.DEFAULT && emojiReactions !== CONST.EMPTY_OBJECT && isFullySupportedLocale(preferredLocale)) {
90+
importEmojiLocale(preferredLocale);
91+
}
92+
93+
const toggleReaction = (emoji: Emoji, skinTone: number, ignoreSkinToneOnCompare?: boolean) => {
94+
if (isAnonymousUser()) {
95+
hideContextMenu(false);
96+
97+
// eslint-disable-next-line @typescript-eslint/no-deprecated
98+
InteractionManager.runAfterInteractions(() => {
99+
signOutAndRedirectToSignIn();
100+
});
101+
return;
102+
}
103+
toggleEmojiReaction(reportID, reportAction, emoji, emojiReactions, skinTone, currentUserAccountID, ignoreSkinToneOnCompare);
104+
};
95105

96106
// Each emoji is sorted by the oldest timestamp of user reactions so that they will always appear in the same order for everyone
97107
const formattedReactions: Array<FormattedReaction | null> = sortBy(
98108
Object.entries(emojiReactions ?? {}).map(([emojiName, emojiReaction]) => {
99-
const {emoji, emojiCodes, reactionCount, hasUserReacted, userAccountIDs, oldestTimestamp} = getEmojiReactionDetails(
100-
emojiName,
101-
emojiReaction,
102-
currentUserPersonalDetails.accountID,
103-
);
109+
const {emoji, emojiCodes, reactionCount, hasUserReacted, userAccountIDs, oldestTimestamp} = getEmojiReactionDetails(emojiName, emojiReaction, currentUserAccountID);
104110

105111
if (reactionCount === 0) {
106112
return null;
@@ -138,14 +144,15 @@ function ReportActionItemEmojiReactions({
138144
if (reaction === null) {
139145
return;
140146
}
147+
141148
return (
142149
<Tooltip
143150
renderTooltipContent={() => (
144151
<ReactionTooltipContent
145-
emojiName={getLocalizedEmojiName(reaction.reactionEmojiName, preferredLocale)}
152+
emojiName={reaction.reactionEmojiName}
146153
emojiCodes={reaction.emojiCodes}
147154
accountIDs={reaction.userAccountIDs}
148-
currentUserPersonalDetails={currentUserPersonalDetails}
155+
currentUserAccountID={currentUserAccountID}
149156
/>
150157
)}
151158
renderTooltipContentKey={[...reaction.userAccountIDs.map(String), ...reaction.emojiCodes]}
@@ -184,4 +191,4 @@ function ReportActionItemEmojiReactions({
184191
);
185192
}
186193

187-
export default withCurrentUserPersonalDetails(ReportActionItemEmojiReactions);
194+
export default ReportActionItemEmojiReactions;

0 commit comments

Comments
 (0)