mattermost-mobile-upstream/app/screens/reactions/reactions.tsx
Carlos Garcia b98363e719
prevent crash when reaction emoji is removed while ReactionsScreen is… (#9837)
* prevent crash when reaction emoji is removed while ReactionsScreen is open

* change test as per copilot suggestion
2026-06-22 12:53:12 +02:00

98 lines
3.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Screens} from '@constants';
import BottomSheet from '@screens/bottom_sheet';
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
import EmojiAliases from './emoji_aliases';
import EmojiBar from './emoji_bar';
import ReactorsList from './reactors_list';
import type ReactionModel from '@typings/database/models/servers/reaction';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
initialEmoji: string;
location: AvailableScreens;
reactions?: ReactionModel[];
}
function getSortedReactions(reactions: ReactionModel[] | undefined, prevSortedReactions: string[]): string[] {
const rs = reactions?.map((r) => getEmojiFirstAlias(r.emojiName));
const sorted = new Set([...prevSortedReactions]);
const added = rs?.filter((r) => !sorted.has(r));
added?.forEach(sorted.add, sorted);
const removed = [...sorted].filter((s) => !rs?.includes(s));
removed.forEach(sorted.delete, sorted);
return Array.from(sorted);
}
const Reactions = ({initialEmoji, location, reactions}: Props) => {
const [sortedReactions, setSortedReactions] = useState(Array.from(new Set(reactions?.map((r) => getEmojiFirstAlias(r.emojiName)))));
const [index, setIndex] = useState(() => Math.max(0, sortedReactions.indexOf(initialEmoji)));
const reactionsByName = useMemo(() => {
return reactions?.reduce((acc, reaction) => {
const emojiAlias = getEmojiFirstAlias(reaction.emojiName);
if (acc.has(emojiAlias)) {
const rs = acc.get(emojiAlias);
// eslint-disable-next-line max-nested-callbacks
const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1;
if (!present) {
rs!.push(reaction);
}
} else {
acc.set(emojiAlias, [reaction]);
}
return acc;
}, new Map<string, ReactionModel[]>());
}, [reactions]);
const renderContent = useCallback(() => {
const emojiAlias = sortedReactions[index];
if (!emojiAlias || !reactionsByName) {
return null;
}
return (
<>
<EmojiBar
emojiSelected={emojiAlias}
reactionsByName={reactionsByName}
setIndex={setIndex}
sortedReactions={sortedReactions}
/>
<EmojiAliases emoji={emojiAlias}/>
<ReactorsList
key={emojiAlias}
location={location}
reactions={reactionsByName.get(emojiAlias)!}
/>
</>
);
}, [index, location, reactionsByName, sortedReactions]);
useEffect(() => {
// This helps keep the reactions in the same position at all times until unmounted
setSortedReactions((prevSortedReactions) => {
return getSortedReactions(reactions, prevSortedReactions);
});
}, [reactions]);
return (
<BottomSheet
renderContent={renderContent}
screen={Screens.REACTIONS}
initialSnapIndex={1}
snapPoints={[1, '50%', '80%']}
testID='reactions'
/>
);
};
export default Reactions;