Skip to content

Commit 2eb4496

Browse files
committed
fix(predict): share pulsing live dot
1 parent 3763fb7 commit 2eb4496

3 files changed

Lines changed: 105 additions & 184 deletions

File tree

app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCard.tsx

Lines changed: 3 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
} from '@metamask/design-system-react-native';
1717
import { useTailwind } from '@metamask/design-system-twrnc-preset';
1818
import { NavigationProp, useNavigation } from '@react-navigation/native';
19-
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
20-
import { Animated, Easing, StyleSheet, TouchableOpacity } from 'react-native';
19+
import React, { useCallback, useMemo } from 'react';
20+
import { TouchableOpacity } from 'react-native';
2121
import I18n from '../../../../../../locales/i18n';
2222
import Routes from '../../../../../constants/navigation/Routes';
2323
import { getIntlDateTimeFormatter } from '../../../../../util/intl';
@@ -43,33 +43,10 @@ import {
4343
import { parseScore } from '../../utils/gameParser';
4444
import TrendingFeedSessionManager from '../../../Trending/services/TrendingFeedSessionManager';
4545
import PredictSportTeamLogo from '../PredictSportTeamLogo/PredictSportTeamLogo';
46+
import PulsingLiveDot from '../PulsingLiveDot/PulsingLiveDot';
4647

4748
const TEAM_LOGO_SIZE = 32;
4849
const COMPACT_TEAM_LOGO_SIZE = 28;
49-
const LIVE_DOT_SIZE = 6;
50-
const LIVE_DOT_RIPPLE_SIZE = 12;
51-
52-
const styles = StyleSheet.create({
53-
liveDotContainer: {
54-
alignItems: 'center',
55-
height: LIVE_DOT_RIPPLE_SIZE,
56-
justifyContent: 'center',
57-
width: LIVE_DOT_RIPPLE_SIZE,
58-
},
59-
liveDot: {
60-
borderRadius: LIVE_DOT_SIZE / 2,
61-
height: LIVE_DOT_SIZE,
62-
shadowOpacity: 0.84,
63-
shadowRadius: 6,
64-
width: LIVE_DOT_SIZE,
65-
},
66-
liveDotRipple: {
67-
borderRadius: LIVE_DOT_RIPPLE_SIZE / 2,
68-
height: LIVE_DOT_RIPPLE_SIZE,
69-
position: 'absolute',
70-
width: LIVE_DOT_RIPPLE_SIZE,
71-
},
72-
});
7350

7451
interface PredictMarketSportCardProps {
7552
market: PredictMarketType;
@@ -92,67 +69,6 @@ interface SportOutcomeButtonItem {
9269
variant: 'home' | 'draw' | 'away';
9370
}
9471

95-
const PulsingLiveDot = () => {
96-
const { colors } = useTheme();
97-
const rippleScale = useRef(new Animated.Value(0.35)).current;
98-
const rippleOpacity = useRef(new Animated.Value(0.8)).current;
99-
100-
useEffect(() => {
101-
const animation = Animated.loop(
102-
Animated.parallel([
103-
Animated.timing(rippleScale, {
104-
toValue: 1,
105-
duration: 1400,
106-
easing: Easing.out(Easing.quad),
107-
useNativeDriver: true,
108-
}),
109-
Animated.timing(rippleOpacity, {
110-
toValue: 0,
111-
duration: 1400,
112-
easing: Easing.out(Easing.quad),
113-
useNativeDriver: true,
114-
}),
115-
]),
116-
);
117-
118-
animation.start();
119-
120-
return () => {
121-
animation.stop();
122-
};
123-
}, [rippleOpacity, rippleScale]);
124-
125-
const rippleStyle = useMemo(
126-
() => [
127-
styles.liveDotRipple,
128-
{
129-
backgroundColor: colors.success.default,
130-
opacity: rippleOpacity,
131-
transform: [{ scale: rippleScale }],
132-
},
133-
],
134-
[colors.success.default, rippleOpacity, rippleScale],
135-
);
136-
137-
const dotStyle = useMemo(
138-
() => [
139-
styles.liveDot,
140-
{
141-
backgroundColor: colors.success.default,
142-
shadowColor: colors.success.default,
143-
},
144-
],
145-
[colors.success.default],
146-
);
147-
148-
return (
149-
<Animated.View style={styles.liveDotContainer}>
150-
<Animated.View style={rippleStyle} />
151-
<Animated.View style={dotStyle} />
152-
</Animated.View>
153-
);
154-
};
155-
15672
const formatGameDateTime = (
15773
startTime: string,
15874
): { date: string; time: string } => {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, { useEffect, useMemo, useRef } from 'react';
2+
import { Animated, Easing, StyleSheet } from 'react-native';
3+
import { useTheme } from '../../../../../util/theme';
4+
5+
const LIVE_DOT_SIZE = 6;
6+
const LIVE_DOT_RIPPLE_SIZE = 12;
7+
8+
const styles = StyleSheet.create({
9+
liveDotContainer: {
10+
alignItems: 'center',
11+
height: LIVE_DOT_RIPPLE_SIZE,
12+
justifyContent: 'center',
13+
width: LIVE_DOT_RIPPLE_SIZE,
14+
},
15+
liveDot: {
16+
borderRadius: LIVE_DOT_SIZE / 2,
17+
height: LIVE_DOT_SIZE,
18+
shadowOpacity: 0.84,
19+
shadowRadius: 6,
20+
width: LIVE_DOT_SIZE,
21+
},
22+
liveDotRipple: {
23+
borderRadius: LIVE_DOT_RIPPLE_SIZE / 2,
24+
height: LIVE_DOT_RIPPLE_SIZE,
25+
position: 'absolute',
26+
width: LIVE_DOT_RIPPLE_SIZE,
27+
},
28+
});
29+
30+
interface PulsingLiveDotProps {
31+
color?: string;
32+
}
33+
34+
const PulsingLiveDot = ({ color }: PulsingLiveDotProps) => {
35+
const { colors } = useTheme();
36+
const resolvedColor = color ?? colors.success.default;
37+
const rippleScale = useRef(new Animated.Value(0.35)).current;
38+
const rippleOpacity = useRef(new Animated.Value(0.8)).current;
39+
40+
useEffect(() => {
41+
const animation = Animated.loop(
42+
Animated.parallel([
43+
Animated.timing(rippleScale, {
44+
toValue: 1,
45+
duration: 1400,
46+
easing: Easing.out(Easing.quad),
47+
useNativeDriver: true,
48+
}),
49+
Animated.timing(rippleOpacity, {
50+
toValue: 0,
51+
duration: 1400,
52+
easing: Easing.out(Easing.quad),
53+
useNativeDriver: true,
54+
}),
55+
]),
56+
);
57+
58+
animation.start();
59+
60+
return () => {
61+
animation.stop();
62+
};
63+
}, [rippleOpacity, rippleScale]);
64+
65+
const rippleStyle = useMemo(
66+
() => [
67+
styles.liveDotRipple,
68+
{
69+
backgroundColor: resolvedColor,
70+
opacity: rippleOpacity,
71+
transform: [{ scale: rippleScale }],
72+
},
73+
],
74+
[resolvedColor, rippleOpacity, rippleScale],
75+
);
76+
77+
const dotStyle = useMemo(
78+
() => [
79+
styles.liveDot,
80+
{
81+
backgroundColor: resolvedColor,
82+
shadowColor: resolvedColor,
83+
},
84+
],
85+
[resolvedColor],
86+
);
87+
88+
return (
89+
<Animated.View style={styles.liveDotContainer}>
90+
<Animated.View style={rippleStyle} />
91+
<Animated.View style={dotStyle} />
92+
</Animated.View>
93+
);
94+
};
95+
96+
export default PulsingLiveDot;

app/components/UI/Predict/views/PredictWorldCup/PredictWorldCup.tsx

Lines changed: 6 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,7 @@
1-
import React, {
2-
useCallback,
3-
useEffect,
4-
useMemo,
5-
useRef,
6-
useState,
7-
} from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
82
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
93
import { useSelector } from 'react-redux';
10-
import {
11-
Animated,
12-
Easing,
13-
Pressable,
14-
RefreshControl,
15-
ScrollView,
16-
StyleSheet,
17-
} from 'react-native';
4+
import { Pressable, RefreshControl, ScrollView } from 'react-native';
185
import { FlashList } from '@shopify/flash-list';
196
import { useTailwind } from '@metamask/design-system-twrnc-preset';
207
import {
@@ -46,6 +33,7 @@ import {
4633
import PredictMarket from '../../components/PredictMarket';
4734
import PredictMarketSkeleton from '../../components/PredictMarketSkeleton';
4835
import PredictOffline from '../../components/PredictOffline';
36+
import PulsingLiveDot from '../../components/PulsingLiveDot/PulsingLiveDot';
4937
import type { PredictWorldCupConfig } from '../../types/flags';
5038
import { strings } from '../../../../../../locales/i18n';
5139

@@ -61,92 +49,11 @@ export const PREDICT_WORLD_CUP_SCREEN_TEST_IDS = {
6149
SKELETON: 'predict-world-cup-skeleton',
6250
} as const;
6351

64-
const LIVE_DOT_SIZE = 6;
65-
const LIVE_DOT_RIPPLE_SIZE = 12;
66-
67-
const styles = StyleSheet.create({
68-
liveDotContainer: {
69-
alignItems: 'center',
70-
height: LIVE_DOT_RIPPLE_SIZE,
71-
justifyContent: 'center',
72-
width: LIVE_DOT_RIPPLE_SIZE,
73-
},
74-
liveDot: {
75-
borderRadius: LIVE_DOT_SIZE / 2,
76-
height: LIVE_DOT_SIZE,
77-
shadowOpacity: 0.84,
78-
shadowRadius: 6,
79-
width: LIVE_DOT_SIZE,
80-
},
81-
liveDotRipple: {
82-
borderRadius: LIVE_DOT_RIPPLE_SIZE / 2,
83-
height: LIVE_DOT_RIPPLE_SIZE,
84-
position: 'absolute',
85-
width: LIVE_DOT_RIPPLE_SIZE,
86-
},
87-
});
88-
89-
type Tw = ReturnType<typeof useTailwind>;
90-
9152
type WorldCupConfigSubset = Pick<
9253
PredictWorldCupConfig,
9354
'seriesId' | 'tagSlug' | 'gamesTagId' | 'stages'
9455
>;
9556

96-
const LiveIndicator = ({ tw }: { tw: Tw }) => {
97-
const rippleScale = useRef(new Animated.Value(0.35)).current;
98-
const rippleOpacity = useRef(new Animated.Value(0.8)).current;
99-
100-
useEffect(() => {
101-
const animation = Animated.loop(
102-
Animated.parallel([
103-
Animated.timing(rippleScale, {
104-
toValue: 1,
105-
duration: 1400,
106-
easing: Easing.out(Easing.quad),
107-
useNativeDriver: true,
108-
}),
109-
Animated.timing(rippleOpacity, {
110-
toValue: 0,
111-
duration: 1400,
112-
easing: Easing.out(Easing.quad),
113-
useNativeDriver: true,
114-
}),
115-
]),
116-
);
117-
118-
animation.start();
119-
120-
return () => {
121-
animation.stop();
122-
};
123-
}, [rippleOpacity, rippleScale]);
124-
125-
const rippleStyle = useMemo(
126-
() => [
127-
styles.liveDotRipple,
128-
tw.style('bg-success-default'),
129-
{
130-
opacity: rippleOpacity,
131-
transform: [{ scale: rippleScale }],
132-
},
133-
],
134-
[rippleOpacity, rippleScale, tw],
135-
);
136-
137-
const dotStyle = useMemo(
138-
() => [styles.liveDot, tw.style('bg-success-default')],
139-
[tw],
140-
);
141-
142-
return (
143-
<Animated.View style={styles.liveDotContainer}>
144-
<Animated.View style={rippleStyle} />
145-
<Animated.View style={dotStyle} />
146-
</Animated.View>
147-
);
148-
};
149-
15057
interface WorldCupTabContentProps {
15158
activeTab: PredictWorldCupTabKey;
15259
config: WorldCupConfigSubset;
@@ -362,7 +269,9 @@ const PredictWorldCup: React.FC = () => {
362269
)}
363270
testID={`${PREDICT_WORLD_CUP_SCREEN_TEST_IDS.TAB}-${tab.key}`}
364271
>
365-
{tab.isLive && <LiveIndicator tw={tw} />}
272+
{tab.isLive && (
273+
<PulsingLiveDot color={tw.color('success-default')} />
274+
)}
366275
<Text
367276
color={
368277
isActive

0 commit comments

Comments
 (0)