stream-chat-react-native-core 9.7.3 → 9.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/lib/commonjs/components/Attachment/Attachment.js +1 -1
  2. package/lib/commonjs/components/Attachment/Attachment.js.map +1 -1
  3. package/lib/commonjs/components/Attachment/Audio/PlayPauseButton.js +1 -1
  4. package/lib/commonjs/components/Attachment/Audio/PlayPauseButton.js.map +1 -1
  5. package/lib/commonjs/components/Attachment/UrlPreview/URLPreview.js +16 -18
  6. package/lib/commonjs/components/Attachment/UrlPreview/URLPreview.js.map +1 -1
  7. package/lib/commonjs/components/Attachment/UrlPreview/URLPreviewCompact.js +16 -18
  8. package/lib/commonjs/components/Attachment/UrlPreview/URLPreviewCompact.js.map +1 -1
  9. package/lib/commonjs/components/Message/MessageItemView/MessageItemView.js +1 -2
  10. package/lib/commonjs/components/Message/MessageItemView/MessageItemView.js.map +1 -1
  11. package/lib/commonjs/components/MessageList/MessageFlashList.js +3 -1
  12. package/lib/commonjs/components/MessageList/MessageFlashList.js.map +1 -1
  13. package/lib/commonjs/components/MessageList/MessageList.js +3 -1
  14. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  15. package/lib/commonjs/components/Poll/Poll.js +5 -3
  16. package/lib/commonjs/components/Poll/Poll.js.map +1 -1
  17. package/lib/commonjs/components/Poll/components/Button.js.map +1 -1
  18. package/lib/commonjs/components/Poll/components/PollButtons.js.map +1 -1
  19. package/lib/commonjs/components/Poll/components/PollOption.js +20 -10
  20. package/lib/commonjs/components/Poll/components/PollOption.js.map +1 -1
  21. package/lib/commonjs/components/Reply/Reply.js +19 -10
  22. package/lib/commonjs/components/Reply/Reply.js.map +1 -1
  23. package/lib/commonjs/contexts/themeContext/ThemeContext.js +8 -1
  24. package/lib/commonjs/contexts/themeContext/ThemeContext.js.map +1 -1
  25. package/lib/commonjs/version.json +1 -1
  26. package/lib/module/components/Attachment/Attachment.js +1 -1
  27. package/lib/module/components/Attachment/Attachment.js.map +1 -1
  28. package/lib/module/components/Attachment/Audio/PlayPauseButton.js +1 -1
  29. package/lib/module/components/Attachment/Audio/PlayPauseButton.js.map +1 -1
  30. package/lib/module/components/Attachment/UrlPreview/URLPreview.js +16 -18
  31. package/lib/module/components/Attachment/UrlPreview/URLPreview.js.map +1 -1
  32. package/lib/module/components/Attachment/UrlPreview/URLPreviewCompact.js +16 -18
  33. package/lib/module/components/Attachment/UrlPreview/URLPreviewCompact.js.map +1 -1
  34. package/lib/module/components/Message/MessageItemView/MessageItemView.js +1 -2
  35. package/lib/module/components/Message/MessageItemView/MessageItemView.js.map +1 -1
  36. package/lib/module/components/MessageList/MessageFlashList.js +3 -1
  37. package/lib/module/components/MessageList/MessageFlashList.js.map +1 -1
  38. package/lib/module/components/MessageList/MessageList.js +3 -1
  39. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  40. package/lib/module/components/Poll/Poll.js +5 -3
  41. package/lib/module/components/Poll/Poll.js.map +1 -1
  42. package/lib/module/components/Poll/components/Button.js.map +1 -1
  43. package/lib/module/components/Poll/components/PollButtons.js.map +1 -1
  44. package/lib/module/components/Poll/components/PollOption.js +20 -10
  45. package/lib/module/components/Poll/components/PollOption.js.map +1 -1
  46. package/lib/module/components/Reply/Reply.js +19 -10
  47. package/lib/module/components/Reply/Reply.js.map +1 -1
  48. package/lib/module/contexts/themeContext/ThemeContext.js +8 -1
  49. package/lib/module/contexts/themeContext/ThemeContext.js.map +1 -1
  50. package/lib/module/version.json +1 -1
  51. package/lib/typescript/components/Attachment/UrlPreview/URLPreview.d.ts.map +1 -1
  52. package/lib/typescript/components/Attachment/UrlPreview/URLPreviewCompact.d.ts.map +1 -1
  53. package/lib/typescript/components/Message/MessageItemView/MessageItemView.d.ts.map +1 -1
  54. package/lib/typescript/components/MessageList/MessageFlashList.d.ts.map +1 -1
  55. package/lib/typescript/components/MessageList/MessageList.d.ts.map +1 -1
  56. package/lib/typescript/components/Poll/Poll.d.ts.map +1 -1
  57. package/lib/typescript/components/Poll/components/Button.d.ts +5 -0
  58. package/lib/typescript/components/Poll/components/Button.d.ts.map +1 -1
  59. package/lib/typescript/components/Poll/components/PollOption.d.ts +1 -1
  60. package/lib/typescript/components/Poll/components/PollOption.d.ts.map +1 -1
  61. package/lib/typescript/components/Reply/Reply.d.ts +1 -0
  62. package/lib/typescript/components/Reply/Reply.d.ts.map +1 -1
  63. package/lib/typescript/contexts/themeContext/ThemeContext.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/components/Attachment/Attachment.tsx +1 -1
  66. package/src/components/Attachment/Audio/PlayPauseButton.tsx +1 -1
  67. package/src/components/Attachment/Audio/__tests__/PlayPauseButton.test.tsx +44 -0
  68. package/src/components/Attachment/UrlPreview/URLPreview.tsx +4 -8
  69. package/src/components/Attachment/UrlPreview/URLPreviewCompact.tsx +4 -8
  70. package/src/components/Attachment/UrlPreview/__tests__/URLPreview.test.tsx +92 -0
  71. package/src/components/Attachment/__tests__/Attachment.test.tsx +48 -0
  72. package/src/components/Message/MessageItemView/MessageItemView.tsx +1 -2
  73. package/src/components/Message/MessageItemView/__tests__/MessageTextContainer.test.tsx +20 -2
  74. package/src/components/Message/MessageItemView/__tests__/__snapshots__/MessageAuthor.test.tsx.snap +2 -2
  75. package/src/components/MessageList/MessageFlashList.tsx +4 -2
  76. package/src/components/MessageList/MessageList.tsx +4 -2
  77. package/src/components/Poll/Poll.tsx +5 -3
  78. package/src/components/Poll/__tests__/Poll.test.tsx +70 -0
  79. package/src/components/Poll/components/Button.tsx +5 -0
  80. package/src/components/Poll/components/PollButtons.tsx +2 -0
  81. package/src/components/Poll/components/PollOption.tsx +22 -21
  82. package/src/components/Poll/components/__tests__/PollOption.test.tsx +99 -0
  83. package/src/components/Reply/Reply.tsx +20 -13
  84. package/src/components/Reply/__tests__/Reply.test.tsx +111 -1
  85. package/src/contexts/themeContext/ThemeContext.tsx +5 -1
  86. package/src/version.json +1 -1
