@planningcenter/chat-react-native 4.5.0-rc.0 → 4.5.0-rc.2

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 (24) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +193 -145
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  5. package/build/components/conversation/reply_shadow_message.js +135 -107
  6. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  7. package/build/components/display/filter_dropdown.d.ts +6 -1
  8. package/build/components/display/filter_dropdown.d.ts.map +1 -1
  9. package/build/components/display/filter_dropdown.js +55 -35
  10. package/build/components/display/filter_dropdown.js.map +1 -1
  11. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -1
  12. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +138 -94
  13. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
  14. package/build/utils/styles.d.ts +1 -0
  15. package/build/utils/styles.d.ts.map +1 -1
  16. package/build/utils/styles.js +1 -1
  17. package/build/utils/styles.js.map +1 -1
  18. package/package.json +2 -2
  19. package/src/components/conversation/message.tsx +10 -2
  20. package/src/components/conversation/reply_shadow_message.tsx +14 -3
  21. package/src/components/display/filter_dropdown.tsx +7 -1
  22. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +45 -0
  23. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +38 -12
  24. package/src/utils/styles.ts +1 -0
@@ -24,11 +24,15 @@ import {
24
24
  CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
25
25
  MAX_FONT_SIZE_MULTIPLIER,
26
26
  MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
27
+ MESSAGE_BACKGROUND_OUTSET,
27
28
  platformFontWeightMedium,
28
29
  } from '../../utils/styles'
29
30
  import { Avatar, Icon, IconProps, Image, Text } from '../display'
30
31
  import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
31
32
 
