Skip to content

Commit 46aaec7

Browse files
authored
fix: poll redesign UI fixes (#3483)
## 🎯 Goal This PR fixes a bunch of issues with the new designs with regards to the various poll flows. It does not yet address all of them, as for some it makes more sense to open another PR. As a brief summary: - Implements the new design for assigning maximum votes per person - Changes the default limits of poll options within the message component to 5 - Implements the new design for the `ShowAllOptions` button - Prevents casting a vote when we suggest an option - Fixes Android hit testing for poll modals - Enables audio seeking on Android ## 🛠 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 458c24c commit 46aaec7

File tree

29 files changed

+521
-244
lines changed

29 files changed

+521
-244
lines changed

examples/SampleApp/src/screens/ChannelScreen.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,9 @@ const ChannelHeader: React.FC<ChannelHeaderProps> = ({ channel }) => {
117117
// Either provide channel or channelId.
118118
export const ChannelScreen: React.FC<ChannelScreenProps> = ({
119119
navigation,
120-
route: {
121-
params: { channel: channelFromProp, channelId, messageId },
122-
},
120+
route,
123121
}) => {
122+
const { channel: channelFromProp, channelId, messageId } = route.params;
124123
const {
125124
chatClient,
126125
messageListImplementation,
@@ -180,33 +179,44 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
180179
if (!thread || !channel) {
181180
return;
182181
}
182+
183+
if (messageId) {
184+
navigation.setParams({ messageId: undefined });
185+
}
186+
183187
setSelectedThread(thread);
184188
setThread(thread);
185189
navigation.navigate('ThreadScreen', {
186190
channel,
187191
thread,
192+
targetedMessageId: undefined,
188193
});
189194
},
190-
[channel, navigation, setThread],
195+
[channel, messageId, navigation, setThread],
191196
);
192197

193198
const onAlsoSentToChannelHeaderPress = useCallback(
194199
async ({ parentMessage, targetedMessageId }: AlsoSentToChannelHeaderPressPayload) => {
195200
if (!channel || !parentMessage) {
196201
return;
197202
}
203+
204+
if (messageId) {
205+
navigation.setParams({ messageId: undefined });
206+
}
207+
198208
setSelectedThread(parentMessage);
199209
setThread(parentMessage);
200210
const params: StackNavigatorParamList['ThreadScreen'] = {
201211
channel,
202212
targetedMessageId,
203213
thread: parentMessage,
204214
};
205-
const hasThreadInStack = navigation.getState().routes.some((route) => {
206-
if (route.name !== 'ThreadScreen') {
215+
const hasThreadInStack = navigation.getState().routes.some((stackRoute) => {
216+
if (stackRoute.name !== 'ThreadScreen') {
207217
return false;
208218
}
209-
const routeParams = route.params as StackNavigatorParamList['ThreadScreen'] | undefined;
219+
const routeParams = stackRoute.params as StackNavigatorParamList['ThreadScreen'] | undefined;
210220
const routeThreadId =
211221
(routeParams?.thread as LocalMessage)?.id ??
212222
(routeParams?.thread as ThreadType)?.thread?.id;
@@ -221,7 +231,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
221231

222232
navigation.navigate('ThreadScreen', params);
223233
},
224-
[channel, navigation, setThread],
234+
[channel, messageId, navigation, setThread],
225235
);
226236

227237
const handleMessageInfo = useCallback((message: LocalMessage) => {

examples/SampleApp/src/screens/ThreadScreen.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,9 @@ const ThreadHeader: React.FC<ThreadHeaderProps> = ({ thread }) => {
7474

7575
export const ThreadScreen: React.FC<ThreadScreenProps> = ({
7676
navigation,
77-
route: {
78-
params: { channel, thread, targetedMessageId: targetedMessageIdFromParams },
79-
},
77+
route,
8078
}) => {
79+
const { channel, thread, targetedMessageId: targetedMessageIdFromParams } = route.params;
8180
const {
8281
theme: {
8382
semantics,
@@ -88,6 +87,7 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
8887
const { t } = useTranslationContext();
8988
const { setThread } = useStreamChatContext();
9089
const { messageInputFloating, messageListImplementation } = useAppContext();
90+
9191
const onPressMessage: NonNullable<React.ComponentProps<typeof Channel>['onPressMessage']> = (
9292
payload,
9393
) => {
@@ -126,11 +126,11 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
126126
};
127127
const hasChannelInStack = navigation
128128
.getState()
129-
.routes.some((route) => {
130-
if (route.name !== 'ChannelScreen') {
129+
.routes.some((stackRoute) => {
130+
if (stackRoute.name !== 'ChannelScreen') {
131131
return false;
132132
}
133-
const routeParams = route.params as StackNavigatorParamList['ChannelScreen'] | undefined;
133+
const routeParams = stackRoute.params as StackNavigatorParamList['ChannelScreen'] | undefined;
134134
const routeChannelId = routeParams?.channel?.id ?? routeParams?.channelId;
135135
return routeChannelId === channel.id;
136136
});

package/src/components/Poll/CreatePollContent.tsx

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { StyleSheet, Switch, Text, View } from 'react-native';
33

44
import { ScrollView } from 'react-native-gesture-handler';
5-
import { useSharedValue } from 'react-native-reanimated';
5+
import Animated, { LinearTransition, useSharedValue } from 'react-native-reanimated';
66

77
import { PollComposerState, VotingVisibility } from 'stream-chat';
88

@@ -153,49 +153,57 @@ export const CreatePollContent = () => {
153153
>
154154
<NameField />
155155
<CreatePollOptions currentOptionPositions={currentOptionPositions} />
156-
<View style={[styles.optionCardWrapper, optionCardWrapper]}>
156+
<Animated.View
157+
layout={LinearTransition.duration(200)}
158+
style={[styles.optionCardWrapper, optionCardWrapper]}
159+
>
157160
<MultipleAnswersField />
158-
<View style={[styles.optionCard, anonymousPoll.wrapper]}>
159-
<View style={[styles.optionCardContent, anonymousPoll.optionCardContent]}>
160-
<Text style={[styles.title, anonymousPoll.title]}>{t('Anonymous voting')}</Text>
161-
<Text style={[styles.description, anonymousPoll.description]}>Hide who voted</Text>
162-
</View>
161+
<Animated.View
162+
layout={LinearTransition.duration(200)}
163+
style={[styles.optionCardWrapper, optionCardWrapper]}
164+
>
165+
<View style={[styles.optionCard, anonymousPoll.wrapper]}>
166+
<View style={[styles.optionCardContent, anonymousPoll.optionCardContent]}>
167+
<Text style={[styles.title, anonymousPoll.title]}>{t('Anonymous voting')}</Text>
168+
<Text style={[styles.description, anonymousPoll.description]}>Hide who voted</Text>
169+
</View>
163170

164-
<Switch
165-
onValueChange={onAnonymousPollChangeHandler}
166-
value={isAnonymousPoll}
167-
style={[styles.optionCardSwitch, anonymousPoll.optionCardSwitch]}
168-
/>
169-
</View>
170-
<View style={[styles.optionCard, suggestOption.wrapper]}>
171-
<View style={[styles.optionCardContent, suggestOption.optionCardContent]}>
172-
<Text style={[styles.title, suggestOption.title]}>{t('Suggest an option')}</Text>
173-
<Text style={[styles.description, suggestOption.description]}>
174-
Let others add options
175-
</Text>
171+
<Switch
172+
onValueChange={onAnonymousPollChangeHandler}
173+
value={isAnonymousPoll}
174+
style={[styles.optionCardSwitch, anonymousPoll.optionCardSwitch]}
175+
/>
176176
</View>
177+
<View style={[styles.optionCard, suggestOption.wrapper]}>
178+
<View style={[styles.optionCardContent, suggestOption.optionCardContent]}>
179+
<Text style={[styles.title, suggestOption.title]}>{t('Suggest an option')}</Text>
180+
<Text style={[styles.description, suggestOption.description]}>
181+
Let others add options
182+
</Text>
183+
</View>
177184

178-
<Switch
179-
onValueChange={onAllowUserSuggestedOptionsChangeHandler}
180-
value={allowUserSuggestedOptions}
181-
style={[styles.optionCardSwitch, suggestOption.optionCardSwitch]}
182-
/>
183-
</View>
184-
<View style={[styles.optionCard, addComment.wrapper]}>
185-
<View style={[styles.optionCardContent, addComment.optionCardContent]}>
186-
<Text style={[styles.title, addComment.title]}>{t('Add a comment')}</Text>
187-
<Text style={[styles.description, addComment.description]}>
188-
Add a comment to the poll
189-
</Text>
185+
<Switch
186+
onValueChange={onAllowUserSuggestedOptionsChangeHandler}
187+
value={allowUserSuggestedOptions}
188+
style={[styles.optionCardSwitch, suggestOption.optionCardSwitch]}
189+
/>
190190
</View>
191+
<View style={[styles.optionCard, addComment.wrapper]}>
192+
<View style={[styles.optionCardContent, addComment.optionCardContent]}>
193+
<Text style={[styles.title, addComment.title]}>{t('Add a comment')}</Text>
194+
<Text style={[styles.description, addComment.description]}>
195+
Add a comment to the poll
196+
</Text>
197+
</View>
191198

192-
<Switch
193-
onValueChange={onAllowAnswersChangeHandler}
194-
value={allowAnswers}
195-
style={[styles.optionCardSwitch, addComment.optionCardSwitch]}
196-
/>
197-
</View>
198-
</View>
199+
<Switch
200+
onValueChange={onAllowAnswersChangeHandler}
201+
value={allowAnswers}
202+
style={[styles.optionCardSwitch, addComment.optionCardSwitch]}
203+
/>
204+
</View>
205+
</Animated.View>
206+
</Animated.View>
199207
</ScrollView>
200208
</>
201209
);

package/src/components/Poll/Poll.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native';
33

44
import { PollOption as PollOptionClass } from 'stream-chat';
55

6-
import { PollButtons, PollOption } from './components';
6+
import { PollButtons, PollOption, ShowAllOptionsButton } from './components';
77

88
import { usePollState } from './hooks/usePollState';
99

@@ -16,6 +16,7 @@ import {
1616
} from '../../contexts';
1717

1818
import { primitives } from '../../theme';
19+
import { defaultPollOptionCount } from '../../utils/constants';
1920

2021
export type PollProps = Pick<PollContextValue, 'poll' | 'message'> &
2122
Pick<MessagesContextValue, 'PollContent'>;
@@ -79,10 +80,11 @@ export const PollContent = ({
7980
{PollHeaderOverride ? <PollHeaderOverride /> : <PollHeader />}
8081
<View style={[styles.optionsWrapper, optionsWrapper]}>
8182
{options
82-
?.slice(0, 10)
83+
?.slice(0, defaultPollOptionCount)
8384
?.map((option: PollOptionClass) => (
8485
<PollOption key={`message_poll_option_${option.id}`} option={option} />
8586
))}
87+
<ShowAllOptionsButton />
8688
</View>
8789
{PollButtonsOverride ? <PollButtonsOverride /> : <PollButtons />}
8890
</View>

package/src/components/Poll/components/CreatePollOptions.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import Animated, {
1212
useSharedValue,
1313
withDelay,
1414
withSpring,
15-
withTiming,
1615
} from 'react-native-reanimated';
1716

1817
import { PollComposerOption, PollComposerState } from 'stream-chat';
@@ -580,11 +579,11 @@ export const CreatePollOptions = ({ currentOptionPositions }: CreatePollOptionsP
580579
useAnimatedReaction(
581580
() => currentOptionPositions.value.totalHeight,
582581
(currentValue, previousValue) => {
583-
if (currentValue !== previousValue) {
584-
animatedOptionsContainerHeight.value = withTiming(currentValue, {
585-
duration: 200,
586-
});
582+
if (currentValue === previousValue) {
583+
return;
587584
}
585+
586+
animatedOptionsContainerHeight.value = currentValue;
588587
},
589588
);
590589

@@ -707,7 +706,7 @@ export const CreatePollOptions = ({ currentOptionPositions }: CreatePollOptionsP
707706
return (
708707
<View style={[styles.container, container]}>
709708
<Text style={[styles.title, title]}>{t('Options')}</Text>
710-
<Animated.View style={animatedOptionsContainerStyle}>
709+
<Animated.View layout={LinearTransition.duration(200)} style={animatedOptionsContainerStyle}>
711710
{options.map((option, index) => (
712711
<MemoizedCreatePollOption
713712
optionsCount={options.length}

package/src/components/Poll/components/MultipleAnswersField.tsx

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
import React, { useCallback, useMemo, useState } from 'react';
22
import { StyleSheet, Switch, Text, View } from 'react-native';
33

4-
import { PollComposerState } from 'stream-chat';
4+
import Animated, { LinearTransition } from 'react-native-reanimated';
5+
6+
import { MultipleVotesSettings } from './MultipleVotesSettings';
57

68
import { useTheme, useTranslationContext } from '../../../contexts';
79
import { useMessageComposer } from '../../../contexts/messageInputContext/hooks/useMessageComposer';
8-
import { useStateStore } from '../../../hooks/useStateStore';
910
import { primitives } from '../../../theme';
10-
import { Input } from '../../ui/Input/Input';
11-
12-
const pollComposerStateSelector = (state: PollComposerState) => ({
13-
error: state.errors.max_votes_allowed,
14-
max_votes_allowed: state.data.max_votes_allowed,
15-
});
1611

1712
export const MultipleAnswersField = () => {
1813
const [allowMultipleVotes, setAllowMultipleVotes] = useState<boolean>(false);
1914
const { t } = useTranslationContext();
2015
const messageComposer = useMessageComposer();
2116
const { pollComposer } = messageComposer;
22-
const { handleFieldBlur, updateFields } = pollComposer;
23-
const { error, max_votes_allowed } = useStateStore(pollComposer.state, pollComposerStateSelector);
17+
const { updateFields } = pollComposer;
2418

2519
const {
2620
theme: {
@@ -40,24 +34,16 @@ export const MultipleAnswersField = () => {
4034
[updateFields],
4135
);
4236

43-
const onChangeTextHandler = useCallback(
44-
async (newText: string) => {
45-
await updateFields({ max_votes_allowed: newText });
46-
},
47-
[updateFields],
48-
);
49-
50-
const onBlurHandler = useCallback(async () => {
51-
await handleFieldBlur('max_votes_allowed');
52-
}, [handleFieldBlur]);
53-
5437
return (
55-
<View style={[styles.multipleAnswersWrapper, multipleAnswers.wrapper]}>
38+
<Animated.View
39+
layout={LinearTransition.duration(200)}
40+
style={[styles.multipleAnswersWrapper, multipleAnswers.wrapper]}
41+
>
5642
<View style={[styles.optionCard, multipleAnswers.optionCard]}>
5743
<View style={[styles.optionCardContent, multipleAnswers.optionCardContent]}>
58-
<Text style={[styles.title, multipleAnswers.title]}>{t('Multiple answers')}</Text>
44+
<Text style={[styles.title, multipleAnswers.title]}>{t('Multiple votes')}</Text>
5945
<Text style={[styles.description, multipleAnswers.description]}>
60-
Select more than one option
46+
{t('Select more than one option')}
6147
</Text>
6248
</View>
6349
<Switch
@@ -66,21 +52,8 @@ export const MultipleAnswersField = () => {
6652
style={[styles.optionCardSwitch, multipleAnswers.optionCardSwitch]}
6753
/>
6854
</View>
69-
{allowMultipleVotes ? (
70-
<Input
71-
inputMode='numeric'
72-
placeholder={t('Maximum votes per person')}
73-
variant='ghost'
74-
state={max_votes_allowed && error ? 'error' : 'default'}
75-
onChangeText={onChangeTextHandler}
76-
onBlur={onBlurHandler}
77-
helperText={true}
78-
infoText={t('Type a number from 2 to 10')}
79-
errorMessage={error ? t(error) : undefined}
80-
containerStyle={styles.maxVotesInput}
81-
/>
82-
) : null}
83-
</View>
55+
{allowMultipleVotes ? <MultipleVotesSettings /> : null}
56+
</Animated.View>
8457
);
8558
};
8659

@@ -90,14 +63,11 @@ const useStyles = () => {
9063
} = useTheme();
9164
return useMemo(() => {
9265
return StyleSheet.create({
93-
maxVotesInput: {
94-
paddingLeft: 0,
95-
},
9666
multipleAnswersWrapper: {
9767
backgroundColor: semantics.inputOptionCardBg,
9868
padding: primitives.spacingMd,
9969
borderRadius: primitives.radiusLg,
100-
gap: primitives.spacingSm,
70+
gap: primitives.spacingMd,
10171
},
10272
title: {
10373
color: semantics.textPrimary,
@@ -119,9 +89,6 @@ const useStyles = () => {
11989
justifyContent: 'space-between',
12090
flexDirection: 'row',
12191
},
122-
optionCardWrapper: {
123-
gap: primitives.spacingMd,
124-
},
12592
optionCardSwitch: { width: 64 },
12693
});
12794
}, [semantics]);

0 commit comments

Comments
 (0)