@@ -257,7 +257,7 @@ const useAudioAttachmentStyles = () => {
257
257
  : semantics.chatBorderOnChatIncoming,
258
258
  },
259
259
  durationText: {
260
- color: semantics.chatTextIncoming,
260
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
261
261
  fontWeight: primitives.typographyFontWeightSemiBold,
262
262
  },
263
263
  leftContainer: {
@@ -65,7 +65,7 @@ const useStyles = () => {
65
65
  justifyContent: 'center',
66
66
  alignItems: 'center',
67
67
  borderWidth: 1,
68
- borderColor: semantics.chatBorderOnChatIncoming,
68
+ borderColor: semantics.controlPlaybackToggleBorder,
69
69
  },
70
70
  });
71
71
  }, [semantics]);
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+
4
+ import { render, screen } from '@testing-library/react-native';
5
+
6
+ import { mergeThemes, ThemeProvider } from '../../../../contexts/themeContext/ThemeContext';
7
+ import { PlayPauseButton } from '../PlayPauseButton';
8
+
9
+ describe('PlayPauseButton', () => {
10
+ const lightTheme = mergeThemes({ scheme: 'light' });
11
+
12
+ const renderButton = (containerStyle?: Record<string, unknown>) =>
13
+ render(
14
+ <ThemeProvider>
15
+ <PlayPauseButton
16
+ containerStyle={containerStyle}
17
+ isPlaying={false}
18
+ onPress={jest.fn()}
19
+ testID='play-pause-button'
20
+ />
21
+ </ThemeProvider>,
22
+ );
23
+
24
+ const borderColorOf = () => {
25
+ const style = screen.getByTestId('play-pause-button').props.style;
26
+ return StyleSheet.flatten(typeof style === 'function' ? style({ pressed: false }) : style)
27
+ ?.borderColor;
28
+ };
29
+
30
+ // This control also renders in the composer's attachment previews, which are
31
+ // not a message bubble - so it must not reach for a chat side token.
32
+ it('defaults to the neutral playback toggle border, not a chat bubble border', () => {
33
+ renderButton();
34
+
35
+ expect(borderColorOf()).toBe(lightTheme.semantics.controlPlaybackToggleBorder);
36
+ expect(borderColorOf()).not.toBe(lightTheme.semantics.chatBorderOnChatIncoming);
37
+ });
38
+
39
+ it('lets in-bubble callers override the border with the resolved chat side', () => {
40
+ renderButton({ borderColor: lightTheme.semantics.chatBorderOnChatOutgoing });
41
+
42
+ expect(borderColorOf()).toBe(lightTheme.semantics.chatBorderOnChatOutgoing);
43
+ });
44
+ });
@@ -68,10 +68,6 @@ const URLPreviewWithContext = (props: URLPreviewPropsWithContext) => {
68
68
 
69
69
  const { icons } = useComponentsContext();
70
70
 
71
- const {
72
- theme: { semantics },
73
- } = useTheme();
74
-
75
71
  const { image_url, og_scrape_url, text, thumb_url, title, type } = attachment;
76
72
 
77
73
  const {
@@ -156,7 +152,7 @@ const URLPreviewWithContext = (props: URLPreviewPropsWithContext) => {
156
152
  </Text>
157
153
  ) : null}
158
154
  <View style={[styles.linkPreview, linkPreview, stylesProp.linkPreview]}>
159
- <icons.Link height={12} width={12} stroke={semantics.chatTextIncoming} />
155
+ <icons.Link height={12} width={12} stroke={styles.linkPreviewText.color} />
160
156
  <Text
161
157
  numberOfLines={1}
162
158
  style={[styles.linkPreviewText, linkPreviewText, stylesProp.linkPreviewText]}
@@ -267,13 +263,13 @@ const useStyles = () => {
267
263
  padding: primitives.spacingSm,
268
264
  },
269
265
  title: {
270
- color: semantics.chatTextIncoming,
266
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
271
267
  fontSize: primitives.typographyFontSizeSm,
272
268
  fontWeight: primitives.typographyFontWeightSemiBold,
273
269
  lineHeight: primitives.typographyLineHeightTight,
274
270
  },
275
271
  description: {
276
- color: semantics.chatTextIncoming,
272
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
277
273
  fontSize: primitives.typographyFontSizeXs,
278
274
  fontWeight: primitives.typographyFontWeightRegular,
279
275
  lineHeight: primitives.typographyLineHeightTight,
@@ -284,7 +280,7 @@ const useStyles = () => {
284
280
  gap: primitives.spacingXxs,
285
281
  },
286
282
  linkPreviewText: {
287
- color: semantics.chatTextIncoming,
283
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
288
284
  fontSize: primitives.typographyFontSizeXs,
289
285
  fontWeight: primitives.typographyFontWeightRegular,
290
286
  lineHeight: primitives.typographyLineHeightTight,
@@ -64,10 +64,6 @@ const URLPreviewCompactWithContext = (props: URLPreviewCompactPropsWithContext)
64
64
 
65
65
  const { icons } = useComponentsContext();
66
66
 
67
- const {
68
- theme: { semantics },
69
- } = useTheme();
70
-
71
67
  const { image_url, og_scrape_url, text, thumb_url, title: titleText, type } = attachment;
72
68
 
73
69
  const {
@@ -158,7 +154,7 @@ const URLPreviewCompactWithContext = (props: URLPreviewCompactPropsWithContext)
158
154
  </Text>
159
155
  ) : null}
160
156
  <View style={[styles.linkPreview, linkPreview, stylesProp.linkPreview]}>
161
- <icons.Link height={12} width={12} stroke={semantics.chatTextIncoming} />
157
+ <icons.Link height={12} width={12} stroke={styles.linkPreviewText.color} />
162
158
  <Text
163
159
  numberOfLines={1}
164
160
  style={[styles.linkPreviewText, linkPreviewText, stylesProp.linkPreviewText]}
@@ -271,13 +267,13 @@ const useStyles = () => {
271
267
  flexShrink: 1,
272
268
  },
273
269
  title: {
274
- color: semantics.chatTextIncoming,
270
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
275
271
  fontSize: primitives.typographyFontSizeSm,
276
272
  fontWeight: primitives.typographyFontWeightSemiBold,
277
273
  lineHeight: primitives.typographyLineHeightTight,
278
274
  },
279
275
  description: {
280
- color: semantics.chatTextIncoming,
276
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
281
277
  fontSize: primitives.typographyFontSizeXs,
282
278
  fontWeight: primitives.typographyFontWeightRegular,
283
279
  lineHeight: primitives.typographyLineHeightTight,
@@ -288,7 +284,7 @@ const useStyles = () => {
288
284
  gap: primitives.spacingXxs,
289
285
  },
290
286
  linkPreviewText: {
291
- color: semantics.chatTextIncoming,
287
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
292
288
  fontSize: primitives.typographyFontSizeXs,
293
289
  fontWeight: primitives.typographyFontWeightRegular,
294
290
  lineHeight: primitives.typographyLineHeightTight,
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+
4
+ import { cleanup, render, screen, waitFor } from '@testing-library/react-native';
5
+
6
+ import type { MessageContextValue } from '../../../../contexts/messageContext/MessageContext';
7
+ import { MessageProvider } from '../../../../contexts/messageContext/MessageContext';
8
+ import type { MessagesContextValue } from '../../../../contexts/messagesContext/MessagesContext';
9
+ import { MessagesProvider } from '../../../../contexts/messagesContext/MessagesContext';
10
+ import { mergeThemes, ThemeProvider } from '../../../../contexts/themeContext/ThemeContext';
11
+ import { generateCardAttachment } from '../../../../mock-builders/generator/attachment';
12
+ import { generateMessage } from '../../../../mock-builders/generator/message';
13
+ import { URLPreview } from '../URLPreview';
14
+ import { URLPreviewCompact } from '../URLPreviewCompact';
15
+
16
+ const lightTheme = mergeThemes({ scheme: 'light' });
17
+
18
+ /**
19
+ * Both URL preview variants resolve every text colour from the same
20
+ * incoming/outgoing pair, so the assertions are shared.
21
+ */
22
+ describe.each([
23
+ ['URLPreview', URLPreview],
24
+ ['URLPreviewCompact', URLPreviewCompact],
25
+ ])('%s chat text side', (_name, Component) => {
26
+ const renderPreview = (messageContextValue: Partial<MessageContextValue> = {}) => {
27
+ const attachment = generateCardAttachment({ title: 'A title', text: 'A description' });
28
+
29
+ return render(
30
+ <ThemeProvider>
31
+ <MessagesProvider value={{} as unknown as MessagesContextValue}>
32
+ <MessageProvider
33
+ value={
34
+ {
35
+ message: generateMessage(),
36
+ ...messageContextValue,
37
+ } as unknown as MessageContextValue
38
+ }
39
+ >
40
+ <Component attachment={attachment} />
41
+ </MessageProvider>
42
+ </MessagesProvider>
43
+ </ThemeProvider>,
44
+ );
45
+ };
46
+
47
+ const colorOf = (text: string) => StyleSheet.flatten(screen.getByText(text).props.style)?.color;
48
+
49
+ afterEach(cleanup);
50
+
51
+ it('uses the outgoing chat text colour on our own message', async () => {
52
+ renderPreview({ isMyMessage: true });
53
+
54
+ await waitFor(() => {
55
+ expect(colorOf('A title')).toBe(lightTheme.semantics.chatTextOutgoing);
56
+ });
57
+ expect(colorOf('A description')).toBe(lightTheme.semantics.chatTextOutgoing);
58
+ });
59
+
60
+ it("uses the incoming chat text colour on another user's message", async () => {
61
+ renderPreview({ isMyMessage: false });
62
+
63
+ await waitFor(() => {
64
+ expect(colorOf('A title')).toBe(lightTheme.semantics.chatTextIncoming);
65
+ });
66
+ expect(colorOf('A description')).toBe(lightTheme.semantics.chatTextIncoming);
67
+ });
68
+
69
+ it('uses the outgoing attachment background on our own message', async () => {
70
+ renderPreview({ isMyMessage: true });
71
+
72
+ await waitFor(() => {
73
+ expect(StyleSheet.flatten(screen.getByTestId('card-attachment').props.style)).toEqual(
74
+ expect.objectContaining({
75
+ backgroundColor: lightTheme.semantics.chatBgAttachmentOutgoing,
76
+ }),
77
+ );
78
+ });
79
+ });
80
+
81
+ it('strokes the link icon with the same colour as the link text', async () => {
82
+ renderPreview({ isMyMessage: true });
83
+
84
+ // The URL text and its leading icon must not disagree on the side.
85
+ await waitFor(() => {
86
+ expect(screen.getByTestId('card-attachment')).toBeTruthy();
87
+ });
88
+
89
+ const linkIcon = screen.UNSAFE_getByProps({ stroke: lightTheme.semantics.chatTextOutgoing });
90
+ expect(linkIcon).toBeTruthy();
91
+ });
92
+ });
@@ -218,6 +218,54 @@ describe('Attachment', () => {
218
218
  isSoundPackageAvailable.mockReturnValue(false);
219
219
  });
220
220
 
221
+ it('uses the outgoing audio player background on our own message', async () => {
222
+ const { isSoundPackageAvailable } = require('../../../native');
223
+ isSoundPackageAvailable.mockReturnValue(true);
224
+ const attachment = generateAudioAttachment({ duration: 10, waveform_data: [0.2, 0.6] });
225
+ const message = generateMessage({ attachments: [attachment, generateAudioAttachment()] });
226
+
227
+ const { getByLabelText } = render(
228
+ getAttachmentComponent(
229
+ { attachment },
230
+ { isMyMessage: true, message, messageHasOnlySingleAttachment: false },
231
+ ),
232
+ );
233
+
234
+ await waitFor(() => {
235
+ const style = StyleSheet.flatten(getByLabelText('audio-attachment-preview').props.style);
236
+ expect(style.backgroundColor).toBe(lightTheme.semantics.chatBgAttachmentOutgoing);
237
+ });
238
+ isSoundPackageAvailable.mockReturnValue(false);
239
+ });
240
+
241
+ it('resolves the audio duration label colour from the message side', async () => {
242
+ const { isSoundPackageAvailable } = require('../../../native');
243
+ isSoundPackageAvailable.mockReturnValue(true);
244
+ const attachment = generateAudioAttachment({ duration: 10, waveform_data: [0.2, 0.6] });
245
+ const message = generateMessage({ attachments: [attachment] });
246
+
247
+ const { getByLabelText } = render(
248
+ getAttachmentComponent(
249
+ { attachment },
250
+ { isMyMessage: true, message, messageHasOnlySingleAttachment: false },
251
+ ),
252
+ );
253
+
254
+ // NOTE: `StableDurationLabel` applies its own `visibleStyle` last, which
255
+ // always sets `color` from the playback state - so this asserts the wiring
256
+ // reaching the label, not the final rendered pixel. See the reserve label.
257
+ await waitFor(() => {
258
+ expect(getByLabelText('Progress Duration').props.style).toEqual(
259
+ expect.arrayContaining([
260
+ expect.arrayContaining([
261
+ expect.objectContaining({ color: lightTheme.semantics.chatTextOutgoing }),
262
+ ]),
263
+ ]),
264
+ );
265
+ });
266
+ isSoundPackageAvailable.mockReturnValue(false);
267
+ });
268
+
221
269
  it('should render UrlPreview component if attachment has title_link or og_scrape_url', async () => {
222
270
  const attachment = generateImageAttachment({
223
271
  og_scrape_url: uuidv4(),
@@ -171,7 +171,6 @@ const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => {
171
171
 
172
172
  const {
173
173
  isMessageErrorType,
174
- isMessageReceivedOrErrorType,
175
174
  isMessageTypeDeleted,
176
175
  isVeryLastMessage,
177
176
  messageGroupedSingleOrBottom,
@@ -200,7 +199,7 @@ const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => {
200
199
  backgroundColor = 'transparent';
201
200
  } else if (hasStandaloneGiphyOrImgur) {
202
201
  backgroundColor = 'transparent';
203
- } else if (isMessageReceivedOrErrorType) {
202
+ } else if (!isMyMessage) {
204
203
  backgroundColor = semantics.chatBgIncoming;
205
204
  }
206
205
 
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
- import { Text } from 'react-native';
2
+ import { StyleSheet, Text } from 'react-native';
3
3
 
4
4
  import { cleanup, render, waitFor } from '@testing-library/react-native';
5
5
 
6
6
  import { WithComponents } from '../../../../contexts/componentsContext/ComponentsContext';
7
7
  import { OverlayProvider } from '../../../../contexts/overlayContext/OverlayProvider';
8
- import { ThemeProvider } from '../../../../contexts/themeContext/ThemeContext';
8
+ import { mergeThemes, ThemeProvider } from '../../../../contexts/themeContext/ThemeContext';
9
9
  import { defaultTheme } from '../../../../contexts/themeContext/utils/theme';
10
10
  import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel';
11
11
  import { useMockedApis } from '../../../../mock-builders/api/useMockedApis';
@@ -22,6 +22,8 @@ import { MessageList } from '../../../MessageList/MessageList';
22
22
  import { MessageTextContainer } from '../MessageTextContainer';
23
23
 
24
24
  describe('MessageTextContainer', () => {
25
+ const lightTheme = mergeThemes({ scheme: 'light' });
26
+
25
27
  afterEach(cleanup);
26
28
 
27
29
  it('should render message text container', async () => {
@@ -110,4 +112,20 @@ describe('MessageTextContainer', () => {
110
112
  expect(getByText(message.i18n.no_text)).toBeTruthy();
111
113
  });
112
114
  });
115
+
116
+ it('renders our own message text with the outgoing chat text colour', async () => {
117
+ const message = generateMessage({ user: generateStaticUser(1) });
118
+
119
+ const { getByText } = render(
120
+ <ThemeProvider>
121
+ <MessageTextContainer isMyMessage message={message} />
122
+ </ThemeProvider>,
123
+ );
124
+
125
+ await waitFor(() => {
126
+ expect(StyleSheet.flatten(getByText(message.text as string).props.style)?.color).toBe(
127
+ lightTheme.semantics.chatTextOutgoing,
128
+ );
129
+ });
130
+ });
113
131
  });
@@ -22,10 +22,10 @@ exports[`MessageAuthor should render message author 1`] = `
22
22
  "width": 32,
23
23
  },
24
24
  {
25
- "backgroundColor": "#006970",
25
+ "backgroundColor": "#a9e4ea",
26
26
  },
27
27
  {
28
- "borderColor": "rgba(255, 255, 255, 0.2)",
28
+ "borderColor": "rgba(26, 27, 37, 0.1)",
29
29
  "borderWidth": 1,
30
30
  },
31
31
  undefined,
@@ -4,6 +4,7 @@ import {
4
4
  ScrollViewProps,
5
5
  StyleSheet,
6
6
  View,
7
+ useColorScheme,
7
8
  ViewabilityConfig,
8
9
  ViewToken,
9
10
  } from 'react-native';
@@ -401,11 +402,12 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
401
402
  const styles = useStyles();
402
403
 
403
404
  const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]);
405
+ const scheme = useColorScheme();
404
406
 
405
407
  const modifiedTheme = useMemo(
406
- () => mergeThemes({ style: myMessageTheme, theme }),
408
+ () => mergeThemes({ scheme, style: myMessageTheme, theme }),
407
409
  // eslint-disable-next-line react-hooks/exhaustive-deps
408
- [myMessageThemeString, theme],
410
+ [myMessageThemeString, scheme, theme],
409
411
  );
410
412
 
411
413
  const { processedMessageList, rawMessageList, viewabilityChangedCallback } = useMessageList({
@@ -7,6 +7,7 @@ import {
7
7
  ScrollViewProps,
8
8
  StyleSheet,
9
9
  View,
10
+ useColorScheme,
10
11
  ViewabilityConfig,
11
12
  ViewToken,
12
13
  } from 'react-native';
@@ -394,11 +395,12 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
394
395
  });
395
396
 
396
397
  const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]);
398
+ const scheme = useColorScheme();
397
399
 
398
400
  const modifiedTheme = useMemo(
399
- () => mergeThemes({ style: myMessageTheme, theme }),
401
+ () => mergeThemes({ scheme, style: myMessageTheme, theme }),
400
402
  // eslint-disable-next-line react-hooks/exhaustive-deps
401
- [myMessageThemeString, theme],
403
+ [myMessageThemeString, scheme, theme],
402
404
  );
403
405
 
404
406
  /**
@@ -6,6 +6,7 @@ import { PollOption as PollOptionClass } from 'stream-chat';
6
6
  import { PollOption, ShowAllOptionsButton } from './components';
7
7
  import { PollUIStateProvider } from './contexts/PollUIStateContext';
8
8
 
9
+ import { useIsPollCreatedByCurrentUser } from './hook/useIsPollCreatedByCurrentUser';
9
10
  import { usePollState } from './hooks/usePollState';
10
11
 
11
12
  import {
@@ -105,6 +106,7 @@ const useStyles = () => {
105
106
  const {
106
107
  theme: { semantics },
107
108
  } = useTheme();
109
+ const isPollCreatedByClient = useIsPollCreatedByCurrentUser();
108
110
  return useMemo(() => {
109
111
  return StyleSheet.create({
110
112
  container: {
@@ -114,14 +116,14 @@ const useStyles = () => {
114
116
  },
115
117
  headerContainer: { gap: primitives.spacingXxs },
116
118
  headerSubtitle: {
117
- color: semantics.chatTextIncoming,
119
+ color: isPollCreatedByClient ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
118
120
  fontSize: primitives.typographyFontSizeSm,
119
121
  fontWeight: primitives.typographyFontWeightRegular,
120
122
  lineHeight: primitives.typographyLineHeightTight,
121
123
  textAlign: 'left',
122
124
  },
123
125
  headerTitle: {
124
- color: semantics.chatTextIncoming,
126
+ color: isPollCreatedByClient ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
125
127
  fontSize: primitives.typographyFontSizeMd,
126
128
  fontWeight: primitives.typographyFontWeightSemiBold,
127
129
  lineHeight: primitives.typographyLineHeightNormal,
@@ -131,5 +133,5 @@ const useStyles = () => {
131
133
  gap: primitives.spacingMd,
132
134
  },
133
135
  });
134
- }, [semantics]);
136
+ }, [isPollCreatedByClient, semantics]);
135
137
  };
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+
4
+ import { render, screen } from '@testing-library/react-native';
5
+
6
+ import {
7
+ OwnCapabilitiesProvider,
8
+ PollContextProvider,
9
+ ThemeProvider,
10
+ TranslationProvider,
11
+ } from '../../../contexts';
12
+ import { mergeThemes } from '../../../contexts/themeContext/ThemeContext';
13
+ import { generateMessage } from '../../../mock-builders/generator/message';
14
+ import { PollHeader } from '../Poll';
15
+
16
+ const mockIsPollCreatedByCurrentUser = jest.fn();
17
+
18
+ jest.mock('../hook/useIsPollCreatedByCurrentUser', () => ({
19
+ useIsPollCreatedByCurrentUser: () => mockIsPollCreatedByCurrentUser(),
20
+ }));
21
+
22
+ jest.mock('../hooks/usePollState', () => ({
23
+ usePollState: () => ({
24
+ enforceUniqueVote: true,
25
+ isClosed: false,
26
+ maxVotesAllowed: undefined,
27
+ name: 'A poll question',
28
+ }),
29
+ }));
30
+
31
+ describe('PollHeader chat text side', () => {
32
+ const lightTheme = mergeThemes({ scheme: 'light' });
33
+
34
+ const renderHeader = () =>
35
+ render(
36
+ <ThemeProvider>
37
+ <TranslationProvider value={{ t: (key: string) => key } as never}>
38
+ <OwnCapabilitiesProvider value={{} as never}>
39
+ <PollContextProvider
40
+ value={{ message: generateMessage(), poll: { data: {} } as never }}
41
+ >
42
+ <PollHeader />
43
+ </PollContextProvider>
44
+ </OwnCapabilitiesProvider>
45
+ </TranslationProvider>
46
+ </ThemeProvider>,
47
+ );
48
+
49
+ const colorOf = (text: string) => StyleSheet.flatten(screen.getByText(text).props.style)?.color;
50
+
51
+ afterEach(() => {
52
+ mockIsPollCreatedByCurrentUser.mockReset();
53
+ });
54
+
55
+ it('uses the outgoing chat text colour for our own poll', () => {
56
+ mockIsPollCreatedByCurrentUser.mockReturnValue(true);
57
+ renderHeader();
58
+
59
+ expect(colorOf('A poll question')).toBe(lightTheme.semantics.chatTextOutgoing);
60
+ expect(colorOf('Select one')).toBe(lightTheme.semantics.chatTextOutgoing);
61
+ });
62
+
63
+ it("uses the incoming chat text colour for another user's poll", () => {
64
+ mockIsPollCreatedByCurrentUser.mockReturnValue(false);
65
+ renderHeader();
66
+
67
+ expect(colorOf('A poll question')).toBe(lightTheme.semantics.chatTextIncoming);
68
+ expect(colorOf('Select one')).toBe(lightTheme.semantics.chatTextIncoming);
69
+ });
70
+ });
@@ -10,6 +10,11 @@ export type PollButtonProps = {
10
10
  };
11
11
 
12
12
  export type PollVoteButtonProps = {
13
+ /**
14
+ * Render with the incoming chat tokens regardless of poll ownership. Set on
15
+ * surfaces that are not a message bubble, such as the full-options list.
16
+ */
17
+ forceIncoming?: boolean;
13
18
  option: PollOption;
14
19
  style?: StyleProp<ViewStyle>;
15
20
  } & Pick<PollButtonProps, 'onPress'>;
@@ -247,6 +247,8 @@ const useStyles = () => {
247
247
  ? semantics.chatBorderOnChatOutgoing
248
248
  : semantics.chatBorderOnChatIncoming,
249
249
  },
250
+ // NOTE: PollButtons only ever renders on the in-bubble surface (it is what
251
+ // opens the full-options modal), so it needs no `forceIncoming` handling.
250
252
  });
251
253
  }, [semantics, isPollCreatedByClient]);
252
254
  };
@@ -92,7 +92,11 @@ export const PollAllOptions = ({
92
92
  export const PollOption = ({ option, showProgressBar = true, forceIncoming }: PollOptionProps) => {
93
93
  const { latestVotesByOption, voteCountsByOption, voteCount } = usePollState();
94
94
  const { t } = useTranslationContext();
95
- const styles = useStyles();
95
+ const isPollCreatedByClient = useIsPollCreatedByCurrentUser();
96
+ // `forceIncoming` is set on the full-options surface, which is a neutral card
97
+ // rather than a bubble - so it wins over poll ownership.
98
+ const isIncoming = !isPollCreatedByClient || !!forceIncoming;
99
+ const styles = useStyles({ isIncoming });
96
100
 
97
101
  const relevantVotes = useMemo(
98
102
  () => latestVotesByOption?.[option.id] || [],
@@ -111,21 +115,17 @@ export const PollOption = ({ option, showProgressBar = true, forceIncoming }: Po
111
115
  semantics,
112
116
  },
113
117
  } = useTheme();
114
- const isPollCreatedByClient = useIsPollCreatedByCurrentUser();
118
+ const unFilledColor = isIncoming
119
+ ? semantics.chatPollProgressTrackIncoming
120
+ : semantics.chatPollProgressTrackOutgoing;
115
121
 
116
- const unFilledColor =
117
- isPollCreatedByClient && !forceIncoming
118
- ? semantics.chatPollProgressTrackOutgoing
119
- : semantics.chatPollProgressTrackIncoming;
120
-
121
- const filledColor =
122
- isPollCreatedByClient && !forceIncoming
123
- ? semantics.chatPollProgressFillOutgoing
124
- : semantics.chatPollProgressFillIncoming;
122
+ const filledColor = isIncoming
123
+ ? semantics.chatPollProgressFillIncoming
124
+ : semantics.chatPollProgressFillOutgoing;
125
125
 
126
126
  return (
127
127
  <View style={[styles.container, container]}>
128
- <VoteButton option={option} />
128
+ <VoteButton forceIncoming={forceIncoming} option={option} />
129
129
  <View style={[styles.info, info]}>
130
130
  <View style={[styles.header, header]}>
131
131
  <Text ellipsizeMode='tail' numberOfLines={2} style={[styles.text, text]}>
@@ -163,7 +163,7 @@ export const PollOption = ({ option, showProgressBar = true, forceIncoming }: Po
163
163
  );
164
164
  };
165
165
 
166
- export const VoteButton = ({ onPress, option }: PollVoteButtonProps) => {
166
+ export const VoteButton = ({ forceIncoming, onPress, option }: PollVoteButtonProps) => {
167
167
  const { icons } = useComponentsContext();
168
168
  const { message, poll } = usePollContext();
169
169
  const { isClosed, ownVotesByOptionId } = usePollState();
@@ -172,7 +172,8 @@ export const VoteButton = ({ onPress, option }: PollVoteButtonProps) => {
172
172
  theme: { semantics },
173
173
  } = useTheme();
174
174
  const isPollCreatedByClient = useIsPollCreatedByCurrentUser();
175
- const styles = useStyles();
175
+ const isIncoming = !isPollCreatedByClient || !!forceIncoming;
176
+ const styles = useStyles({ isIncoming });
176
177
 
177
178
  const {
178
179
  theme: {
@@ -212,9 +213,9 @@ export const VoteButton = ({ onPress, option }: PollVoteButtonProps) => {
212
213
  {
213
214
  borderWidth: hasVote ? 0 : 1,
214
215
  backgroundColor: hasVote ? semantics.accentPrimary : 'transparent',
215
- borderColor: isPollCreatedByClient
216
- ? semantics.chatBorderOnChatOutgoing
217
- : semantics.chatBorderOnChatIncoming,
216
+ borderColor: isIncoming
217
+ ? semantics.chatBorderOnChatIncoming
218
+ : semantics.chatBorderOnChatOutgoing,
218
219
  },
219
220
  voteButtonContainer,
220
221
  ]}
@@ -224,7 +225,7 @@ export const VoteButton = ({ onPress, option }: PollVoteButtonProps) => {
224
225
  ) : null;
225
226
  };
226
227
 
227
- const useStyles = () => {
228
+ const useStyles = ({ isIncoming }: { isIncoming: boolean }) => {
228
229
  const {
229
230
  theme: { semantics },
230
231
  } = useTheme();
@@ -242,7 +243,7 @@ const useStyles = () => {
242
243
  minWidth: 0,
243
244
  },
244
245
  text: {
245
- color: semantics.chatTextIncoming,
246
+ color: isIncoming ? semantics.chatTextIncoming : semantics.chatTextOutgoing,
246
247
  flexGrow: 1,
247
248
  flexShrink: 1,
248
249
  fontSize: primitives.typographyFontSizeSm,
@@ -264,7 +265,7 @@ const useStyles = () => {
264
265
  minHeight: 20,
265
266
  },
266
267
  votesText: {
267
- color: semantics.chatTextIncoming,
268
+ color: isIncoming ? semantics.chatTextIncoming : semantics.chatTextOutgoing,
268
269
  fontSize: primitives.typographyFontSizeXs,
269
270
  fontWeight: primitives.typographyFontWeightRegular,
270
271
  lineHeight: primitives.typographyLineHeightTight,
@@ -281,7 +282,7 @@ const useStyles = () => {
281
282
  width: 24,
282
283
  },
283
284
  });
284
- }, [semantics]);
285
+ }, [isIncoming, semantics]);
285
286
  };
286
287
 
287
288
  const useAllOptionStyles = () => {