Skip to content

Commit 19a3077

Browse files
authored
fix: poll input dialog redesign (#3495)
## 🎯 Goal <!-- Describe why we are making this change --> ## πŸ›  Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## πŸ§ͺ Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## β˜‘οΈ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent b935bbe commit 19a3077

File tree

6 files changed

+139
-91
lines changed

6 files changed

+139
-91
lines changed

β€Žpackage/src/components/Poll/CreatePollContent.tsxβ€Ž

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { StyleSheet, Switch, Text, View } from 'react-native';
44
import { ScrollView } from 'react-native-gesture-handler';
55
import Animated, { LinearTransition, useSharedValue } from 'react-native-reanimated';
66

7-
import { PollComposerState, VotingVisibility } from 'stream-chat';
7+
import { PollComposerState, StateStore, VotingVisibility } from 'stream-chat';
88

99
import { CreatePollOptions, CurrentOptionPositionsCache } from './components';
1010

@@ -13,6 +13,7 @@ import { MultipleAnswersField } from './components/MultipleAnswersField';
1313
import { NameField } from './components/NameField';
1414

1515
import {
16+
CreatePollModalState,
1617
CreatePollContentContextValue,
1718
CreatePollContentProvider,
1819
InputMessageInputContextValue,
@@ -107,9 +108,8 @@ export const CreatePollContent = () => {
107108
}, [currentOptionPositions, normalizedCreatePollOptionGap, optionIdsKey]);
108109

109110
const onBackPressHandler = useCallback(() => {
110-
pollComposer.initState();
111111
closePollCreationDialog?.();
112-
}, [pollComposer, closePollCreationDialog]);
112+
}, [closePollCreationDialog]);
113113

