[Gekidou] animated reactions (#6035)

* Add animation for reaction counter

* Preserve reaction order while the component is mounted
This commit is contained in:
Elias Nahum 2022-03-11 09:08:02 -03:00 committed by GitHub
parent 7001e29567
commit 86bd5c5072
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 43 deletions

View file

@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Platform, Text} from 'react-native';
import {View} from 'react-native';
import AnimatedNumbers from 'react-native-animated-numbers';
import {TouchableOpacity} from 'react-native-gesture-handler';
import Emoji from '@components/emoji';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type ReactionProps = {
count: number;
@ -20,23 +22,30 @@ type ReactionProps = {
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
count: {
color: theme.linkColor,
marginLeft: 6,
color: changeOpacity(theme.centerChannelColor, 0.56),
...typography('Body', 100, 'SemiBold'),
},
countContainer: {marginRight: 5},
countHighlight: {
color: theme.buttonBg,
},
customEmojiStyle: {color: '#000'},
emoji: {marginHorizontal: 5},
highlight: {
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
borderColor: theme.buttonBg,
borderWidth: 1,
},
customEmojiStyle: {marginHorizontal: 3},
highlight: {backgroundColor: changeOpacity(theme.linkColor, 0.1)},
reaction: {
alignItems: 'center',
borderRadius: 2,
borderColor: changeOpacity(theme.linkColor, 0.4),
borderWidth: 1,
borderRadius: 4,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
flexDirection: 'row',
height: 30,
marginRight: 6,
marginBottom: 5,
marginTop: 10,
paddingHorizontal: 6,
paddingBottom: Platform.select({android: 2}),
height: 32,
justifyContent: 'center',
marginBottom: 12,
marginRight: 8,
minWidth: 50,
},
};
});
@ -55,19 +64,22 @@ const Reaction = ({count, emojiName, highlight, onPress, onLongPress, theme}: Re
delayLongPress={350}
style={[styles.reaction, (highlight && styles.highlight)]}
>
<Emoji
emojiName={emojiName}
size={20}
textStyle={{color: '#000'}}
customEmojiStyle={styles.customEmojiStyle}
testID={`reaction.emoji.${emojiName}`}
/>
<Text
style={styles.count}
testID={`reaction.emoji.${emojiName}.count`}
>
{count}
</Text>
<View style={styles.emoji}>
<Emoji
emojiName={emojiName}
size={20}
textStyle={styles.customEmojiStyle}
testID={`reaction.emoji.${emojiName}`}
/>
</View>
<View style={styles.countContainer}>
<AnimatedNumbers
includeComma={false}
fontStyle={[styles.count, (highlight && styles.countHighlight)]}
animateToNumber={count}
animationDuration={450}
/>
</View>
</TouchableOpacity>
);
};

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef} from 'react';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {View} from 'react-native';
import {TouchableOpacity} from 'react-native-gesture-handler';
@ -36,23 +36,22 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
reaction: {
alignItems: 'center',
justifyContent: 'center',
borderRadius: 2,
borderColor: changeOpacity(theme.centerChannelColor, 0.3),
borderWidth: 1,
borderRadius: 4,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
flexDirection: 'row',
height: 30,
height: 32,
marginBottom: 12,
marginRight: 6,
marginBottom: 5,
marginTop: 10,
paddingVertical: 2,
paddingVertical: 4,
paddingHorizontal: 6,
width: 40,
width: 36,
},
reactionsContainer: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
alignContent: 'flex-start',
marginTop: 12,
},
};
});
@ -61,12 +60,21 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
const intl = useIntl();
const serverUrl = useServerUrl();
const pressed = useRef(false);
if (!reactions) {
return null;
}
const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => r.emojiName)));
const styles = getStyleSheet(theme);
const buildReactionsMap = () => {
useEffect(() => {
// This helps keep the reactions in the same position at all times until unmounted
const rs = reactions.map((r) => r.emojiName);
const sorted = new Set([...sortedReactions]);
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);
setSortedReactions(sorted);
}, [reactions]);
const buildReactionsMap = useCallback(() => {
const highlightedReactions: string[] = [];
const reactionsByName = reactions.reduce((acc, reaction) => {
@ -86,7 +94,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
}, new Map<string, ReactionModel[]>());
return {reactionsByName, highlightedReactions};
};
}, [sortedReactions]);
const handleAddReactionToPost = (emoji: string) => {
addReaction(serverUrl, postId, emoji);
@ -148,11 +156,12 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
return (
<View style={styles.reactionsContainer}>
{
Array.from(reactionsByName.keys()).map((r) => {
Array.from(sortedReactions).map((r) => {
const reaction = reactionsByName.get(r);
return (
<Reaction
key={r}
count={reactionsByName.get(r)!.length}
count={reaction!.length}
emojiName={r}
highlight={highlightedReactions.includes(r)}
onPress={handleReactionPress}

121
package-lock.json generated
View file

@ -53,6 +53,7 @@
"react-intl": "5.24.6",
"react-native": "0.67.3",
"react-native-android-open-settings": "1.3.0",
"react-native-animated-numbers": "0.4.1",
"react-native-background-timer": "2.4.1",
"react-native-button": "3.0.1",
"react-native-calendars": "1.1278.0",
@ -8895,6 +8896,14 @@
"sha.js": "^2.4.8"
}
},
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
@ -11491,6 +11500,11 @@
"ua-parser-js": "^0.7.30"
}
},
"node_modules/fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
},
"node_modules/fbjs/node_modules/isomorphic-fetch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
@ -19506,6 +19520,55 @@
"resolved": "https://registry.npmjs.org/react-native-android-open-settings/-/react-native-android-open-settings-1.3.0.tgz",
"integrity": "sha512-h4FTWRtTLRVNS7RK4Bg2gAXe8G5bFZL8Jtmfk2rG2a/N/wJR+v1rAY2BxRkC48lQTqwQCIAQRbWgLVkJ8XmaLQ=="
},
"node_modules/react-native-animated-numbers": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/react-native-animated-numbers/-/react-native-animated-numbers-0.4.1.tgz",
"integrity": "sha512-3ZnJ5XBipdPeET4vWh8X4YinGaspb28Lp9Aw/n39r57+8ApWp+0pRFeNobG4FBo6UzBjRmMydD7q+fAC+5LZsA==",
"dependencies": {
"react-native-gesture-handler": "^1.10.3",
"react-native-reanimated": "^2.2.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-native": ">=0.59",
"react-native-gesture-handler": "1.10.3",
"react-native-reanimated": ">=2"
}
},
"node_modules/react-native-animated-numbers/node_modules/fbjs": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz",
"integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==",
"dependencies": {
"cross-fetch": "^3.1.5",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.30"
}
},
"node_modules/react-native-animated-numbers/node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dependencies": {
"asap": "~2.0.3"
}
},
"node_modules/react-native-animated-numbers/node_modules/react-native-gesture-handler": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
"integrity": "sha512-cBGMi1IEsIVMgoox4RvMx7V2r6bNKw0uR1Mu1o7NbuHS6BRSVLq0dP34l2ecnPlC+jpWd3le6Yg1nrdCjby2Mw==",
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"fbjs": "^3.0.0",
"hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4",
"prop-types": "^15.7.2"
}
},
"node_modules/react-native-background-timer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz",
@ -30926,6 +30989,14 @@
"sha.js": "^2.4.8"
}
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": {
"node-fetch": "2.6.7"
}
},
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
@ -32945,6 +33016,11 @@
}
}
},
"fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
},
"figgy-pudding": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
@ -39296,6 +39372,51 @@
"resolved": "https://registry.npmjs.org/react-native-android-open-settings/-/react-native-android-open-settings-1.3.0.tgz",
"integrity": "sha512-h4FTWRtTLRVNS7RK4Bg2gAXe8G5bFZL8Jtmfk2rG2a/N/wJR+v1rAY2BxRkC48lQTqwQCIAQRbWgLVkJ8XmaLQ=="
},
"react-native-animated-numbers": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/react-native-animated-numbers/-/react-native-animated-numbers-0.4.1.tgz",
"integrity": "sha512-3ZnJ5XBipdPeET4vWh8X4YinGaspb28Lp9Aw/n39r57+8ApWp+0pRFeNobG4FBo6UzBjRmMydD7q+fAC+5LZsA==",
"requires": {
"react-native-gesture-handler": "^1.10.3",
"react-native-reanimated": "^2.2.4"
},
"dependencies": {
"fbjs": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz",
"integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==",
"requires": {
"cross-fetch": "^3.1.5",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.30"
}
},
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
"asap": "~2.0.3"
}
},
"react-native-gesture-handler": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
"integrity": "sha512-cBGMi1IEsIVMgoox4RvMx7V2r6bNKw0uR1Mu1o7NbuHS6BRSVLq0dP34l2ecnPlC+jpWd3le6Yg1nrdCjby2Mw==",
"requires": {
"@egjs/hammerjs": "^2.0.17",
"fbjs": "^3.0.0",
"hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4",
"prop-types": "^15.7.2"
}
}
}
},
"react-native-background-timer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz",

View file

@ -50,6 +50,7 @@
"react-intl": "5.24.6",
"react-native": "0.67.3",
"react-native-android-open-settings": "1.3.0",
"react-native-animated-numbers": "0.4.1",
"react-native-background-timer": "2.4.1",
"react-native-button": "3.0.1",
"react-native-calendars": "1.1278.0",