33
+ const SPACE_BELOW_MESSAGE = 12
34
+ const SPACE_BELOW_PRESSABLE = SPACE_BELOW_MESSAGE - MESSAGE_BACKGROUND_OUTSET
35
+
32
36
  interface ReplyShadowMessageProps extends MessageResource {
33
37
  messageId: string
34
38
  conversation_id: number
@@ -96,6 +100,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
96
100
 
97
101
  return (
98
102
  <Pressable
103
+ style={styles.pressable}
99
104
  android_ripple={{ color: colors.androidRippleNeutral }}
100
105
  onPress={handleNavigateToReplies}
101
106
  onPressIn={handleMessagePressIn}
@@ -104,7 +109,8 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
104
109
  accessibilityHint="Navigate to reply screen for this message"
105
110
  accessibilityRole="link"
106
111
  >
107
- <Animated.View style={[styles.message, animatedBackgroundColor]}>
112
+ <Animated.View style={[StyleSheet.absoluteFill, animatedBackgroundColor]} />
113
+ <View style={[styles.message, { marginBottom: -SPACE_BELOW_PRESSABLE }]}>
108
114
  {!message.mine && (
109
115
  <View>
110
116
  <View style={[styles.avatarWrapper, !isDeleted && styles.avatarOpacity]}>
@@ -145,7 +151,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
145
151
  {message.mine && (
146
152
  <MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
147
153
  )}
148
- </Animated.View>
154
+ </View>
149
155
  </Pressable>
150
156
  )
151
157
  }
@@ -282,6 +288,11 @@ const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}
282
288
  const tabletWidth = width >= 744 // Smallest iPad Mini's width
283
289
 
284
290
  return StyleSheet.create({
291
+ pressable: {
292
+ marginTop: -MESSAGE_BACKGROUND_OUTSET,
293
+ paddingTop: MESSAGE_BACKGROUND_OUTSET,
294
+ marginBottom: SPACE_BELOW_PRESSABLE,
295
+ },
285
296
  message: {
286
297
  gap: 8,
287
298
  flexDirection: mine ? 'row-reverse' : 'row',
@@ -290,7 +301,7 @@ const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}
290
301
  messageContent: {
291
302
  flex: 1,
292
303
  gap: 4,
293
- marginBottom: 12,
304
+ marginBottom: SPACE_BELOW_MESSAGE,
294
305
  },
295
306
  avatarWrapper: {
296
307
  width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
@@ -35,6 +35,11 @@ export interface FilterDropdownProps {
35
35
  onMoreFilters: () => void
36
36
  triggerLabel: string
37
37
  showMoreFilters?: boolean
38
+ /**
39
+ * Widest the trigger may render, in points. Hosts that measure the trigger at its intrinsic
40
+ * width need this for the label to truncate.
41
+ */
42
+ maxWidth?: number
38
43
  }
39
44
 
40
45
  export function FilterDropdown({
@@ -44,6 +49,7 @@ export function FilterDropdown({
44
49
  onMoreFilters,
45
50
  triggerLabel,
46
51
  showMoreFilters = true,
52
+ maxWidth,
47
53
  }: FilterDropdownProps) {
48
54
  const styles = useStyles()
49
55
 
@@ -71,7 +77,7 @@ export function FilterDropdown({
71
77
  return (
72
78
  <Menu.View actions={actions} onPressAction={handlePressAction} style={styles.container}>
73
79
  <View
74
- style={styles.trigger}
80
+ style={[styles.trigger, maxWidth !== undefined && { maxWidth }]}
75
81
  accessible
76
82
  accessibilityRole="button"
77
83
  accessibilityHint="Opens filter options"
@@ -21,6 +21,15 @@ const mockSearchEnabled = jest.fn()
21
21
  const mockCanBrowseArchived = jest.fn()
22
22
  const mockGroupsByGraphId: Record<string, { name: string }> = {}
23
23
 
24
+ jest.mock('react-native', () => {
25
+ const actual = jest.requireActual('react-native')
26
+ return Object.defineProperty(actual, 'useWindowDimensions', {
27
+ value: jest.fn(() => ({ fontScale: 1 })),
28
+ configurable: true,
29
+ })
30
+ })
31
+ const mockUseWindowDimensions = jest.requireMock('react-native').useWindowDimensions
32
+
24
33
  jest.mock('@react-navigation/native', () => ({
25
34
  ...jest.requireActual('@react-navigation/native'),
26
35
  useNavigation: () => ({ setParams: mockSetParams, navigate: mockNavigate }),
@@ -121,10 +130,46 @@ describe('FilterDropdownListHeaderComponent', () => {
121
130
  mockSetParams.mockClear()
122
131
  mockSearchEnabled.mockReturnValue(true)
123
132
  mockCanBrowseArchived.mockReturnValue(false)
133
+ mockUseWindowDimensions.mockReturnValue({ fontScale: 1 })
124
134
  setCapabilities({ groups: true, teams: true })
125
135
  setRouteParams({})
126
136
  })
127
137
 
138
+ it('keeps the dropdown and actions on one line at the default font scale', () => {
139
+ mockUseWindowDimensions.mockReturnValue({ fontScale: 1 })
140
+
141
+ render(<FilterDropdownListHeaderComponent />)
142
+
143
+ expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
144
+ flexDirection: 'row',
145
+ })
146
+ })
147
+
148
+ it('stacks the actions under the dropdown at the first font size above the default', () => {
149
+ mockUseWindowDimensions.mockReturnValue({ fontScale: 1.12 })
150
+
151
+ render(<FilterDropdownListHeaderComponent />)
152
+
153
+ expect(screen.getByTestId('conversations-filter-heading-row')).toHaveStyle({
154
+ flexDirection: 'column',
155
+ })
156
+ })
157
+
158
+ it('caps the trigger to the room beside the actions so a long label truncates', () => {
159
+ render(<FilterDropdownListHeaderComponent />)
160
+
161
+ fireEvent(screen.getByTestId('conversations-filter-heading-row'), 'layout', {
162
+ nativeEvent: { layout: { width: 360 } },
163
+ })
164
+ fireEvent(screen.getByTestId('conversations-filter-actions'), 'layout', {
165
+ nativeEvent: { layout: { width: 160 } },
166
+ })
167
+
168
+ expect(screen.getByRole('button', { name: 'All conversations' })).toHaveStyle({
169
+ maxWidth: 156,
170
+ })
171
+ })
172
+
128
173
  it('offers every filter the person can use, then More filters', () => {
129
174
  render(<FilterDropdownListHeaderComponent />)
130
175
 
@@ -1,13 +1,13 @@
1
1
  import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
2
- import React, { useCallback, useMemo } from 'react'
3
- import { Alert, StyleSheet, View } from 'react-native'
2
+ import React, { useCallback, useMemo, useState } from 'react'
3
+ import { Alert, LayoutChangeEvent, StyleSheet, View } from 'react-native'
4
4
  import { FilterDropdown, FilterDropdownOption, TextButton } from '../../../components'
5
5
  import {
6
- useAtFontScaleBreakpoint,
7
6
  useCanBrowseArchivedConversations,
8
7
  useCanCreateConversations,
9
8
  useCurrentPersonCache,
10
9
  useDirectMessagesEligible,
10
+ useFontScale,
11
11
  useHasDirectMessages,
12
12
  useNewConversationEntry,
13
13
  useTheme,
@@ -25,6 +25,8 @@ import { ConversationsScreenProps } from '../conversations_screen'
25
25
  import { ListHeaderActionButton } from './list_header_action_button'
26
26
 
27
27
  const ROW_PADDING_HORIZONTAL = 16
28
+ const HEADING_ROW_GAP = 12
29
+ const DEFAULT_FONT_SCALE = 1
28
30
  const ALL_CONVERSATIONS_LABEL = 'All conversations'
29
31
  const ARCHIVED_LABEL = 'Archived'
30
32
 
@@ -205,11 +207,31 @@ export const FilterDropdownListHeaderComponent = () => {
205
207
 
206
208
  const showMoreFilters = canFilterByGroups || canFilterByTeams || canBrowseArchived
207
209
 
208
- const styles = useStyles()
210
+ const stacked = useFontScale() > DEFAULT_FONT_SCALE
211
+ const styles = useStyles({ stacked })
212
+
213
+ // The Android menu host measures the trigger at its intrinsic width, so flex shrinking never
214
+ // truncates the label. Cap it to the room left beside the action buttons instead.
215
+ const [rowWidth, setRowWidth] = useState<number>()
216
+ const [actionsWidth, setActionsWidth] = useState<number>()
217
+ const handleRowLayout = useCallback((event: LayoutChangeEvent) => {
218
+ setRowWidth(event.nativeEvent.layout.width)
219
+ }, [])
220
+ const handleActionsLayout = useCallback((event: LayoutChangeEvent) => {
221
+ setActionsWidth(event.nativeEvent.layout.width)
222
+ }, [])
223
+ const triggerMaxWidth =
224
+ !stacked && rowWidth !== undefined && actionsWidth !== undefined
225
+ ? rowWidth - ROW_PADDING_HORIZONTAL * 2 - actionsWidth - HEADING_ROW_GAP
226
+ : undefined
209
227
 
210
228
  return (
211
229
  <View style={styles.listHeader}>
212
- <View style={styles.headingRow}>
230
+ <View
231
+ style={styles.headingRow}
232
+ onLayout={handleRowLayout}
233
+ testID="conversations-filter-heading-row"
234
+ >
213
235
  <View style={styles.titleContainer}>
214
236
  <FilterDropdown
215
237
  options={options}
@@ -218,9 +240,14 @@ export const FilterDropdownListHeaderComponent = () => {
218
240
  onMoreFilters={handleOpenConversationFilters}
219
241
  showMoreFilters={showMoreFilters}
220
242
  triggerLabel={triggerLabel}
243
+ maxWidth={triggerMaxWidth}
221
244
  />
222
245
  </View>
223
- <View style={styles.actionsContainer}>
246
+ <View
247
+ style={styles.actionsContainer}
248
+ onLayout={handleActionsLayout}
249
+ testID="conversations-filter-actions"
250
+ >
224
251
  <ListHeaderActionButton
225
252
  name="general.cog"
226
253
  onPress={handleChatSettings}
@@ -265,9 +292,8 @@ export const FilterDropdownListHeaderComponent = () => {
265
292
  )
266
293
  }
267
294
 
268
- const useStyles = () => {
295
+ const useStyles = ({ stacked }: { stacked: boolean }) => {
269
296
  const theme = useTheme()
270
- const atFontScaleBreakpoint = useAtFontScaleBreakpoint()
271
297
 
272
298
  return StyleSheet.create({
273
299
  listHeader: {
@@ -277,11 +303,11 @@ const useStyles = () => {
277
303
  gap: 8,
278
304
  },
279
305
  headingRow: {
280
- gap: 12,
281
- flexDirection: atFontScaleBreakpoint ? 'column' : 'row',
282
- alignItems: atFontScaleBreakpoint ? 'flex-start' : 'center',
306
+ gap: HEADING_ROW_GAP,
307
+ flexDirection: stacked ? 'column' : 'row',
308
+ alignItems: stacked ? 'flex-start' : 'center',
283
309
  justifyContent: 'space-between',
284
- paddingBottom: atFontScaleBreakpoint ? 4 : 0,
310
+ paddingBottom: stacked ? 4 : 0,
285
311
  paddingHorizontal: ROW_PADDING_HORIZONTAL,
286
312
  },
287
313
  titleContainer: {
@@ -6,6 +6,7 @@ export const MAX_FONT_SIZE_MULTIPLIER_LANDMARK = 1.2 // Use in headers, footers,
6
6
 
7
7
  export const CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL = 16 // TODO: move to `screens/conversation/utils/styles` when `/screens/conversation` is created
8
8
  export const MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH = 32 // TODO: move to `screens/conversation/utils/styles` when `/screens/conversation` is created
9
+ export const MESSAGE_BACKGROUND_OUTSET = 2
9
10
 
10
11
  export const platformFontWeightMedium = Platform.select({
11
12
  ios: tokens.fontWeightMedium,