114114
const onCreatePollPressHandler = useCallback(async () => {
115115
await createAndSendPoll();
@@ -220,27 +220,51 @@ export const CreatePoll = ({
220220
> &
221221
Pick<InputMessageInputContextValue, 'CreatePollContent'>) => {
222222
const messageComposer = useMessageComposer();
223+
const [modalStateStore] = useState(
224+
() => new StateStore<CreatePollModalState>({ isClosing: false }),
225+
);
226+
const closeFrameRef = useRef<number | null>(null);
227+
228+
const closeCreatePollDialog = useCallback(() => {
229+
if (closeFrameRef.current !== null || modalStateStore.getLatestValue().isClosing) {
230+
return;
231+
}
232+
233+
// Let the modal render once with exit animations disabled before we dismiss it.
234+
modalStateStore.partialNext({ isClosing: true });
235+
closeFrameRef.current = requestAnimationFrame(() => {
236+
closeFrameRef.current = null;
237+
closePollCreationDialog?.();
238+
});
239+
}, [closePollCreationDialog, modalStateStore]);
240+
241+
useEffect(() => {
242+
return () => {
243+
if (closeFrameRef.current !== null) {
244+
cancelAnimationFrame(closeFrameRef.current);
245+
}
246+
// Reset after teardown so poll field exit animations do not delay modal dismissal.
247+
messageComposer.pollComposer.initState();
248+
};
249+
}, [messageComposer]);
223250

224251
const createAndSendPoll = useCallback(async () => {
225252
try {
226253
await messageComposer.createPoll();
227254
await sendMessage();
228-
closePollCreationDialog?.();
229-
// it's important that the reset of the pollComposer state happens
230-
// after we've already closed the modal; as otherwise we'd get weird
231-
// UI behaviour.
232-
messageComposer.pollComposer.initState();
255+
closeCreatePollDialog();
233256
} catch (error) {
234257
console.log('Error creating a poll and sending a message:', error);
235258
}
236-
}, [messageComposer, sendMessage, closePollCreationDialog]);
259+
}, [closeCreatePollDialog, messageComposer, sendMessage]);
237260

238261
return (
239262
<CreatePollContentProvider
240263
value={{
241-
closePollCreationDialog,
264+
closePollCreationDialog: closeCreatePollDialog,
242265
createAndSendPoll,
243266
createPollOptionGap,
267+
modalStateStore,
244268
sendMessage,
245269
}}
246270
>

β€Žpackage/src/components/Poll/components/MultipleVotesSettings.tsxβ€Ž

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Animated, { LinearTransition, StretchInY, StretchOutY } from 'react-nativ
55

66
import { PollComposerState } from 'stream-chat';
77

8-
import { useTheme, useTranslationContext } from '../../../contexts';
8+
import { useCreatePollContentContext, useTheme, useTranslationContext } from '../../../contexts';
99
import { useMessageComposer } from '../../../contexts/messageInputContext/hooks/useMessageComposer';
1010
import { useStableCallback } from '../../../hooks';
1111
import { useStateStore } from '../../../hooks/useStateStore';
@@ -17,6 +17,10 @@ const pollComposerStateSelector = (state: PollComposerState) => ({
1717
max_votes_allowed: state.data.max_votes_allowed,
1818
});
1919

20+
const modalStateSelector = (state: { isClosing: boolean }) => ({
21+
isClosing: state.isClosing,
22+
});
23+
2024
const MaxVotesTextInput = () => {
2125
const messageComposer = useMessageComposer();
2226
const { pollComposer } = messageComposer;
@@ -91,6 +95,8 @@ const MaxVotesTextInput = () => {
9195
export const MultipleVotesSettings = () => {
9296
const [allowMaxVotesPerPerson, setAllowMaxVotesPerPerson] = useState<boolean>(false);
9397
const { t } = useTranslationContext();
98+
const { modalStateStore } = useCreatePollContentContext();
99+
const { isClosing = false } = useStateStore(modalStateStore, modalStateSelector) ?? {};
94100
const messageComposer = useMessageComposer();
95101
const { pollComposer } = messageComposer;
96102
const { updateFields } = pollComposer;
@@ -133,7 +139,7 @@ export const MultipleVotesSettings = () => {
133139
<Animated.View
134140
layout={LinearTransition.duration(200)}
135141
entering={StretchInY.duration(200)}
136-
exiting={StretchOutY.duration(200)}
142+
exiting={isClosing ? undefined : StretchOutY.duration(200)}
137143
style={[styles.settingsWrapper, multipleAnswers.settingsWrapper]}
138144
>
139145
<View style={[styles.optionCard, multipleAnswers.optionCard]}>
@@ -152,7 +158,7 @@ export const MultipleVotesSettings = () => {
152158
{allowMaxVotesPerPerson ? (
153159
<Animated.View
154160
entering={StretchInY.duration(200)}
155-
exiting={StretchOutY.duration(200)}
161+
exiting={isClosing ? undefined : StretchOutY.duration(200)}
156162
style={[styles.row, multipleAnswers.row]}
157163
>
158164
<Button
Lines changed: 88 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import React, { useState } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import {
33
KeyboardAvoidingView,
44
Modal,
55
Platform,
6-
Pressable,
76
StyleSheet,
87
Text,
98
TextInput,
@@ -14,6 +13,8 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
1413
import Animated, { LinearTransition, ZoomIn } from 'react-native-reanimated';
1514

1615
import { useTheme, useTranslationContext } from '../../../contexts';
16+
import { primitives } from '../../../theme';
17+
import { Button } from '../../ui';
1718

1819
export type PollInputDialogProps = {
1920
closeDialog: () => void;
@@ -35,20 +36,15 @@ export const PollInputDialog = ({
3536

3637
const {
3738
theme: {
38-
colors: { accent_dark_blue, black, white },
39+
semantics,
3940
poll: {
40-
inputDialog: {
41-
button,
42-
buttonContainer,
43-
container,
44-
input,
45-
title: titleStyle,
46-
transparentContainer,
47-
},
41+
inputDialog: { buttonContainer, container, input, title: titleStyle, transparentContainer },
4842
},
4943
},
5044
} = useTheme();
5145

46+
const styles = useStyles();
47+
5248
return (
5349
<Modal animationType='fade' onRequestClose={closeDialog} transparent={true} visible={visible}>
5450
<GestureHandlerRootView style={styles.modalRoot}>
@@ -59,36 +55,40 @@ export const PollInputDialog = ({
5955
<Animated.View
6056
entering={ZoomIn.duration(200)}
6157
layout={LinearTransition.duration(200)}
62-
style={[styles.container, { backgroundColor: white }, container]}
58+
style={[styles.container, container]}
6359
>
64-
<Text style={[styles.title, { color: black }, titleStyle]}>{title}</Text>
65-
<TextInput
66-
autoFocus={true}
67-
onChangeText={setDialogInput}
68-
placeholder={t('Ask a question')}
69-
style={[styles.input, { color: black }, input]}
70-
value={dialogInput}
71-
/>
60+
<View style={styles.inputContainer}>
61+
<Text style={[styles.title, titleStyle]}>{title}</Text>
62+
<TextInput
63+
autoFocus={true}
64+
onChangeText={setDialogInput}
65+
placeholder={t('Ask a question')}
66+
placeholderTextColor={semantics.inputTextPlaceholder}
67+
style={[styles.input, input]}
68+
value={dialogInput}
69+
/>
70+
</View>
7271
<View style={[styles.buttonContainer, buttonContainer]}>
73-
<Pressable
72+
<Button
73+
variant={'secondary'}
74+
type={'ghost'}
75+
label={t('Cancel')}
76+
size='md'
7477
onPress={closeDialog}
75-
style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1 })}
76-
>
77-
<Text style={[styles.button, { color: accent_dark_blue }, button]}>
78-
{t('Cancel')}
79-
</Text>
80-
</Pressable>
81-
<Pressable
78+
style={styles.button}
79+
/>
80+
<Button
81+
variant={'secondary'}
82+
type={'solid'}
83+
label={t('Send')}
84+
size='md'
8285
onPress={() => {
8386
onSubmit(dialogInput);
8487
closeDialog();
8588
}}
86-
style={({ pressed }) => ({ marginLeft: 32, opacity: pressed ? 0.5 : 1 })}
87-
>
88-
<Text style={[styles.button, { color: accent_dark_blue }, button]}>
89-
{t('SEND')}
90-
</Text>
91-
</Pressable>
89+
style={styles.button}
90+
disabled={!dialogInput}
91+
/>
9292
</View>
9393
</Animated.View>
9494
</KeyboardAvoidingView>
@@ -97,36 +97,57 @@ export const PollInputDialog = ({
9797
);
9898
};
9999

100-
const styles = StyleSheet.create({
101-
button: { fontSize: 16, fontWeight: '500' },
102-
buttonContainer: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 52 },
103-
container: {
104-
backgroundColor: 'white',
105-
borderRadius: 16,
106-
paddingBottom: 20,
107-
paddingHorizontal: 16,
108-
paddingTop: 32,
109-
width: '80%',
110-
},
111-
modalRoot: {
112-
flex: 1,
113-
},
114-
input: {
115-
alignItems: 'center',
116-
borderColor: 'gray',
117-
borderRadius: 18,
118-
borderWidth: 1,
119-
fontSize: 16,
120-
height: 36,
121-
marginTop: 16,
122-
padding: 0,
123-
paddingHorizontal: 16,
124-
},
125-
title: { fontSize: 17, fontWeight: '500', lineHeight: 20 },
126-
transparentContainer: {
127-
alignItems: 'center',
128-
backgroundColor: 'rgba(0, 0, 0, 0.4)',
129-
flex: 1,
130-
justifyContent: 'center',
131-
},
132-
});
100+
const useStyles = () => {
101+
const {
102+
theme: {
103+
semantics,
104+
poll: {
105+
inputDialog: { button },
106+
},
107+
},
108+
} = useTheme();
109+
return useMemo(
110+
() =>
111+
StyleSheet.create({
112+
button: { flex: 1, width: undefined, ...button },
113+
buttonContainer: { flexDirection: 'row', gap: primitives.spacingXs },
114+
container: {
115+
backgroundColor: semantics.backgroundCoreElevation1,
116+
borderRadius: primitives.radiusXl,
117+
paddingBottom: primitives.spacingXl,
118+
paddingHorizontal: primitives.spacingXl,
119+
paddingTop: primitives.spacing2xl,
120+
gap: primitives.spacing2xl,
121+
width: 304,
122+
},
123+
modalRoot: {
124+
flex: 1,
125+
},
126+
inputContainer: {
127+
gap: primitives.spacingXs,
128+
},
129+
input: {
130+
alignItems: 'center',
131+
borderColor: semantics.borderUtilityActive,
132+
borderRadius: primitives.radiusMd,
133+
borderWidth: 1,
134+
fontSize: primitives.typographyFontSizeMd,
135+
padding: primitives.spacingSm,
136+
color: semantics.textPrimary,
137+
},
138+
title: {
139+
color: semantics.textPrimary,
140+
fontSize: primitives.typographyFontSizeMd,
141+
fontWeight: primitives.typographyFontWeightMedium,
142+
lineHeight: primitives.typographyLineHeightNormal,
143+
},
144+
transparentContainer: {
145+
alignItems: 'center',
146+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
147+
flex: 1,
148+
justifyContent: 'center',
149+
},
150+
}),
151+
[button, semantics],
152+
);
153+
};

β€Žpackage/src/contexts/pollContext/createPollContentContext.tsxβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import React, { PropsWithChildren, useContext } from 'react';
22

3+
import { StateStore } from 'stream-chat';
4+
35
import { MessageInputContextValue } from '../messageInputContext/MessageInputContext';
46
import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue';
57

68
import { isTestEnvironment } from '../utils/isTestEnvironment';
79

10+
export type CreatePollModalState = {
11+
isClosing: boolean;
12+
};
13+
814
export type CreatePollContentContextValue = {
915
createAndSendPoll: () => Promise<void>;
1016
sendMessage: MessageInputContextValue['sendMessage'];
1117
closePollCreationDialog?: () => void;
18+
modalStateStore?: StateStore<CreatePollModalState>;
1219
/**
1320
* Vertical gap between poll options in the poll creation screen.
1421
*

β€Žpackage/src/contexts/themeContext/utils/theme.tsβ€Ž

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ export type Theme = {
206206
textContainer: ViewStyle;
207207
timestamp: ViewStyle;
208208
};
209-
colors: typeof Colors;
210209
channelPreview: {
211210
container: ViewStyle;
212211
contentContainer: ViewStyle;
@@ -1172,7 +1171,6 @@ export const defaultTheme: Theme = {
11721171
subtitle: {},
11731172
},
11741173
},
1175-
colors: Colors,
11761174
dateHeader: {
11771175
container: {},
11781176
text: {},

β€Žpackage/src/icons/utils/base.tsxβ€Ž

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React from 'react';
22
import Svg, { Path, PathProps, SvgProps } from 'react-native-svg';
33

4-
import { useTheme } from '../../contexts/themeContext/ThemeContext';
5-
64
export type IconProps = Partial<SvgProps> &
75
Omit<RootPathProps, 'd'> & {
86
height?: number;
@@ -32,13 +30,7 @@ export type RootPathProps = Pick<PathProps, 'd'> & {
3230
};
3331

3432
export const RootPath = (props: RootPathProps) => {
35-
const {
36-
theme: {
37-
colors: { black },
38-
},
39-
} = useTheme();
40-
41-
const { d, pathFill = black, pathOpacity } = props;
33+
const { d, pathFill = 'black', pathOpacity } = props;
4234
return (
4335
<Path
4436
{...{

0 commit comments

Comments
Β (0)