@planningcenter/chat-react-native 3.15.0-rc.1 → 3.15.0-rc.10

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 (160) hide show
  1. package/build/components/conversations/conversation_actions.js +1 -1
  2. package/build/components/conversations/conversation_actions.js.map +1 -1
  3. package/build/components/display/action_button.js +1 -3
  4. package/build/components/display/action_button.js.map +1 -1
  5. package/build/components/display/avatar.d.ts +3 -1
  6. package/build/components/display/avatar.d.ts.map +1 -1
  7. package/build/components/display/avatar.js +2 -2
  8. package/build/components/display/avatar.js.map +1 -1
  9. package/build/components/display/avatar_group.d.ts +3 -1
  10. package/build/components/display/avatar_group.d.ts.map +1 -1
  11. package/build/components/display/avatar_group.js +2 -2
  12. package/build/components/display/avatar_group.js.map +1 -1
  13. package/build/components/display/icon.d.ts +26 -13
  14. package/build/components/display/icon.d.ts.map +1 -1
  15. package/build/components/display/icon.js +0 -12
  16. package/build/components/display/icon.js.map +1 -1
  17. package/build/components/display/index.d.ts +1 -0
  18. package/build/components/display/index.d.ts.map +1 -1
  19. package/build/components/display/index.js +1 -0
  20. package/build/components/display/index.js.map +1 -1
  21. package/build/components/display/pressable_row.d.ts +14 -0
  22. package/build/components/display/pressable_row.d.ts.map +1 -0
  23. package/build/components/display/pressable_row.js +65 -0
  24. package/build/components/display/pressable_row.js.map +1 -0
  25. package/build/components/primitive/avatar_primitive.d.ts +2 -0
  26. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  27. package/build/components/primitive/avatar_primitive.js +20 -19
  28. package/build/components/primitive/avatar_primitive.js.map +1 -1
  29. package/build/components/primitive/form_sheet.d.ts +3 -2
  30. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  31. package/build/components/primitive/form_sheet.js +5 -3
  32. package/build/components/primitive/form_sheet.js.map +1 -1
  33. package/build/hooks/use_api.d.ts +1 -1
  34. package/build/hooks/use_api.d.ts.map +1 -1
  35. package/build/hooks/use_api.js.map +1 -1
  36. package/build/hooks/use_api_client.d.ts +1 -1
  37. package/build/hooks/use_api_client.d.ts.map +1 -1
  38. package/build/hooks/use_api_client.js +1 -1
  39. package/build/hooks/use_api_client.js.map +1 -1
  40. package/build/hooks/use_app_name.d.ts +3 -0
  41. package/build/hooks/use_app_name.d.ts.map +1 -0
  42. package/build/hooks/use_app_name.js +12 -0
  43. package/build/hooks/use_app_name.js.map +1 -0
  44. package/build/hooks/use_async_storage.d.ts +1 -1
  45. package/build/hooks/use_async_storage.d.ts.map +1 -1
  46. package/build/hooks/use_async_storage.js +6 -5
  47. package/build/hooks/use_async_storage.js.map +1 -1
  48. package/build/hooks/use_report_bug_action.d.ts +1 -1
  49. package/build/hooks/use_report_bug_action.d.ts.map +1 -1
  50. package/build/hooks/use_report_bug_action.js +1 -9
  51. package/build/hooks/use_report_bug_action.js.map +1 -1
  52. package/build/hooks/use_suspense_api.d.ts +1 -1
  53. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  54. package/build/hooks/use_suspense_api.js.map +1 -1
  55. package/build/index.d.ts +2 -0
  56. package/build/index.d.ts.map +1 -1
  57. package/build/index.js +2 -0
  58. package/build/index.js.map +1 -1
  59. package/build/navigation/index.d.ts +20 -5
  60. package/build/navigation/index.d.ts.map +1 -1
  61. package/build/navigation/index.js +23 -15
  62. package/build/navigation/index.js.map +1 -1
  63. package/build/polyfills/events/CustomEvent.d.ts +21 -0
  64. package/build/polyfills/events/CustomEvent.d.ts.map +1 -0
  65. package/build/polyfills/events/CustomEvent.js +22 -0
  66. package/build/polyfills/events/CustomEvent.js.map +1 -0
  67. package/build/polyfills/events/Event.d.ts +49 -0
  68. package/build/polyfills/events/Event.d.ts.map +1 -0
  69. package/build/polyfills/events/Event.js +125 -0
  70. package/build/polyfills/events/Event.js.map +1 -0
  71. package/build/polyfills/events/EventHandlerAttributes.d.ts +8 -0
  72. package/build/polyfills/events/EventHandlerAttributes.d.ts.map +1 -0
  73. package/build/polyfills/events/EventHandlerAttributes.js +46 -0
  74. package/build/polyfills/events/EventHandlerAttributes.js.map +1 -0
  75. package/build/polyfills/events/EventTarget.d.ts +33 -0
  76. package/build/polyfills/events/EventTarget.d.ts.map +1 -0
  77. package/build/polyfills/events/EventTarget.js +238 -0
  78. package/build/polyfills/events/EventTarget.js.map +1 -0
  79. package/build/polyfills/events/internals/EventInternals.d.ts +30 -0
  80. package/build/polyfills/events/internals/EventInternals.d.ts.map +1 -0
  81. package/build/polyfills/events/internals/EventInternals.js +76 -0
  82. package/build/polyfills/events/internals/EventInternals.js.map +1 -0
  83. package/build/polyfills/events/internals/EventTargetInternals.d.ts +9 -0
  84. package/build/polyfills/events/internals/EventTargetInternals.d.ts.map +1 -0
  85. package/build/polyfills/events/internals/EventTargetInternals.js +11 -0
  86. package/build/polyfills/events/internals/EventTargetInternals.js.map +1 -0
  87. package/build/polyfills/webidl/PlatformObjects.d.ts +31 -0
  88. package/build/polyfills/webidl/PlatformObjects.d.ts.map +1 -0
  89. package/build/polyfills/webidl/PlatformObjects.js +39 -0
  90. package/build/polyfills/webidl/PlatformObjects.js.map +1 -0
  91. package/build/screens/bug_report_screen.d.ts.map +1 -1
  92. package/build/screens/bug_report_screen.js +62 -57
  93. package/build/screens/bug_report_screen.js.map +1 -1
  94. package/build/screens/conversation_filters/components/conversation_filters.js +9 -7
  95. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  96. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
  97. package/build/screens/conversation_filters/components/rows.js +50 -31
  98. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  99. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  100. package/build/screens/conversations/conversations_screen.js +6 -6
  101. package/build/screens/conversations/conversations_screen.js.map +1 -1
  102. package/build/screens/design_system_screen.js +1 -1
  103. package/build/screens/design_system_screen.js.map +1 -1
  104. package/build/screens/get_help_screen.d.ts +5 -0
  105. package/build/screens/get_help_screen.d.ts.map +1 -0
  106. package/build/screens/get_help_screen.js +94 -0
  107. package/build/screens/get_help_screen.js.map +1 -0
  108. package/build/screens/message_actions_screen.d.ts +1 -1
  109. package/build/screens/message_actions_screen.d.ts.map +1 -1
  110. package/build/screens/message_actions_screen.js +14 -11
  111. package/build/screens/message_actions_screen.js.map +1 -1
  112. package/build/utils/client/index.d.ts +1 -0
  113. package/build/utils/client/index.d.ts.map +1 -1
  114. package/build/utils/client/index.js +1 -0
  115. package/build/utils/client/index.js.map +1 -1
  116. package/build/utils/client/types.d.ts +61 -0
  117. package/build/utils/client/types.d.ts.map +1 -0
  118. package/build/utils/client/types.js +2 -0
  119. package/build/utils/client/types.js.map +1 -0
  120. package/build/utils/theme.d.ts +1 -0
  121. package/build/utils/theme.d.ts.map +1 -1
  122. package/build/utils/theme.js +2 -0
  123. package/build/utils/theme.js.map +1 -1
  124. package/package.json +5 -5
  125. package/src/__tests__/event-polyfill.test.ts +314 -0
  126. package/src/components/conversations/conversation_actions.tsx +1 -1
  127. package/src/components/display/action_button.tsx +1 -4
  128. package/src/components/display/avatar.tsx +5 -1
  129. package/src/components/display/avatar_group.tsx +5 -1
  130. package/src/components/display/icon.tsx +17 -14
  131. package/src/components/display/index.ts +1 -0
  132. package/src/components/display/pressable_row.tsx +103 -0
  133. package/src/components/primitive/avatar_primitive.tsx +35 -19
  134. package/src/components/primitive/form_sheet.tsx +33 -5
  135. package/src/hooks/use_api.ts +1 -1
  136. package/src/hooks/use_api_client.ts +2 -2
  137. package/src/hooks/use_app_name.ts +17 -0
  138. package/src/hooks/use_async_storage.ts +8 -5
  139. package/src/hooks/use_report_bug_action.ts +2 -10
  140. package/src/hooks/use_suspense_api.ts +1 -1
  141. package/src/index.tsx +2 -0
  142. package/src/navigation/index.tsx +38 -25
  143. package/src/polyfills/events/CustomEvent.ts +32 -0
  144. package/src/polyfills/events/Event.ts +186 -0
  145. package/src/polyfills/events/EventHandlerAttributes.ts +67 -0
  146. package/src/polyfills/events/EventTarget.ts +360 -0
  147. package/src/polyfills/events/README.md +1 -0
  148. package/src/polyfills/events/internals/EventInternals.ts +95 -0
  149. package/src/polyfills/events/internals/EventTargetInternals.ts +16 -0
  150. package/src/polyfills/webidl/PlatformObjects.ts +50 -0
  151. package/src/screens/bug_report_screen.tsx +79 -67
  152. package/src/screens/conversation_filters/components/conversation_filters.tsx +10 -7
  153. package/src/screens/conversation_filters/components/rows.tsx +63 -50
  154. package/src/screens/conversations/conversations_screen.tsx +7 -7
  155. package/src/screens/design_system_screen.tsx +1 -1
  156. package/src/screens/get_help_screen.tsx +131 -0
  157. package/src/screens/message_actions_screen.tsx +34 -12
  158. package/src/utils/client/index.ts +1 -0
  159. package/src/utils/theme.ts +3 -0
  160. /package/src/utils/client/{types.d.ts → types.ts} +0 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Tests for Event/CustomEvent/EventTarget polyfills
