Skip to content

Commit 2f42d10

Browse files
authored
fix: poll screen design fixes (#3508)
## 🎯 Goal This PR adds a final round of design fixes for the poll screens. In addition it fixes a couple of miscellaneous bugs. Most notably: - Answer screen redesign - Poll results screen redesign (items themselves as well as general spacing) - Inline button locations - Refresh control issues - Votes screen redesign (colors and general business logic) - Poll fill logic ## 🛠 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 1495d0d commit 2f42d10

9 files changed

Lines changed: 167 additions & 65 deletions

File tree

package/src/components/ChannelList/__tests__/ChannelList.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import dispatchChannelDeletedEvent from '../../../mock-builders/event/channelDel
2020
import dispatchChannelHiddenEvent from '../../../mock-builders/event/channelHidden';
2121
import dispatchChannelTruncatedEvent from '../../../mock-builders/event/channelTruncated';
2222
import dispatchChannelUpdatedEvent from '../../../mock-builders/event/channelUpdated';
23+
import dispatchConnectionChangedEvent from '../../../mock-builders/event/connectionChanged';
2324
import dispatchConnectionRecoveredEvent from '../../../mock-builders/event/connectionRecovered';
2425
import dispatchMessageNewEvent from '../../../mock-builders/event/messageNew';
2526
import dispatchNotificationAddedToChannelEvent from '../../../mock-builders/event/notificationAddedToChannel';
@@ -75,6 +76,11 @@ const ChannelListSwipeActionsProbe = () => {
7576
return <Text testID='swipe-actions-enabled'>{`${swipeActionsEnabled}`}</Text>;
7677
};
7778

79+
const ChannelListRefreshingProbe = () => {
80+
const { refreshing } = useChannelsContext();
81+
return <Text testID='refreshing'>{`${refreshing}`}</Text>;
82+
};
83+
7884
const ChannelPreviewContent = ({ unread }) => <Text testID='preview-unread'>{`${unread}`}</Text>;
7985

8086
const ChannelListWithChannelPreview = () => {
@@ -805,6 +811,43 @@ describe('ChannelList', () => {
805811
});
806812
});
807813

