Skip to content
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
636 changes: 318 additions & 318 deletions examples/SampleApp/ios/Podfile.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions examples/SampleApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"emoji-mart": "^5.6.0",
"lodash.mergewith": "^4.6.2",
"react": "19.2.3",
"react-native": "0.86.0",
"react-native": "0.86.2",
"react-native-blob-util": "^0.24.10",
"react-native-gesture-handler": "^3.1.0",
"react-native-haptic-feedback": "^3.0.0",
Expand Down Expand Up @@ -81,9 +81,9 @@
"@react-native-community/cli": "20.1.0",
"@react-native-community/cli-platform-android": "20.1.0",
"@react-native-community/cli-platform-ios": "20.1.0",
"@react-native/babel-preset": "0.86.0",
"@react-native/metro-config": "0.86.0",
"@react-native/typescript-config": "0.86.0",
"@react-native/babel-preset": "0.86.2",
"@react-native/metro-config": "0.86.2",
"@react-native/typescript-config": "0.86.2",
"@rnx-kit/metro-config": "^2.1.0",
"@types/lodash.mergewith": "^4.6.9",
"@types/react": "^19.2.0",
Expand Down
20 changes: 19 additions & 1 deletion package/src/components/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,20 +136,36 @@ export type GalleryThumbnailTouchableHandlerPayload = {
additionalInfo?: GalleryThumbnailTouchableHandlerAdditionalInfo;
};

export type ReactionListTouchableHandlerAdditionalInfo = {
/**
* The reaction type of the pressed reaction. Populated for presses that
* originate from a single reaction (segmented list item/clustered pill),
* undefined for the "+N more" overflow item.
*/
reactionType?: string;
};

export type ReactionListTouchableHandlerPayload = {
emitter: 'reactionList';
additionalInfo?: ReactionListTouchableHandlerAdditionalInfo;
};

export type PressableHandlerPayload = {
defaultHandler?: () => void;
event?: GestureResponderEvent;
} & (
| {
additionalInfo?: Record<string, unknown>;
emitter?: Exclude<
TouchableEmitter,
'textMention' | 'textLink' | 'urlPreview' | 'fileAttachment' | 'gallery'
'textMention' | 'textLink' | 'urlPreview' | 'fileAttachment' | 'gallery' | 'reactionList'
>;
}
| TextMentionTouchableHandlerPayload
| UrlTouchableHandlerPayload
| FileAttachmentTouchableHandlerPayload
| GalleryThumbnailTouchableHandlerPayload
| ReactionListTouchableHandlerPayload
);