3
+ */
4
+
5
+ import Event from '../polyfills/events/Event'
6
+ import EventTarget from '../polyfills/events/EventTarget'
7
+ import { CustomEvent } from '../polyfills/events/CustomEvent'
8
+
9
+ // Set up global polyfill for tests
10
+ ;(global as any).Event = Event
11
+ ;(global as any).CustomEvent = CustomEvent
12
+
13
+ describe('Event Polyfill', () => {
14
+ describe('Event', () => {
15
+ it('should create a basic event', () => {
16
+ const event = new Event('test')
17
+ expect(event.type).toBe('test')
18
+ expect(event.bubbles).toBe(false)
19
+ expect(event.cancelable).toBe(false)
20
+ expect(event.composed).toBe(false)
21
+ expect(event.defaultPrevented).toBe(false)
22
+ expect(event.isTrusted).toBe(false)
23
+ expect(event.eventPhase).toBe(Event.NONE)
24
+ expect(typeof event.timeStamp).toBe('number')
25
+ })
26
+
27
+ it('should create an event with options', () => {
28
+ const event = new Event('test', {
29
+ bubbles: true,
30
+ cancelable: true,
31
+ composed: true,
32
+ })
33
+ expect(event.bubbles).toBe(true)
34
+ expect(event.cancelable).toBe(true)
35
+ expect(event.composed).toBe(true)
36
+ })
37
+
38
+ it('should have correct phase constants', () => {
39
+ expect(Event.NONE).toBe(0)
40
+ expect(Event.CAPTURING_PHASE).toBe(1)
41
+ expect(Event.AT_TARGET).toBe(2)
42
+ expect(Event.BUBBLING_PHASE).toBe(3)
43
+ })
44
+
45
+ it('should prevent default when cancelable', () => {
46
+ const event = new Event('test', { cancelable: true })
47
+ expect(event.defaultPrevented).toBe(false)
48
+ event.preventDefault()
49
+ expect(event.defaultPrevented).toBe(true)
50
+ })
51
+
52
+ it('should not prevent default when not cancelable', () => {
53
+ const event = new Event('test', { cancelable: false })
54
+ event.preventDefault()
55
+ expect(event.defaultPrevented).toBe(false)
56
+ })
57
+
58
+ it('should support stopPropagation', () => {
59
+ const event = new Event('test')
60
+ // No error thrown - internal flags are set
61
+ event.stopPropagation()
62
+ event.stopImmediatePropagation()
63
+ })
64
+ })
65
+
66
+ describe('CustomEvent', () => {
67
+ it('should create a custom event with detail', () => {
68
+ const detail = { foo: 'bar', count: 42 }
69
+ const event = new CustomEvent('custom', { detail })
70
+ expect(event.type).toBe('custom')
71
+ expect(event.detail).toEqual(detail)
72
+ })
73
+
74
+ it('should inherit from Event', () => {
75
+ const event = new CustomEvent('custom', {
76
+ bubbles: true,
77
+ cancelable: true,
78
+ detail: 'test',
79
+ })
80
+ expect(event instanceof Event).toBe(true)
81
+ expect(event.bubbles).toBe(true)
82
+ expect(event.cancelable).toBe(true)
83
+ })
84
+
85
+ it('should handle undefined detail', () => {
86
+ const event = new CustomEvent('custom')
87
+ expect(event.detail).toBeUndefined()
88
+ })
89
+ })
90
+
91
+ describe('EventTarget', () => {
92
+ let target: EventTarget
93
+
94
+ beforeEach(() => {
95
+ target = new EventTarget()
96
+ })
97
+
98
+ it('should add and remove event listeners', () => {
99
+ const listener = jest.fn()
100
+ target.addEventListener('test', listener)
101
+
102
+ const event = new Event('test')
103
+ const result = target.dispatchEvent(event)
104
+
105
+ expect(listener).toHaveBeenCalledWith(event)
106
+ expect(listener).toHaveBeenCalledTimes(1)
107
+ expect(result).toBe(true) // not prevented
108
+
109
+ target.removeEventListener('test', listener)
110
+ target.dispatchEvent(new Event('test'))
111
+ expect(listener).toHaveBeenCalledTimes(1) // not called again
112
+ })
113
+
114
+ it('should handle multiple listeners for the same event', () => {
115
+ const listener1 = jest.fn()
116
+ const listener2 = jest.fn()
117
+
118
+ target.addEventListener('test', listener1)
119
+ target.addEventListener('test', listener2)
120
+
121
+ target.dispatchEvent(new Event('test'))
122
+
123
+ expect(listener1).toHaveBeenCalledTimes(1)
124
+ expect(listener2).toHaveBeenCalledTimes(1)
125
+ })
126
+
127
+ it('should handle once option', () => {
128
+ const listener = jest.fn()
129
+ target.addEventListener('test', listener, { once: true })
130
+
131
+ target.dispatchEvent(new Event('test'))
132
+ target.dispatchEvent(new Event('test'))
133
+
134
+ expect(listener).toHaveBeenCalledTimes(1)
135
+ })
136
+
137
+ it('should handle capture option', () => {
138
+ const listener = jest.fn()
139
+ target.addEventListener('test', listener, { capture: true })
140
+
141
+ target.dispatchEvent(new Event('test'))
142
+ expect(listener).toHaveBeenCalledTimes(1)
143
+
144
+ target.removeEventListener('test', listener, { capture: true })
145
+ target.dispatchEvent(new Event('test'))
146
+ expect(listener).toHaveBeenCalledTimes(1)
147
+ })
148
+
149
+ it('should handle passive option', () => {
150
+ const listener = jest.fn()
151
+ target.addEventListener('test', listener, { passive: true })
152
+
153
+ target.dispatchEvent(new Event('test'))
154
+ expect(listener).toHaveBeenCalledTimes(1)
155
+ })
156
+
157
+ it('should handle object listeners with handleEvent', () => {
158
+ const listener = {
159
+ handleEvent: jest.fn(),
160
+ }
161
+
162
+ target.addEventListener('test', listener)
163
+ target.dispatchEvent(new Event('test'))
164
+
165
+ expect(listener.handleEvent).toHaveBeenCalledTimes(1)
166
+ })
167
+
168
+ it('should return false when event is prevented', () => {
169
+ const listener = (event: Event) => {
170
+ event.preventDefault()
171
+ }
172
+
173
+ target.addEventListener('test', listener)
174
+ const event = new Event('test', { cancelable: true })
175
+ const result = target.dispatchEvent(event)
176
+
177
+ expect(result).toBe(false)
178
+ expect(event.defaultPrevented).toBe(true)
179
+ })
180
+
181
+ it('should set event properties during dispatch', () => {
182
+ let capturedEvent: Event | null = null
183
+
184
+ const listener = (event: Event) => {
185
+ capturedEvent = event
186
+ expect(event.target).toBe(target)
187
+ expect(event.currentTarget).toBe(target)
188
+ expect(event.eventPhase).toBe(Event.AT_TARGET)
189
+ }
190
+
191
+ target.addEventListener('test', listener)
192
+ target.dispatchEvent(new Event('test'))
193
+
194
+ expect(capturedEvent).not.toBeNull()
195
+ // After dispatch, these should be reset
196
+ expect(capturedEvent!.currentTarget).toBeNull()
197
+ expect(capturedEvent!.eventPhase).toBe(Event.NONE)
198
+ })
199
+
200
+ it('should handle bubbling events', () => {
201
+ const listener = jest.fn()
202
+ target.addEventListener('test', listener)
203
+
204
+ const bubblingEvent = new Event('test', { bubbles: true })
205
+ target.dispatchEvent(bubblingEvent)
206
+
207
+ expect(listener).toHaveBeenCalledWith(bubblingEvent)
208
+ })
209
+
210
+ it('should handle error in listener gracefully', () => {
211
+ const errorListener = () => {
212
+ throw new Error('Test error')
213
+ }
214
+ const normalListener = jest.fn()
215
+
216
+ // Mock console.error to avoid test output noise
217
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
218
+
219
+ target.addEventListener('test', errorListener)
220
+ target.addEventListener('test', normalListener)
221
+
222
+ target.dispatchEvent(new Event('test'))
223
+
224
+ expect(normalListener).toHaveBeenCalledTimes(1)
225
+ expect(consoleErrorSpy).toHaveBeenCalled()
226
+
227
+ consoleErrorSpy.mockRestore()
228
+ })
229
+
230
+ it('should ignore null listeners', () => {
231
+ // Should not throw
232
+ target.addEventListener('test', null)
233
+ target.removeEventListener('test', null)
234
+ target.dispatchEvent(new Event('test'))
235
+ })
236
+
237
+ it('should handle composed path', () => {
238
+ let composedPath: readonly EventTarget[] | null = null
239
+
240
+ const listener = (event: Event) => {
241
+ composedPath = event.composedPath()
242
+ }
243
+
244
+ target.addEventListener('test', listener)
245
+ target.dispatchEvent(new Event('test'))
246
+
247
+ expect(composedPath).toEqual([target])
248
+ })
249
+
250
+ it('should prevent duplicate listeners', () => {
251
+ const listener = jest.fn()
252
+
253
+ target.addEventListener('test', listener)
254
+ target.addEventListener('test', listener) // Same listener
255
+
256
+ target.dispatchEvent(new Event('test'))
257
+ expect(listener).toHaveBeenCalledTimes(1) // Only called once
258
+ })
259
+ })
260
+
261
+ describe('Event flow integration', () => {
262
+ it('should work with CustomEvent dispatch', () => {
263
+ const target = new EventTarget()
264
+ const listener = jest.fn()
265
+ const detail = { message: 'hello' }
266
+
267
+ target.addEventListener('custom', listener)
268
+
269
+ const event = new CustomEvent('custom', {
270
+ detail,
271
+ bubbles: true,
272
+ cancelable: true,
273
+ })
274
+
275
+ const result = target.dispatchEvent(event)
276
+
277
+ expect(listener).toHaveBeenCalledWith(event)
278
+ expect(listener.mock.calls[0][0].detail).toEqual(detail)
279
+ expect(result).toBe(true)
280
+ })
281
+
282
+ it('should handle stopImmediatePropagation', () => {
283
+ const target = new EventTarget()
284
+ const listener1 = jest.fn((event: Event) => {
285
+ event.stopImmediatePropagation()
286
+ })
287
+ const listener2 = jest.fn()
288
+
289
+ target.addEventListener('test', listener1)
290
+ target.addEventListener('test', listener2)
291
+
292
+ target.dispatchEvent(new Event('test'))
293
+
294
+ expect(listener1).toHaveBeenCalledTimes(1)
295
+ expect(listener2).toHaveBeenCalledTimes(0) // Stopped
296
+ })
297
+ })
298
+
299
+ describe('Global polyfill installation', () => {
300
+ it('should make Event available globally', () => {
301
+ // This tests that our polyfill setup works
302
+ expect(typeof (global as any).Event).toBe('function')
303
+ expect(typeof (global as any).CustomEvent).toBe('function')
304
+
305
+ const globalEvent = new (global as any).Event('test')
306
+ expect(globalEvent.type).toBe('test')
307
+
308
+ const globalCustomEvent = new (global as any).CustomEvent('custom', {
309
+ detail: 'test',
310
+ })
311
+ expect(globalCustomEvent.detail).toBe('test')
312
+ })
313
+ })
314
+ })
@@ -219,7 +219,7 @@ const useStyles = () => {
219
219
  const { colors } = useTheme()
220
220
  return StyleSheet.create({
221
221
  swipeableChildContainer: {
222
- backgroundColor: colors.surfaceColor100,
222
+ backgroundColor: colors.conversationActionsBackground,
223
223
  },
224
224
  actionButtonContainer: {
225
225
  flexDirection: 'row',
@@ -66,14 +66,11 @@ export const ActionButton = ({
66
66
  )
67
67
  }
68
68
 
69
- const SCALE_THAT_BUTTONS_WRAP = 1.15
70
-
71
69
  const useStyles = () => {
72
70
  const { fontScale } = useWindowDimensions()
73
71
  const { bottom } = useSafeAreaInsets()
74
72
  const { colors } = useTheme()
75
73
  const containerVerticalPadding = 16
76
- const isButtonsWrapping = fontScale >= SCALE_THAT_BUTTONS_WRAP
77
74
 
78
75
  return StyleSheet.create({
79
76
  container: {
@@ -87,7 +84,7 @@ const useStyles = () => {
87
84
  },
88
85
  buttonRow: {
89
86
  flexDirection: 'row',
90
- justifyContent: isButtonsWrapping ? 'center' : 'space-between',
87
+ justifyContent: fontScale > 1 ? 'center' : 'space-between',
91
88
  alignItems: 'center',
92
89
  gap: 16,
93
90
  flexWrap: 'wrap-reverse',
@@ -12,6 +12,8 @@ interface AvatarProps {
12
12
  presence?: AvatarPresenceProps['presence']
13
13
  showFallback?: boolean
14
14
  fallbackIconName?: IconString
15
+ style?: AvatarRootProps['style']
16
+ maxFontSizeMultiplier?: AvatarRootProps['maxFontSizeMultiplier']
15
17
  }
16
18
 
17
19
  export function Avatar({
@@ -20,11 +22,13 @@ export function Avatar({
20
22
  sourceUri,
21
23
  showFallback = false,
22
24
  fallbackIconName = 'general.person',
25
+ style,
26
+ maxFontSizeMultiplier,
23
27
  }: AvatarProps) {
24
28
  const shouldShowFallback = showFallback || !sourceUri
25
29
 
26
30
  return (
27
- <AvatarPrimitive.Root size={size}>
31
+ <AvatarPrimitive.Root size={size} style={style} maxFontSizeMultiplier={maxFontSizeMultiplier}>
28
32
  <AvatarPrimitive.Mask>
29
33
  {shouldShowFallback ? (
30
34
  <AvatarPrimitive.ImageFallback name={fallbackIconName} />
@@ -10,6 +10,8 @@ interface AvatarGroupDisplayProps {
10
10
  showFallback?: boolean
11
11
  fallbackIconName?: IconString
12
12
  size?: AvatarRootProps['size']
13
+ style?: AvatarRootProps['style']
14
+ maxFontSizeMultiplier?: AvatarRootProps['maxFontSizeMultiplier']
13
15
  }
14
16
 
15
17
  export function AvatarGroup({
@@ -17,11 +19,13 @@ export function AvatarGroup({
17
19
  showFallback = false,
18
20
  fallbackIconName = 'general.person',
19
21
  size = 'lg',
22
+ style,
23
+ maxFontSizeMultiplier,
20
24
  }: AvatarGroupDisplayProps) {
21
25
  const shouldShowFallback = showFallback || !sourceUris || sourceUris.length === 0
22
26
 
23
27
  return (
24
- <AvatarPrimitive.Root size={size}>
28
+ <AvatarPrimitive.Root size={size} style={style} maxFontSizeMultiplier={maxFontSizeMultiplier}>
25
29
  <AvatarPrimitive.Mask>
26
30
  {shouldShowFallback ? (
27
31
  <AvatarPrimitive.ImageFallback name={fallbackIconName} />
@@ -5,29 +5,17 @@ import { SvgXml } from 'react-native-svg'
5
5
  import type { XmlProps } from 'react-native-svg'
6
6
  import { useFontScale, useTheme } from '../../hooks'
7
7
 
8
- // @ts-ignore
9
8
  import * as accounts from '@planningcenter/icons/paths/accounts'
10
- // @ts-ignore
11
9
  import * as api from '@planningcenter/icons/paths/api'
12
- // @ts-ignore
13
10
  import * as brand from '@planningcenter/icons/paths/brand'
14
- // @ts-ignore
15
11
  import * as calendar from '@planningcenter/icons/paths/calendar'
16
- // @ts-ignore
17
12
  import * as chat from '@planningcenter/icons/paths/chat'
18
- // @ts-ignore
19
13
  import * as churchCenter from '@planningcenter/icons/paths/church-center'
20
- // @ts-ignore
21
14
  import * as general from '@planningcenter/icons/paths/general'
22
- // @ts-ignore
23
15
  import * as groups from '@planningcenter/icons/paths/groups'
24
- // @ts-ignore
25
16
  import * as logomark from '@planningcenter/icons/paths/logomark'
26
- // @ts-ignore
27
17
  import * as people from '@planningcenter/icons/paths/people'
28
- // @ts-ignore
29
18
  import * as services from '@planningcenter/icons/paths/services'
30
- // @ts-ignore
31
19
  import * as publishing from '@planningcenter/icons/paths/publishing'
32
20
 
33
21
  // =================================
@@ -57,7 +45,22 @@ export type IconStyle = ViewStyle & {
57
45
  }
58
46
 
59
47
  export type IconSetName = keyof typeof ICONS
60
- export type IconString = `${IconSetName}.${(typeof ICONS)[IconSetName]}`
48
+
49
+ type IconName<T extends IconSetName> = keyof (typeof ICONS)[T] & string
50
+
51
+ export type IconString =
52
+ | `accounts.${IconName<'accounts'>}`
53
+ | `api.${IconName<'api'>}`
54
+ | `brand.${IconName<'brand'>}`
55
+ | `calendar.${IconName<'calendar'>}`
56
+ | `chat.${IconName<'chat'>}`
57
+ | `churchCenter.${IconName<'churchCenter'>}`
58
+ | `general.${IconName<'general'>}`
59
+ | `groups.${IconName<'groups'>}`
60
+ | `logomark.${IconName<'logomark'>}`
61
+ | `people.${IconName<'people'>}`
62
+ | `services.${IconName<'services'>}`
63
+ | `publishing.${IconName<'publishing'>}`
61
64
 
62
65
  // =================================
63
66
  // ====== Component ================
@@ -137,7 +140,7 @@ const useGetIconSize = (size?: number, style?: IconStyle, maxFontSizeMultiplier?
137
140
  const getIconPath = (name: IconString): string => {
138
141
  const [setName, iconName] = name.split('.')
139
142
 
140
- return ICONS[setName as IconSetName]?.[iconName]
143
+ return (ICONS[setName as IconSetName] as Record<string, string>)?.[iconName]
141
144
  }
142
145
 
143
146
  // =================================
@@ -19,3 +19,4 @@ export * from './text_inline_button'
19
19
  export * from './text'
20
20
  export * from './toggle_button'
21
21
  export * from './keyboard_view'
22
+ export * from './pressable_row'
@@ -0,0 +1,103 @@
1
+ import { PlatformPressable } from '@react-navigation/elements'
2
+ import { PropsWithChildren } from 'react'
3
+ import { StyleSheet, View, ViewStyle } from 'react-native'
4
+ import { useTheme } from '../../hooks'
5
+ import { Icon, IconProps } from './icon'
6
+ import { Text } from './text'
7
+
8
+ export interface PressableRowProps extends PropsWithChildren {
9
+ isActive?: boolean
10
+ onPress: () => void
11
+ style?: ViewStyle
12
+ textStyle?: any
13
+ text: string
14
+ iconPath?: IconProps['name']
15
+ iconColor?: string
16
+ }
17
+
18
+ export const PressableRow = ({
19
+ children,
20
+ isActive,
21
+ onPress,
22
+ style,
23
+ text,
24
+ textStyle,
25
+ iconPath,
26
+ iconColor,
27
+ }: PressableRowProps) => {
28
+ const styles = useRowStyles({ isActive, iconColor })
29
+
30
+ return (
31
+ <PlatformPressable
32
+ style={styles.container}
33
+ onPress={onPress}
34
+ accessibilityRole="radio"
35
+ accessibilityState={{ selected: isActive }}
36
+ >
37
+ <View style={[styles.innerContainer, style]}>
38
+ {children}
39
+ <Text style={textStyle}>{text}</Text>
40
+ <Icon
41
+ name={iconPath || 'general.check'}
42
+ size={16}
43
+ style={styles.rowIconRight}
44
+ accessibilityElementsHidden
45
+ />
46
+ </View>
47
+ </PlatformPressable>
48
+ )
49
+ }
50
+
51
+ const ASPECT_RATIO = 16 / 9
52
+ const THUMBNAIL_WIDTH = 80
53
+ const THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH / ASPECT_RATIO
54
+
55
+ const useRowStyles = ({
56
+ isActive = false,
57
+ iconColor,
58
+ }: { isActive?: boolean; iconColor?: string } = {}) => {
59
+ const theme = useTheme()
60
+ return StyleSheet.create({
61
+ container: {
62
+ paddingLeft: 16,
63
+ },
64
+ innerContainer: {
65
+ flexDirection: 'row',
66
+ alignItems: 'center',
67
+ gap: 12,
68
+ borderBottomWidth: 1,
69
+ borderBottomColor: theme.colors.fillColorNeutral050Base,
70
+ paddingVertical: 12,
71
+ paddingRight: 16,
72
+ },
73
+ borderLessRow: {
74
+ flexDirection: 'row',
75
+ alignItems: 'center',
76
+ gap: 12,
77
+ paddingVertical: 12,
78
+ paddingRight: 16,
79
+ paddingLeft: 16,
80
+ },
81
+ viewMoreButton: {
82
+ flex: 1,
83
+ },
84
+ row: {},
85
+ rowImage: {
86
+ width: THUMBNAIL_WIDTH,
87
+ height: THUMBNAIL_HEIGHT,
88
+ borderRadius: 4,
89
+ },
90
+ rowContent: {
91
+ flexShrink: 1,
92
+ },
93
+ rowIconRight: {
94
+ marginLeft: 'auto',
95
+ color: iconColor || theme.colors.iconColorDefaultDim,
96
+ opacity: isActive ? 1 : 0,
97
+ },
98
+ rowTitle: {
99
+ fontSize: 16,
100
+ flexShrink: 1,
101
+ },
102
+ })
103
+ }
@@ -85,6 +85,7 @@ interface AvatarContextType {
85
85
  size: AvatarSize
86
86
  allImagesLoaded: boolean
87
87
  setAllImagesLoaded: React.Dispatch<React.SetStateAction<boolean>>
88
+ maxFontSizeMultiplier: number
88
89
  }
89
90
 
90
91
  const AvatarContext = createContext<AvatarContextType | null>(null)
@@ -104,15 +105,24 @@ function useAvatarContext() {
104
105
  interface AvatarRootProps {
105
106
  children: React.ReactNode
106
107
  size?: AvatarSize
108
+ style?: ViewProps['style']
109
+ maxFontSizeMultiplier?: number
107
110
  }
108
111
 
109
- function AvatarRoot({ children, size = 'md' }: AvatarRootProps) {
112
+ function AvatarRoot({
113
+ children,
114
+ size = 'md',
115
+ style,
116
+ maxFontSizeMultiplier = MAX_FONT_SIZE_MULTIPLIER,
117
+ }: AvatarRootProps) {
110
118
  const [allImagesLoaded, setAllImagesLoaded] = useState(false)
111
- const styles = useStyles({ size })
119
+ const styles = useStyles({ size, maxFontSizeMultiplier })
112
120
 
113
121
  return (
114
- <AvatarContext.Provider value={{ size, allImagesLoaded, setAllImagesLoaded }}>
115
- <View style={styles.rootContainer}>{children}</View>
122
+ <AvatarContext.Provider
123
+ value={{ size, allImagesLoaded, setAllImagesLoaded, maxFontSizeMultiplier }}
124
+ >
125
+ <View style={[styles.rootContainer, style]}>{children}</View>
116
126
  </AvatarContext.Provider>
117
127
  )
118
128
  }
@@ -126,7 +136,8 @@ AvatarRoot.displayName = 'Avatar.Root'
126
136
  type AvatarMaskProps = ViewProps
127
137
 
128
138
  function AvatarMask({ children, ...props }: AvatarMaskProps) {
129
- const styles = useStyles()
139
+ const { maxFontSizeMultiplier } = useAvatarContext()
140
+ const styles = useStyles({ maxFontSizeMultiplier })
130
141
 
131
142
  return (
132
143
  <View style={styles.mask} {...props}>
@@ -146,8 +157,8 @@ interface AvatarImageProps extends Omit<ImageProps, 'source' | 'alt'> {
146
157
  }
147
158
 
148
159
  function AvatarImage({ sourceUri, ...props }: AvatarImageProps) {
149
- const { size } = useAvatarContext()
150
- const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })
160
+ const { size, maxFontSizeMultiplier } = useAvatarContext()
161
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
151
162
  const scaledAvatarSize = AVATAR_PX[size] * fontScale
152
163
 
153
164
  return <Image source={{ uri: sourceUri }} loaderSize={scaledAvatarSize} {...props} alt="" />
@@ -172,9 +183,9 @@ interface AvatarImageFallbackProps {
172
183
  }
173
184
 
174
185
  function AvatarImageFallback({ name = 'general.person' }: AvatarImageFallbackProps) {
175
- const { size } = useAvatarContext()
176
- const styles = useStyles()
177
- const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })
186
+ const { size, maxFontSizeMultiplier } = useAvatarContext()
187
+ const styles = useStyles({ maxFontSizeMultiplier })
188
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
178
189
  const scaledIconSize = AVATAR_FALLBACK_ICON_PX[size] * fontScale
179
190
 
180
191
  return (
@@ -202,8 +213,8 @@ interface AvatarGroupProps {
202
213
  type AvatarIndex = 0 | 1 | 2 | 3
203
214
 
204
215
  function AvatarGroup({ sourceUris }: AvatarGroupProps) {
205
- const styles = useStyles()
206
- const { setAllImagesLoaded } = useAvatarContext()
216
+ const { setAllImagesLoaded, maxFontSizeMultiplier } = useAvatarContext()
217
+ const styles = useStyles({ maxFontSizeMultiplier })
207
218
  const [loadingStatus, setLoadingStatus] = useState<Record<AvatarIndex, boolean>>({
208
219
  0: false,
209
220
  1: false,
@@ -312,9 +323,9 @@ AvatarGroup.displayName = 'Avatar.Group'
312
323
  // =================================
313
324
 
314
325
  function AvatarGroupLoader() {
315
- const { size, allImagesLoaded } = useAvatarContext()
316
- const styles = useStyles({ size })
317
- const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })
326
+ const { size, allImagesLoaded, maxFontSizeMultiplier } = useAvatarContext()
327
+ const styles = useStyles({ size, maxFontSizeMultiplier })
328
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
318
329
  const scaledSpinnerSize = AVATAR_PX[size] * fontScale
319
330
 
320
331
  if (allImagesLoaded) return null
@@ -337,8 +348,8 @@ interface AvatarPresenceProps extends ViewProps {
337
348
  }
338
349
 
339
350
  function AvatarPresence({ presence, ...props }: AvatarPresenceProps) {
340
- const { size } = useAvatarContext()
341
- const styles = useStyles({ size, presence })
351
+ const { size, maxFontSizeMultiplier } = useAvatarContext()
352
+ const styles = useStyles({ size, presence, maxFontSizeMultiplier })
342
353
 
343
354
  return <View style={styles.presence} {...props} />
344
355
  }
@@ -352,11 +363,16 @@ AvatarPresence.displayName = 'Avatar.Presence'
352
363
  interface Styles {
353
364
  size?: AvatarSize
354
365
  presence?: AvatarPresenceType
366
+ maxFontSizeMultiplier?: number
355
367
  }
356
368
 
357
- const useStyles = ({ size = 'md', presence = 'offline' }: Styles = {}) => {
369
+ const useStyles = ({
370
+ size = 'md',
371
+ presence = 'offline',
372
+ maxFontSizeMultiplier = MAX_FONT_SIZE_MULTIPLIER,
373
+ }: Styles = {}) => {
358
374
  const { colors } = useTheme()
359
- const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })
375
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
360
376
  const PRESENCE_COLOR = {
361
377
  online: colors.fillColorInteractionOnlineDefault,
362
378
  offline: colors.iconColorDefaultDisabled,