814+
describe('connection.changed', () => {
815+
it('should keep background reconnection refreshes debounced and out of pull-to-refresh UI', async () => {
816+
useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);
817+
const deferredPromise = new DeferredPromise();
818+
const dateNowSpy = jest.spyOn(Date, 'now');
819+
dateNowSpy.mockReturnValueOnce(0);
820+
dateNowSpy.mockReturnValue(6000);
821+
822+
render(
823+
<Chat client={chatClient}>
824+
<ChannelList {...props} List={ChannelListRefreshingProbe} />
825+
</Chat>,
826+
);
827+
828+
await waitFor(() => {
829+
expect(screen.getByTestId('refreshing').children[0]).toBe('false');
830+
});
831+
832+
chatClient.queryChannels = jest.fn(() => deferredPromise.promise);
833+
834+
act(() => dispatchConnectionChangedEvent(chatClient, false));
835+
act(() => dispatchConnectionChangedEvent(chatClient, true));
836+
837+
await waitFor(() => {
838+
expect(chatClient.queryChannels).toHaveBeenCalled();
839+
});
840+
841+
act(() => dispatchConnectionChangedEvent(chatClient, true));
842+
843+
expect(chatClient.queryChannels).toHaveBeenCalledTimes(1);
844+
expect(screen.getByTestId('refreshing').children[0]).toBe('false');
845+
846+
deferredPromise.resolve([testChannel1]);
847+
dateNowSpy.mockRestore();
848+
});
849+
});
850+
808851
describe('channel.truncated', () => {
809852
it('should call the `onChannelTruncated` function prop, if provided', async () => {
810853
useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);

package/src/components/ChannelList/hooks/usePaginatedChannels.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Parameters = {
2424

2525
const RETRY_INTERVAL_IN_MS = 5000;
2626

27-
type QueryType = 'queryLocalDB' | 'reload' | 'refresh' | 'loadChannels';
27+
type QueryType = 'queryLocalDB' | 'reload' | 'refresh' | 'loadChannels' | 'backgroundRefresh';
2828

2929
export type QueryChannels = (queryType?: QueryType, retryCount?: number) => Promise<void>;
3030

@@ -68,6 +68,7 @@ export const usePaginatedChannels = ({
6868
const hasUpdatedData =
6969
queryType === 'loadChannels' ||
7070
queryType === 'refresh' ||
71+
queryType === 'backgroundRefresh' ||
7172
[
7273
JSON.stringify(filtersRef.current) !== JSON.stringify(filters),
7374
JSON.stringify(sortRef.current) !== JSON.stringify(sort),
@@ -129,15 +130,15 @@ export const usePaginatedChannels = ({
129130
setActiveQueryType(null);
130131
};
131132

132-
const refreshList = async () => {
133+
const refreshList = async ({ isBackground = false }: { isBackground?: boolean } = {}) => {
133134
const now = Date.now();
134135
// Only allow pull-to-refresh 5 seconds after last successful refresh.
135136
if (now - lastRefresh.current < RETRY_INTERVAL_IN_MS && error === undefined) {
136137
return;
137138
}
138139

139140
lastRefresh.current = Date.now();
140-
await queryChannels('refresh');
141+
await queryChannels(isBackground ? 'backgroundRefresh' : 'refresh');
141142
};
142143

143144
const reloadList = async () => {
@@ -167,7 +168,9 @@ export const usePaginatedChannels = ({
167168
'connection.changed',
168169
async (event) => {
169170
if (event.online) {
170-
await refreshList();
171+
// Reconnection refreshes should stay silent, but still share the same debounce
172+
// path as pull-to-refresh.
173+
await refreshList({ isBackground: true });
171174
}
172175
},
173176
);
@@ -195,7 +198,7 @@ export const usePaginatedChannels = ({
195198
loadingNextPage: pagination?.isLoadingNext,
196199
loadNextPage: channelManager.loadNext,
197200
refreshing: activeQueryType === 'refresh',
198-
refreshList,
201+
refreshList: () => refreshList(),
199202
reloadList,
200203
staticChannelsActive,
201204
};

package/src/components/MessageMenu/MessageUserReactions.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => {
291291
</Text>
292292
<View style={[styles.reactionSelectorContainer, reactionSelectorContainer]}>
293293
<FlatList
294+
showsHorizontalScrollIndicator={false}
294295
contentContainerStyle={[styles.contentContainer, contentContainer]}
295296
data={selectorReactions}
296297
getItemLayout={getItemLayout}

package/src/components/Poll/components/PollAnswersList.tsx

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PollInputDialog } from './PollInputDialog';
99
import {
1010
PollContextProvider,
1111
PollContextValue,
12+
useChatContext,
1213
usePollContext,
1314
useTheme,
1415
useTranslationContext,
@@ -27,6 +28,8 @@ export const AnswerListAddCommentButton = (props: PollButtonProps) => {
2728
const [showAddCommentDialog, setShowAddCommentDialog] = useState(false);
2829
const { onPress } = props;
2930

31+
const styles = useStyles();
32+
3033
const onPressHandler = useCallback(() => {
3134
if (onPress) {
3235
onPress({ message, poll });
@@ -37,10 +40,10 @@ export const AnswerListAddCommentButton = (props: PollButtonProps) => {
3740
}, [message, onPress, poll]);
3841

3942
return (
40-
<>
43+
<View style={styles.inlineButton}>
4144
<Button
4245
variant={'secondary'}
43-
type={'outline'}
46+
type={'ghost'}
4447
size={'lg'}
4548
label={ownAnswer ? t('Update your comment') : t('Add a comment')}
4649
onPress={onPressHandler}
@@ -54,7 +57,7 @@ export const AnswerListAddCommentButton = (props: PollButtonProps) => {
5457
visible={showAddCommentDialog}
5558
/>
5659
) : null}
57-
</>
60+
</View>
5861
);
5962
};
6063

@@ -64,6 +67,7 @@ export type PollAnswersListProps = PollContextValue & {
6467
};
6568

6669
export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => {
70+
const { client } = useChatContext();
6771
const { t, tDateTimeParser } = useTranslationContext();
6872
const { votingVisibility } = usePollState();
6973

@@ -87,25 +91,32 @@ export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => {
8791
[answer.updated_at, t, tDateTimeParser],
8892
);
8993

94+
const isMyAnswer = client.userID === answer.user?.id;
95+
9096
const isAnonymous = useMemo(
91-
() => votingVisibility === VotingVisibility.anonymous,
92-
[votingVisibility],
97+
() => votingVisibility === VotingVisibility.anonymous && !isMyAnswer,
98+
[votingVisibility, isMyAnswer],
9399
);
94100

101+
const answerAuthorName = isMyAnswer ? t('You') : answer.user?.name;
102+
95103
return (
96-
<View style={[styles.listItemContainer, itemStyle.container]}>
97-
<Text style={[styles.listItemAnswerText, itemStyle.answerText]}>{answer.answer_text}</Text>
98-
<View style={[styles.listItemInfoContainer, itemStyle.infoContainer]}>
99-
<View style={[styles.listItemUserInfoContainer, itemStyle.userInfoContainer]}>
100-
{!isAnonymous && answer.user?.image ? (
101-
<UserAvatar user={answer.user} size='md' showBorder />
102-
) : null}
103-
<Text style={styles.listItemInfoUserName}>
104-
{isAnonymous ? t('Anonymous') : answer.user?.name}
105-
</Text>
104+
<View style={[styles.listItemWrapper, itemStyle.wrapper]}>
105+
<View style={[styles.listItemContainer, itemStyle.container]}>
106+
<Text style={[styles.listItemAnswerText, itemStyle.answerText]}>{answer.answer_text}</Text>
107+
<View style={[styles.listItemInfoContainer, itemStyle.infoContainer]}>
108+
<View style={[styles.listItemUserInfoContainer, itemStyle.userInfoContainer]}>
109+
{!isAnonymous && answer.user?.image ? (
110+
<UserAvatar user={answer.user} size='sm' showBorder />
111+
) : null}
112+
<Text style={styles.listItemInfoUserName}>
113+
{isAnonymous ? t('Anonymous') : answerAuthorName}
114+
</Text>
115+
<Text style={styles.listItemInfoDate}>{dateString}</Text>
116+
</View>
106117
</View>
107-
<Text style={styles.listItemInfoDate}>{dateString}</Text>
108118
</View>
119+
{isMyAnswer ? <AnswerListAddCommentButton /> : null}
109120
</View>
110121
);
111122
};
@@ -137,7 +148,6 @@ export const PollAnswersListContent = ({
137148
renderItem={renderPollAnswerListItem}
138149
{...additionalFlatListProps}
139150
/>
140-
<AnswerListAddCommentButton />
141151
</View>
142152
);
143153
};
@@ -164,46 +174,49 @@ const useStyles = () => {
164174
return useMemo(
165175
() =>
166176
StyleSheet.create({
167-
addCommentButtonContainer: {
168-
alignItems: 'center',
169-
borderRadius: 12,
170-
paddingHorizontal: 16,
171-
paddingVertical: 18,
172-
},
173177
contentContainer: { gap: primitives.spacingMd },
174-
addCommentButtonText: { fontSize: 16 },
175178
container: {
176179
flex: 1,
177180
padding: primitives.spacingMd,
178181
backgroundColor: semantics.backgroundCoreElevation1,
179182
},
180183
listItemAnswerText: {
181184
fontSize: primitives.typographyFontSizeMd,
182-
lineHeight: primitives.typographyLineHeightRelaxed,
183-
fontWeight: primitives.typographyFontWeightSemiBold,
185+
lineHeight: primitives.typographyLineHeightNormal,
184186
color: semantics.textPrimary,
185187
},
186-
listItemContainer: {
188+
listItemWrapper: {
187189
borderRadius: primitives.radiusLg,
188-
padding: primitives.spacingMd,
189190
backgroundColor: semantics.backgroundCoreSurfaceCard,
190191
},
192+
listItemContainer: {
193+
padding: primitives.spacingMd,
194+
gap: primitives.spacingXs,
195+
},
191196
listItemInfoContainer: {
192197
flexDirection: 'row',
193198
justifyContent: 'space-between',
194199
alignItems: 'center',
195-
marginTop: 24,
196200
},
197201
listItemInfoUserName: {
198-
color: semantics.textPrimary,
202+
color: semantics.chatTextUsername,
199203
fontSize: primitives.typographyFontSizeSm,
200-
marginLeft: primitives.spacingXxs,
204+
fontWeight: primitives.typographyFontWeightSemiBold,
205+
lineHeight: primitives.typographyLineHeightNormal,
201206
},
202207
listItemInfoDate: {
203208
fontSize: primitives.typographyFontSizeSm,
204209
color: semantics.textTertiary,
205210
},
206-
listItemUserInfoContainer: { alignItems: 'center', flexDirection: 'row' },
211+
listItemUserInfoContainer: {
212+
gap: primitives.spacingXs,
213+
alignItems: 'center',
214+
flexDirection: 'row',
215+
},
216+
inlineButton: {
217+
borderColor: semantics.borderCoreDefault,
218+
borderTopWidth: 1,
219+
},
207220
}),
208221
[semantics],
209222
);

package/src/components/Poll/components/PollOption.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
useOwnCapabilitiesContext,
1515
usePollContext,
1616
useTheme,
17+
useTranslationContext,
1718
} from '../../../contexts';
1819

1920
import { Check } from '../../../icons';
@@ -26,6 +27,7 @@ import { usePollState } from '../hooks/usePollState';
2627
export type PollOptionProps = {
2728
option: PollOptionClass;
2829
showProgressBar?: boolean;
30+
forceIncoming?: boolean;
2931
};
3032

3133
export type PollAllOptionsContentProps = PollContextValue & {
@@ -36,6 +38,7 @@ export type PollAllOptionsContentProps = PollContextValue & {
3638
export const PollAllOptionsContent = ({
3739
additionalScrollViewProps,
3840
}: Pick<PollAllOptionsContentProps, 'additionalScrollViewProps'>) => {
41+
const { t } = useTranslationContext();
3942
const { name, options } = usePollState();
4043

4144
const {
@@ -50,12 +53,13 @@ export const PollAllOptionsContent = ({
5053
return (
5154
<ScrollView style={[styles.allOptionsWrapper, wrapper]} {...additionalScrollViewProps}>
5255
<View style={[styles.allOptionsTitleContainer, titleContainer]}>
56+
<Text style={styles.allOptionsTitleMeta}>{t('Question')}</Text>
5357
<Text style={[styles.allOptionsTitleText, titleText]}>{name}</Text>
5458
</View>
5559
<View style={[styles.allOptionsListContainer, listContainer]}>
5660
{options?.map((option: PollOptionClass) => (
5761
<View key={`full_poll_options_${option.id}`} style={styles.optionWrapper}>
58-
<PollOption key={option.id} option={option} showProgressBar={false} />
62+
<PollOption key={option.id} option={option} forceIncoming />
5963
</View>
6064
))}
6165
</View>
@@ -78,19 +82,15 @@ export const PollAllOptions = ({
7882
</PollContextProvider>
7983
);
8084

81-
export const PollOption = ({ option, showProgressBar = true }: PollOptionProps) => {
82-
const { latestVotesByOption, maxVotedOptionIds, voteCountsByOption } = usePollState();
85+
export const PollOption = ({ option, showProgressBar = true, forceIncoming }: PollOptionProps) => {
86+
const { latestVotesByOption, voteCountsByOption, voteCount } = usePollState();
8387
const styles = useStyles();
8488

8589
const relevantVotes = useMemo(
8690
() => latestVotesByOption?.[option.id] || [],
8791
[latestVotesByOption, option.id],
8892
);
89-
const maxVotes = useMemo(
90-
() =>
91-
maxVotedOptionIds?.[0] && voteCountsByOption ? voteCountsByOption[maxVotedOptionIds[0]] : 0,
92-
[maxVotedOptionIds, voteCountsByOption],
93-
);
93+
9494
const votes = voteCountsByOption[option.id] || 0;
9595

9696
const {
@@ -105,13 +105,15 @@ export const PollOption = ({ option, showProgressBar = true }: PollOptionProps)
105105
} = useTheme();
106106
const isPollCreatedByClient = useIsPollCreatedByCurrentUser();
107107

108-
const unFilledColor = isPollCreatedByClient
109-
? semantics.chatPollProgressTrackOutgoing
110-
: semantics.chatPollProgressTrackIncoming;
108+
const unFilledColor =
109+
isPollCreatedByClient && !forceIncoming
110+
? semantics.chatPollProgressTrackOutgoing
111+
: semantics.chatPollProgressTrackIncoming;
111112

112-
const filledColor = isPollCreatedByClient
113-
? semantics.chatPollProgressFillOutgoing
114-
: semantics.chatPollProgressFillIncoming;
113+
const filledColor =
114+
isPollCreatedByClient && !forceIncoming
115+
? semantics.chatPollProgressFillOutgoing
116+
: semantics.chatPollProgressFillIncoming;
115117

116118
return (
117119
<View style={[styles.container, container]}>
@@ -133,7 +135,7 @@ export const PollOption = ({ option, showProgressBar = true }: PollOptionProps)
133135
{showProgressBar ? (
134136
<View style={styles.progressBarContainer}>
135137
<ProgressBar
136-
progress={votes / maxVotes}
138+
progress={votes / voteCount}
137139
filledColor={filledColor}
138140
emptyColor={unFilledColor}
139141
/>
@@ -273,6 +275,13 @@ const useAllOptionStyles = () => {
273275
lineHeight: primitives.typographyLineHeightRelaxed,
274276
fontWeight: primitives.typographyFontWeightSemiBold,
275277
color: semantics.textPrimary,
278+
paddingTop: primitives.spacingXs,
279+
},
280+
allOptionsTitleMeta: {
281+
fontSize: primitives.typographyFontSizeSm,
282+
color: semantics.textTertiary,
283+
lineHeight: primitives.typographyLineHeightNormal,
284+
fontWeight: primitives.typographyFontWeightMedium,
276285
},
277286
allOptionsWrapper: {
278287
flex: 1,

0 commit comments

Comments
 (0)