export type MessagePressableHandlerPayload = PressableHandlerPayload & {
Expand Down Expand Up @@ -741,6 +757,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
onLongPress: (payload) => {
const onLongPressArgs = {
actionHandlers,
additionalInfo: payload?.additionalInfo,
defaultHandler: payload?.defaultHandler || onLongPress,
emitter: payload?.emitter || 'message',
event: payload?.event,
Expand Down Expand Up @@ -796,6 +813,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
if (onPressInMessageProp) {
return onPressInMessageProp({
actionHandlers,
additionalInfo: payload.additionalInfo,
defaultHandler: payload.defaultHandler,
emitter: payload.emitter || 'message',
event: payload.event,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export type ReactionListBottomProps = Partial<
Pick<
MessageContextValue,
| 'alignment'
| 'handleReaction'
| 'hasReactions'
| 'onLongPress'
| 'onPress'
Expand All @@ -43,7 +42,6 @@ const ItemSeparatorComponent = () => {
export const ReactionListBottom = (props: ReactionListBottomProps) => {
const {
alignment: propAlignment,
handleReaction: propHandlerReaction,
hasReactions: propHasReactions,
onLongPress: propOnLongPress,
onPress: propOnPress,
Expand All @@ -58,7 +56,6 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => {

const {
alignment: contextAlignment,
handleReaction: contextHandleReaction,
hasReactions: contextHasReactions,
onLongPress: contextOnLongPress,
onPress: contextOnPress,
Expand All @@ -72,7 +69,6 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => {
const { supportedReactions: contextSupportedReactions } = useMessagesContext();

const alignment = propAlignment || contextAlignment;
const handleReaction = propHandlerReaction || contextHandleReaction;
const hasReactions = propHasReactions || contextHasReactions;
const onLongPress = propOnLongPress || contextOnLongPress;
const onPress = propOnPress || contextOnPress;
Expand All @@ -85,7 +81,6 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => {
const renderItem = useCallback(
({ index, item }: { index: number; item: ReactionListItemProps }) => (
<ReactionListItem
handleReaction={item.handleReaction}
key={index}
onLongPress={item.onLongPress}
onPress={item.onPress}
Expand Down Expand Up @@ -115,7 +110,6 @@ export const ReactionListBottom = (props: ReactionListBottomProps) => {
}

const reactionListBottomItemData: ReactionListItemProps[] = reactions.map((reaction) => ({
handleReaction,
onLongPress,
onPress,
onPressIn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,7 @@ const Icon = ({ size, style, supportedReactions, type }: Props) => {
export type ReactionListItemProps = Partial<
Pick<
MessageContextValue,
| 'handleReaction'
| 'onLongPress'
| 'onPress'
| 'onPressIn'
| 'preventPress'
| 'showReactionsOverlay'
'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress' | 'showReactionsOverlay'
>
> &
Partial<Pick<MessagesContextValue, 'supportedReactions'>> & {
Expand All @@ -49,7 +44,6 @@ export type ReactionListItemProps = Partial<

export const ReactionListItem = (props: ReactionListItemProps) => {
const {
handleReaction,
onLongPress,
onPress,
onPressIn,
Expand Down Expand Up @@ -80,6 +74,7 @@ export const ReactionListItem = (props: ReactionListItemProps) => {
onLongPress={(event) => {
if (onLongPress) {
onLongPress({
additionalInfo: { reactionType: reaction.type },
defaultHandler: () => {
if (showReactionsOverlay) {
showReactionsOverlay(reaction.type);
Expand All @@ -93,9 +88,10 @@ export const ReactionListItem = (props: ReactionListItemProps) => {
onPress={(event) => {
if (onPress) {
onPress({
additionalInfo: { reactionType: reaction.type },
defaultHandler: () => {
if (handleReaction) {
handleReaction(reaction.type);
if (showReactionsOverlay) {
showReactionsOverlay(reaction.type);
}
},
emitter: 'reactionList',
Expand All @@ -106,9 +102,10 @@ export const ReactionListItem = (props: ReactionListItemProps) => {
onPressIn={(event) => {
if (onPressIn) {
onPressIn({
additionalInfo: { reactionType: reaction.type },
defaultHandler: () => {
if (handleReaction) {
handleReaction(reaction.type);
if (showReactionsOverlay) {
showReactionsOverlay(reaction.type);
}
},
emitter: 'reactionList',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export type ReactionListTopProps = Partial<
| 'preventPress'
| 'reactions'
| 'showReactionsOverlay'
| 'handleReaction'
> &
Pick<MessagesContextValue, 'supportedReactions' | 'reactionListType'>
> & {
Expand All @@ -47,7 +46,6 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
reactions: propReactions,
showReactionsOverlay: propShowReactionsOverlay,
supportedReactions: propSupportedReactions,
handleReaction: propHandleReaction,
type,
showCount = true,
} = props;
Expand All @@ -61,7 +59,6 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
preventPress: contextPreventPress,
reactions: contextReactions,
showReactionsOverlay: contextShowReactionsOverlay,
handleReaction: contextHandleReaction,
} = useMessageContext();

const { ReactionListClustered, ReactionListCountItem, ReactionListItem } = useComponentsContext();
Expand All @@ -76,7 +73,6 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
const reactions = propReactions || contextReactions;
const showReactionsOverlay = propShowReactionsOverlay || contextShowReactionsOverlay;
const supportedReactions = propSupportedReactions || contextSupportedReactions;
const handleReaction = propHandleReaction || contextHandleReaction;

const styles = useStyles({ alignment });

Expand Down Expand Up @@ -115,7 +111,6 @@ export const ReactionListTop = (props: ReactionListTopProps) => {
<ReactionListItem
key={reaction.type}
reaction={reaction}
handleReaction={handleReaction}
onLongPress={onLongPress}
onPress={onPress}
onPressIn={onPressIn}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ describe('ReactionListBottom', () => {
// });
// });

it('call handleReaction on press', () => {
it('does not toggle the reaction on press but opens the reactions bottom sheet instead', () => {
// Tapping a segmented reaction must mirror the clustered flow: it opens the
// reactions bottom sheet (preselecting the tapped reaction) rather than
// adding/removing the current user's reaction.
const handleReactionMock = jest.fn();
const user = generateUser();
const reaction = generateReaction();
Expand All @@ -172,6 +175,43 @@ describe('ReactionListBottom', () => {

fireEvent(reactionListBottomItem, 'onPress');

expect(handleReactionMock).toHaveBeenCalledTimes(1);
expect(handleReactionMock).not.toHaveBeenCalled();
expect(screen.getByLabelText('User Reactions on long press message')).toBeTruthy();
});

it('exposes the pressed reaction type via onPressMessage so integrators can toggle', () => {
// Integrators that want the old toggle-on-tap behavior can intercept the
// `reactionList` emitter and call `actionHandlers.toggleReaction(reactionType)`.
const onPressMessageMock = jest.fn();
const user = generateUser();
const reaction = generateReaction();
const message = generateMessage({
reaction_groups: { [reaction.type]: reaction } as unknown as ReturnType<
typeof generateMessage
>['reaction_groups'],
user,
});

renderMessage(
{ message },
{
onPressMessage: onPressMessageMock,
reactionListPosition: 'bottom',
reactionListType: 'segmented',
},
);

const reactionListBottomItem = screen.getByTestId('reaction-list-item');

fireEvent(reactionListBottomItem, 'onPress');

expect(onPressMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
additionalInfo: { reactionType: reaction.type },
emitter: 'reactionList',
}),
);
// onPressMessage replaces the default handler, so the sheet must NOT open here.
expect(screen.queryByLabelText('User Reactions on long press message')).toBeNull();
});
});
11 changes: 9 additions & 2 deletions package/src/components/MessageMenu/MessageUserReactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,21 @@ const reactionSelectorKeyExtractor = (item: ReactionSelectorItemType) => item.ty
export const MessageUserReactions = (props: MessageUserReactionsProps) => {
const styles = useStyles();
const [showMoreReactions, setShowMoreReactions] = useState(false);
const { message, reactions: propReactions, supportedReactions: propSupportedReactions } = props;
const {
message,
reactions: propReactions,
selectedReaction: propSelectedReaction,
supportedReactions: propSupportedReactions,
} = props;
const selectorListRef = useRef<FlatList>(null);
const { close } = useBottomSheetContext();
const reactionTypes = useMemo(
() => Object.keys(message?.reaction_groups ?? {}),
[message?.reaction_groups],
);
const [selectedReaction, setSelectedReaction] = useState<string | undefined>(undefined);
const [selectedReaction, setSelectedReaction] = useState<string | undefined>(
propSelectedReaction,
);
const { supportedReactions: contextSupportedReactions } = useMessagesContext();
const { icons, MessageUserReactionsItem } = useComponentsContext();
const { handleReaction } = useMessageContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ describe('MessageUserReactions when the supportedReactions are defined', () => {
expect(reactionButtons[1].props.accessibilityState.selected).toBe(false);
});

it('preselects the reaction passed via the selectedReaction prop', () => {
const { getAllByRole } = renderComponent({ selectedReaction: 'love' });
const reactionButtons = filterReactionButtons(getAllByRole('button'));
// reaction_groups order is `like`, `love`
expect(reactionButtons[0].props.accessibilityState.selected).toBe(false);
expect(reactionButtons[1].props.accessibilityState.selected).toBe(true);
});

it('toggles the selected reaction when a reaction button is pressed twice', () => {
const { getAllByRole } = renderComponent();
let reactionButtons = filterReactionButtons(getAllByRole('button'));
Expand Down
16 changes: 16 additions & 0 deletions package/src/contexts/messagesContext/MessagesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export type MessagesContextValue = Pick<MessageContextValue, 'isMessageAIGenerat
* toggleMuteUser, // () => Promise<void>;
* toggleReaction, // (reactionType: string) => Promise<void>;
* },
* additionalInfo, // emitter-specific info, e.g. `{ reactionType }` when emitter === 'reactionList'
* defaultHandler, // () => void
* event, // any event object corresponding to touchable feedback
* emitter, // which component trigged this touchable feedback e.g. card, fileAttachment, gallery, message ... etc
Expand All @@ -311,6 +312,21 @@ export type MessagesContextValue = Pick<MessageContextValue, 'isMessageAIGenerat
* }}
* />
* ```
*
* By default, pressing a reaction opens the reactions bottom sheet. To instead toggle
* the reaction (add/remove the current user's reaction), intercept the `reactionList` emitter:
*
* ```
* <Channel
* onPressMessage={({ emitter, additionalInfo, actionHandlers, defaultHandler }) => {
* if (emitter === 'reactionList' && additionalInfo?.reactionType) {
* actionHandlers?.toggleReaction(additionalInfo.reactionType as string);
* return;
* }
* defaultHandler?.();
* }}
* />
* ```
*/
onPressMessage?: (payload: MessagePressableHandlerPayload) => void;
quotedMessage?: LocalMessage | null;
Expand Down
Loading
Loading