@planningcenter/chat-react-native 3.2.0-rc.0 → 3.2.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 (193) hide show
  1. package/build/components/conversation/message_form.js.map +1 -1
  2. package/build/components/conversation/message_reaction.d.ts +2 -2
  3. package/build/components/conversations/action_toggle_button.d.ts +21 -0
  4. package/build/components/conversations/action_toggle_button.d.ts.map +1 -0
  5. package/build/components/conversations/action_toggle_button.js +47 -0
  6. package/build/components/conversations/action_toggle_button.js.map +1 -0
  7. package/build/components/conversations/conversation_actions.d.ts +10 -0
  8. package/build/components/conversations/conversation_actions.d.ts.map +1 -0
  9. package/build/components/conversations/conversation_actions.js +70 -0
  10. package/build/components/conversations/conversation_actions.js.map +1 -0
  11. package/build/components/conversations/conversation_preview.d.ts +2 -1
  12. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  13. package/build/components/conversations/conversation_preview.js +27 -9
  14. package/build/components/conversations/conversation_preview.js.map +1 -1
  15. package/build/components/conversations/conversations.d.ts +5 -3
  16. package/build/components/conversations/conversations.d.ts.map +1 -1
  17. package/build/components/conversations/conversations.js +11 -7
  18. package/build/components/conversations/conversations.js.map +1 -1
  19. package/build/components/conversations/mute_indicator.d.ts +5 -0
  20. package/build/components/conversations/mute_indicator.d.ts.map +1 -0
  21. package/build/components/conversations/mute_indicator.js +19 -0
  22. package/build/components/conversations/mute_indicator.js.map +1 -0
  23. package/build/components/conversations/unread_count_badge.d.ts +2 -1
  24. package/build/components/conversations/unread_count_badge.d.ts.map +1 -1
  25. package/build/components/conversations/unread_count_badge.js +9 -1
  26. package/build/components/conversations/unread_count_badge.js.map +1 -1
  27. package/build/components/display/badge.d.ts +5 -4
  28. package/build/components/display/badge.d.ts.map +1 -1
  29. package/build/components/display/badge.js +5 -2
  30. package/build/components/display/badge.js.map +1 -1
  31. package/build/components/display/button.d.ts +3 -2
  32. package/build/components/display/button.d.ts.map +1 -1
  33. package/build/components/display/button.js.map +1 -1
  34. package/build/components/display/icon.d.ts +14 -1
  35. package/build/components/display/icon.d.ts.map +1 -1
  36. package/build/components/display/icon.js +9 -0
  37. package/build/components/display/icon.js.map +1 -1
  38. package/build/components/display/icon_button.d.ts +2 -2
  39. package/build/components/display/icon_button.d.ts.map +1 -1
  40. package/build/components/display/icon_button.js.map +1 -1
  41. package/build/components/display/index.d.ts +1 -0
  42. package/build/components/display/index.d.ts.map +1 -1
  43. package/build/components/display/index.js +1 -0
  44. package/build/components/display/index.js.map +1 -1
  45. package/build/components/display/text.js.map +1 -1
  46. package/build/components/display/toggle_button.d.ts +43 -0
  47. package/build/components/display/toggle_button.d.ts.map +1 -0
  48. package/build/components/display/toggle_button.js +67 -0
  49. package/build/components/display/toggle_button.js.map +1 -0
  50. package/build/components/display/utils/button_colors.d.ts +3 -3
  51. package/build/components/display/utils/button_colors.d.ts.map +1 -1
  52. package/build/components/display/utils/button_colors.js +1 -1
  53. package/build/components/display/utils/button_colors.js.map +1 -1
  54. package/build/components/page/error_boundary.d.ts +1 -1
  55. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  56. package/build/components/primitive/avatar_primitive.js +3 -2
  57. package/build/components/primitive/avatar_primitive.js.map +1 -1
  58. package/build/components/primitive/banner_primitive.d.ts +2 -1
  59. package/build/components/primitive/banner_primitive.d.ts.map +1 -1
  60. package/build/components/primitive/banner_primitive.js.map +1 -1
  61. package/build/contexts/api_provider.js.map +1 -1
  62. package/build/contexts/swipeable_active_conversation.d.ts +11 -0
  63. package/build/contexts/swipeable_active_conversation.d.ts.map +1 -0
  64. package/build/contexts/swipeable_active_conversation.js +16 -0
  65. package/build/contexts/swipeable_active_conversation.js.map +1 -0
  66. package/build/hooks/use_conversation.d.ts +1 -1
  67. package/build/hooks/use_conversation.d.ts.map +1 -1
  68. package/build/hooks/use_conversation.js +1 -1
  69. package/build/hooks/use_conversation.js.map +1 -1
  70. package/build/hooks/use_create_android_ripple_color.d.ts +1 -1
  71. package/build/hooks/use_create_android_ripple_color.d.ts.map +1 -1
  72. package/build/hooks/use_jolt.js +1 -1
  73. package/build/hooks/use_jolt.js.map +1 -1
  74. package/build/navigation/index.d.ts +9 -1
  75. package/build/navigation/index.d.ts.map +1 -1
  76. package/build/navigation/index.js +13 -1
  77. package/build/navigation/index.js.map +1 -1
  78. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  79. package/build/screens/conversation_filters/components/rows.d.ts +2 -1
  80. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
  81. package/build/screens/conversation_filters/components/rows.js +19 -14
  82. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  83. package/build/screens/conversations/components/chat_group_badge.d.ts +3 -0
  84. package/build/screens/conversations/components/chat_group_badge.d.ts.map +1 -0
  85. package/build/screens/conversations/components/chat_group_badge.js +40 -0
  86. package/build/screens/conversations/components/chat_group_badge.js.map +1 -0
  87. package/build/screens/conversations/components/list_header_component.d.ts +3 -0
  88. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -0
  89. package/build/screens/conversations/components/list_header_component.js +88 -0
  90. package/build/screens/conversations/components/list_header_component.js.map +1 -0
  91. package/build/screens/{conversations_screen.d.ts → conversations/conversations_screen.d.ts} +1 -1
  92. package/build/screens/conversations/conversations_screen.d.ts.map +1 -0
  93. package/build/screens/conversations/conversations_screen.js +35 -0
  94. package/build/screens/conversations/conversations_screen.js.map +1 -0
  95. package/build/screens/create/conversation_create_screen.d.ts.map +1 -1
  96. package/build/screens/create/conversation_create_screen.js +0 -1
  97. package/build/screens/create/conversation_create_screen.js.map +1 -1
  98. package/build/screens/design_system_screen.d.ts.map +1 -1
  99. package/build/screens/design_system_screen.js +10 -1
  100. package/build/screens/design_system_screen.js.map +1 -1
  101. package/build/screens/message_actions_screen.js +1 -1
  102. package/build/screens/message_actions_screen.js.map +1 -1
  103. package/build/utils/client/client.d.ts +2 -2
  104. package/build/utils/client/client.d.ts.map +1 -1
  105. package/build/utils/client/client.js +12 -4
  106. package/build/utils/client/client.js.map +1 -1
  107. package/build/utils/client/request_helpers.d.ts +15 -8
  108. package/build/utils/client/request_helpers.d.ts.map +1 -1
  109. package/build/utils/client/request_helpers.js +2 -1
  110. package/build/utils/client/request_helpers.js.map +1 -1
  111. package/build/utils/client/transform_request_data.d.ts +11 -6
  112. package/build/utils/client/transform_request_data.d.ts.map +1 -1
  113. package/build/utils/client/transform_request_data.js +1 -1
  114. package/build/utils/client/transform_request_data.js.map +1 -1
  115. package/build/utils/client/transform_response.d.ts +1 -1
  116. package/build/utils/client/transform_response.d.ts.map +1 -1
  117. package/build/utils/client/transform_response.js +2 -0
  118. package/build/utils/client/transform_response.js.map +1 -1
  119. package/build/utils/client/utils.d.ts +3 -3
  120. package/build/utils/client/utils.d.ts.map +1 -1
  121. package/build/utils/client/utils.js +6 -6
  122. package/build/utils/client/utils.js.map +1 -1
  123. package/build/utils/date.d.ts.map +1 -1
  124. package/build/utils/date.js +1 -0
  125. package/build/utils/date.js.map +1 -1
  126. package/build/utils/deepCamelCaseKeys.d.ts.map +1 -1
  127. package/build/utils/deepCamelCaseKeys.js +3 -1
  128. package/build/utils/deepCamelCaseKeys.js.map +1 -1
  129. package/build/utils/parse_simple_markdown.d.ts +1 -1
  130. package/build/utils/parse_simple_markdown.d.ts.map +1 -1
  131. package/build/utils/parse_simple_markdown.js +2 -1
  132. package/build/utils/parse_simple_markdown.js.map +1 -1
  133. package/build/utils/space.js.map +1 -1
  134. package/build/utils/uri.d.ts +2 -2
  135. package/build/utils/uri.d.ts.map +1 -1
  136. package/build/utils/uri.js +2 -2
  137. package/build/utils/uri.js.map +1 -1
  138. package/build/vendor/tapestry/alias_tokens_color_map.d.ts +8 -0
  139. package/build/vendor/tapestry/alias_tokens_color_map.d.ts.map +1 -1
  140. package/build/vendor/tapestry/alias_tokens_color_map.js +8 -0
  141. package/build/vendor/tapestry/alias_tokens_color_map.js.map +1 -1
  142. package/build/vendor/tapestry/tokens.d.ts +2 -0
  143. package/build/vendor/tapestry/tokens.d.ts.map +1 -1
  144. package/build/vendor/tapestry/tokens.js +2 -0
  145. package/build/vendor/tapestry/tokens.js.map +1 -1
  146. package/package.json +8 -6
  147. package/src/components/conversation/message_form.tsx +1 -1
  148. package/src/components/conversations/action_toggle_button.tsx +83 -0
  149. package/src/components/conversations/conversation_actions.tsx +119 -0
  150. package/src/components/conversations/conversation_preview.tsx +46 -16
  151. package/src/components/conversations/conversations.tsx +38 -30
  152. package/src/components/conversations/mute_indicator.tsx +21 -0
  153. package/src/components/conversations/unread_count_badge.tsx +8 -1
  154. package/src/components/display/badge.tsx +12 -8
  155. package/src/components/display/button.tsx +3 -3
  156. package/src/components/display/icon.tsx +16 -3
  157. package/src/components/display/icon_button.tsx +2 -2
  158. package/src/components/display/index.ts +1 -0
  159. package/src/components/display/text.tsx +1 -1
  160. package/src/components/display/toggle_button.tsx +157 -0
  161. package/src/components/display/utils/button_colors.ts +8 -3
  162. package/src/components/primitive/avatar_primitive.tsx +5 -3
  163. package/src/components/primitive/banner_primitive.tsx +2 -2
  164. package/src/contexts/api_provider.tsx +1 -1
  165. package/src/contexts/swipeable_active_conversation.tsx +27 -0
  166. package/src/hooks/use_conversation.ts +5 -5
  167. package/src/hooks/use_jolt.ts +2 -2
  168. package/src/navigation/index.tsx +19 -1
  169. package/src/screens/conversation_filters/components/conversation_filters.tsx +1 -1
  170. package/src/screens/conversation_filters/components/rows.tsx +44 -14
  171. package/src/screens/conversations/components/chat_group_badge.tsx +47 -0
  172. package/src/screens/conversations/components/list_header_component.tsx +125 -0
  173. package/src/screens/conversations/conversations_screen.tsx +56 -0
  174. package/src/screens/create/conversation_create_screen.tsx +7 -4
  175. package/src/screens/design_system_screen.tsx +29 -0
  176. package/src/screens/message_actions_screen.tsx +2 -2
  177. package/src/utils/client/client.ts +15 -7
  178. package/src/utils/client/request_helpers.ts +15 -6
  179. package/src/utils/client/transform_request_data.ts +13 -4
  180. package/src/utils/client/transform_response.ts +3 -0
  181. package/src/utils/client/types.d.ts +2 -1
  182. package/src/utils/client/utils.ts +13 -12
  183. package/src/utils/date.ts +1 -0
  184. package/src/utils/deepCamelCaseKeys.ts +3 -2
  185. package/src/utils/parse_simple_markdown.ts +3 -1
  186. package/src/utils/space.ts +1 -1
  187. package/src/utils/uri.ts +2 -2
  188. package/src/vendor/tapestry/alias_tokens_color_map.ts +12 -0
  189. package/src/vendor/tapestry/tokens.ts +2 -0
  190. package/build/screens/conversations_screen.d.ts.map +0 -1
  191. package/build/screens/conversations_screen.js +0 -144
  192. package/build/screens/conversations_screen.js.map +0 -1
  193. package/src/screens/conversations_screen.tsx +0 -222
@@ -4,14 +4,24 @@ import type { ColorValue, StyleProp, ViewStyle } from 'react-native'
4
4
  import { SvgXml } from 'react-native-svg'
5
5
  import type { XmlProps } from 'react-native-svg'
6
6
  import { useFontScale, useTheme } from '../../hooks'
7
+
8
+ // @ts-ignore
7
9
  import * as api from '@planningcenter/icons/paths/api'
10
+ // @ts-ignore
8
11
  import * as brand from '@planningcenter/icons/paths/brand'
12
+ // @ts-ignore
9
13
  import * as calendar from '@planningcenter/icons/paths/calendar'
14
+ // @ts-ignore
10
15
  import * as churchCenter from '@planningcenter/icons/paths/church-center'
16
+ // @ts-ignore
11
17
  import * as general from '@planningcenter/icons/paths/general'
18
+ // @ts-ignore
12
19
  import * as groups from '@planningcenter/icons/paths/groups'
20
+ // @ts-ignore
13
21
  import * as logomark from '@planningcenter/icons/paths/logomark'
22
+ // @ts-ignore
14
23
  import * as people from '@planningcenter/icons/paths/people'
24
+ // @ts-ignore
15
25
  import * as services from '@planningcenter/icons/paths/services'
16
26
 
17
27
  // =================================
@@ -37,6 +47,9 @@ type IconStyle = ViewStyle & {
37
47
  color?: string
38
48
  }
39
49
 
50
+ export type IconSetName = keyof typeof ICONS
51
+ export type IconString = `${IconSetName}.${(typeof ICONS)[IconSetName]}`
52
+
40
53
  // =================================
41
54
  // ====== Component ================
42
55
  // =================================
@@ -46,7 +59,7 @@ export interface IconProps extends Omit<XmlProps, 'xml' | 'fontSize'> {
46
59
  * Made up of the set.iconName.
47
60
  * Example: "general.textMessage"
48
61
  */
49
- name: string
62
+ name: IconString
50
63
  /**
51
64
  * This sets a static size for the icon.
52
65
  * Providing a fontSize style will allow the icon to scale with the device's text a11y size.
@@ -112,10 +125,10 @@ const useGetIconSize = (size?: number, style?: IconStyle, maxFontSizeMultiplier?
112
125
  return size || FALLBACK_SIZE
113
126
  }
114
127
 
115
- const getIconPath = (name: string): string => {
128
+ const getIconPath = (name: IconString): string => {
116
129
  const [setName, iconName] = name.split('.')
117
130
 
118
- return ICONS[setName]?.[iconName]
131
+ return ICONS[setName as IconSetName]?.[iconName]
119
132
  }
120
133
 
121
134
  // =================================
@@ -2,7 +2,7 @@ import React from 'react'
2
2
  import { Pressable, StyleSheet } from 'react-native'
3
3
  import type { PressableProps, ViewStyle } from 'react-native'
4
4
  import { Icon } from './icon'
5
- import type { IconProps } from './icon'
5
+ import type { IconProps, IconString } from './icon'
6
6
  import { useTheme, useFontScale, useCreateAndroidRippleColor } from '../../hooks'
7
7
  import { platformPressedOpacityStyle } from '../../utils'
8
8
  import { Spinner } from './spinner'
@@ -67,7 +67,7 @@ interface IconButtonProps extends PressableProps {
67
67
  /**
68
68
  * Generates an icon from `@planningcenter/icons`
69
69
  */
70
- name: string
70
+ name: IconString
71
71
  /**
72
72
  * Changes the overall size of the button and its contents
73
73
  */
@@ -15,3 +15,4 @@ export * from './switch'
15
15
  export * from './text_button'
16
16
  export * from './text_inline_button'
17
17
  export * from './text'
18
+ export * from './toggle_button'
@@ -1,6 +1,6 @@
1
1
  import { useTheme } from '../../hooks'
2
2
  import React from 'react'
3
- import { Platform, StyleSheet, Text as ReactNativeText } from 'react-native'
3
+ import { StyleSheet, Text as ReactNativeText } from 'react-native'
4
4
  import type { TextStyle, TextProps as ReactNativeTextProps } from 'react-native'
5
5
  import { tokens } from '../../vendor/tapestry/tokens'
6
6
 
@@ -0,0 +1,157 @@
1
+ import React, { useMemo } from 'react'
2
+ import { Pressable, StyleSheet } from 'react-native'
3
+ import type { PressableProps } from 'react-native'
4
+ import { Text } from './text'
5
+ import { useTheme, useFontScale, useCreateAndroidRippleColor } from '../../hooks'
6
+ import { platformFontWeightBold, platformPressedOpacityStyle } from '../../utils'
7
+ import colorFunction from 'color'
8
+ import { Icon, IconString } from './icon'
9
+ import { tokens } from '../../vendor/tapestry/tokens'
10
+
11
+ // =================================
12
+ // ====== Component ================
13
+ // =================================
14
+
15
+ export interface ToggleButtonProps extends PressableProps {
16
+ /**
17
+ * Pressable container styles
18
+ */
19
+ active: boolean
20
+ /**
21
+ * Specifies whether fonts should be scaled down automatically to fit given style constraints.
22
+ */
23
+ adjustsFontSizeToFit?: boolean
24
+ /**
25
+ * Specifies whether fonts should scale to respect the device's text size accessibility settings. The default is true.
26
+ */
27
+ allowFontScaling?: boolean
28
+ /**
29
+ * Generates an icon to the left of the button text
30
+ */
31
+ iconNameLeft?: IconString
32
+ /**
33
+ * Generates an icon to the right of the button text
34
+ */
35
+ iconNameRight?: IconString
36
+ /**
37
+ * Specifies the maximum size a font can reach when allowFontScaling is enabled.
38
+ */
39
+ maxFontSizeMultiplier?: number
40
+ /**
41
+ * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
42
+ */
43
+ minimumFontScale?: number
44
+ /**
45
+ * Renders as text within the button
46
+ */
47
+ title: string
48
+ /**
49
+ * Pressable container styles
50
+ */
51
+ style?: PressableProps['style']
52
+ }
53
+
54
+ export function ToggleButton({
55
+ active,
56
+ adjustsFontSizeToFit = false,
57
+ allowFontScaling = true,
58
+ iconNameLeft,
59
+ iconNameRight,
60
+ maxFontSizeMultiplier,
61
+ minimumFontScale,
62
+ title,
63
+ style,
64
+ ...props
65
+ }: ToggleButtonProps) {
66
+ const styles = useStyles({ active, maxFontSizeMultiplier })
67
+ const { colors } = useTheme()
68
+ const overrideStyles = StyleSheet.flatten(style) // Ensures the pressed styles still get applied
69
+
70
+ const baseRippleColor = active ? colors.interaction : colors.fillColorNeutral050Base
71
+ const androidRippleColor = useCreateAndroidRippleColor({ color: baseRippleColor })
72
+
73
+ return (
74
+ <Pressable
75
+ style={({ pressed }) => ({
76
+ ...styles.pressable,
77
+ ...(pressed ? platformPressedOpacityStyle : null),
78
+ ...overrideStyles,
79
+ })}
80
+ accessibilityRole="togglebutton"
81
+ accessibilityState={{ checked: active }}
82
+ android_ripple={{ color: androidRippleColor, borderless: false, foreground: true }}
83
+ {...props}
84
+ >
85
+ {iconNameLeft && (
86
+ <Icon
87
+ name={iconNameLeft}
88
+ style={styles.icon}
89
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
90
+ />
91
+ )}
92
+ <Text
93
+ allowFontScaling={allowFontScaling}
94
+ minimumFontScale={minimumFontScale}
95
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
96
+ adjustsFontSizeToFit={adjustsFontSizeToFit}
97
+ numberOfLines={1}
98
+ style={styles.text}
99
+ variant="tertiary"
100
+ >
101
+ {title}
102
+ </Text>
103
+ {iconNameRight && (
104
+ <Icon
105
+ name={iconNameRight}
106
+ style={styles.icon}
107
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
108
+ />
109
+ )}
110
+ </Pressable>
111
+ )
112
+ }
113
+
114
+ // =================================
115
+ // ====== Styles ===================
116
+ // =================================
117
+
118
+ const useStyles = ({ active, maxFontSizeMultiplier }: Partial<ToggleButtonProps>) => {
119
+ const { colors, button } = useTheme()
120
+ const fontScale = useFontScale({ maxFontSizeMultiplier })
121
+
122
+ const activeBackgroundColor = useMemo(() => {
123
+ return colorFunction(colors.interaction)
124
+ .hsl()
125
+ .lightness(colors.name === 'dark' ? 12 : 92)
126
+ .string()
127
+ }, [colors.interaction, colors.name])
128
+
129
+ const color = active ? colors.interaction : colors.textColorDefaultSecondary
130
+
131
+ return StyleSheet.create({
132
+ pressable: {
133
+ borderRadius: button.borderRadius * fontScale,
134
+ borderWidth: 1,
135
+ borderColor: active ? colors.interaction : colors.borderColorDefaultBase,
136
+ backgroundColor: active ? activeBackgroundColor : 'transparent',
137
+ paddingHorizontal: 16 * fontScale,
138
+ flexDirection: 'row',
139
+ alignItems: 'center',
140
+ justifyContent: 'center',
141
+ height: 32 * fontScale,
142
+ gap: 6 * fontScale,
143
+ overflow: 'hidden',
144
+ },
145
+ text: {
146
+ textAlign: 'center',
147
+ textAlignVertical: 'center',
148
+ includeFontPadding: false,
149
+ fontWeight: platformFontWeightBold,
150
+ color,
151
+ },
152
+ icon: {
153
+ fontSize: tokens.fontSizeSm * fontScale,
154
+ color,
155
+ },
156
+ })
157
+ }
@@ -89,12 +89,17 @@ function useGradientColorMap(): GradientColorMap {
89
89
  // ====== Functions ================
90
90
  // =================================
91
91
 
92
- interface GetColorKeyArgs {
92
+ interface GetColorKeyArgs<T> {
93
93
  disabled: boolean | null
94
94
  loading?: boolean
95
- appearance: ButtonAppearanceUnion | IconButtonAppearanceUnion
95
+ appearance: T
96
96
  }
97
- function getColorKey({ disabled, loading, appearance }: GetColorKeyArgs) {
97
+
98
+ function getColorKey<T extends ButtonAppearanceUnion | IconButtonAppearanceUnion>({
99
+ disabled,
100
+ loading,
101
+ appearance,
102
+ }: GetColorKeyArgs<T>) {
98
103
  if (disabled || loading) return 'disabled'
99
104
  return appearance
100
105
  }
@@ -168,10 +168,11 @@ interface AvatarGroupProps {
168
168
  sourceUris: string[]
169
169
  }
170
170
 
171
+ type AvatarIndex = 0 | 1 | 2 | 3
171
172
  function AvatarGroup({ sourceUris }: AvatarGroupProps) {
172
173
  const styles = useStyles()
173
174
  const { setAllImagesLoaded } = useAvatarContext()
174
- const [loadingStatus, setLoadingStatus] = useState({
175
+ const [loadingStatus, setLoadingStatus] = useState<Record<AvatarIndex, boolean>>({
175
176
  0: false,
176
177
  1: false,
177
178
  2: false,
@@ -180,7 +181,7 @@ function AvatarGroup({ sourceUris }: AvatarGroupProps) {
180
181
  const displayUris = sourceUris.slice(0, 4)
181
182
  const hasDisplayUris = displayUris.length > 0
182
183
 
183
- const handleImageLoaded = index => {
184
+ const handleImageLoaded = (index: AvatarIndex) => {
184
185
  setLoadingStatus(prev => ({
185
186
  ...prev,
186
187
  [index]: true,
@@ -189,7 +190,8 @@ function AvatarGroup({ sourceUris }: AvatarGroupProps) {
189
190
 
190
191
  useEffect(() => {
191
192
  const allImagesLoaded =
192
- hasDisplayUris && displayUris.every((_, index) => loadingStatus[index] === true)
193
+ hasDisplayUris &&
194
+ displayUris.every((_, index) => loadingStatus[index as AvatarIndex] === true)
193
195
 
194
196
  setAllImagesLoaded(allImagesLoaded)
195
197
  }, [displayUris, hasDisplayUris, loadingStatus, setAllImagesLoaded])
@@ -12,7 +12,7 @@ import {
12
12
  } from '../../utils'
13
13
  import { tokens } from '../../vendor/tapestry/tokens'
14
14
  import { useCreateAndroidRippleColor, useFontScale } from '../../hooks'
15
- import { Icon } from '../display/icon'
15
+ import { Icon, IconString } from '../display/icon'
16
16
  import { Heading } from '../display/heading'
17
17
  import { Text } from '../display/text'
18
18
  import { TextInlineButton } from '../display/text_inline_button'
@@ -161,7 +161,7 @@ BannerContent.displayName = 'Banner.Content'
161
161
  // ========================================
162
162
 
163
163
  interface BannerStatusIconProps {
164
- iconName?: string
164
+ iconName?: IconString
165
165
  }
166
166
 
167
167
  function BannerStatusIcon({ iconName }: BannerStatusIconProps) {
@@ -48,7 +48,7 @@ function useSessionChanged(value: Pick<ChatContextValue, 'token' | 'env'>): bool
48
48
  return Boolean(prevToken && newToken !== prevToken) || Boolean(prevEnv && newEnv !== prevEnv)
49
49
  }
50
50
 
51
- function usePrevious<T>(value) {
51
+ function usePrevious<T>(value: T): T {
52
52
  const ref = useRef<T>(value)
53
53
 
54
54
  useEffect(() => {
@@ -0,0 +1,27 @@
1
+ import React, { createContext, useContext, useState } from 'react'
2
+
3
+ type ConversationActionsValue = {
4
+ activeConversationId?: number
5
+ setActiveConversationId: (_id: number) => void
6
+ }
7
+ const ConversationActionsContext = createContext<ConversationActionsValue>({
8
+ activeConversationId: undefined,
9
+ setActiveConversationId: () => {},
10
+ })
11
+
12
+ export const ConversationActionsProvider = ({ children }: { children: React.ReactNode }) => {
13
+ const [activeConversationId, setActiveConversationId] = useState<number | undefined>()
14
+
15
+ return (
16
+ <ConversationActionsContext.Provider
17
+ value={{
18
+ activeConversationId,
19
+ setActiveConversationId,
20
+ }}
21
+ >
22
+ {children}
23
+ </ConversationActionsContext.Provider>
24
+ )
25
+ }
26
+
27
+ export const useConversationActionsContext = () => useContext(ConversationActionsContext)
@@ -34,7 +34,7 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
34
34
  },
35
35
  })
36
36
 
37
- export const useConversation = ({ conversation_id }) => {
37
+ export const useConversation = ({ conversation_id }: { conversation_id: number }) => {
38
38
  return useSuspenseGet<ConversationResource>(getConversationRequestArgs({ conversation_id }))
39
39
  }
40
40
 
@@ -60,7 +60,7 @@ export const useConversationMute = ({ conversation_id }: { conversation_id: numb
60
60
  mutationFn: async (muted: boolean) => {
61
61
  const action = muted ? 'mute' : 'unmute'
62
62
 
63
- return apiClient.chat.post({
63
+ return apiClient.chat.post<ApiResource<ConversationResource>>({
64
64
  url: `/me/conversations/${conversation_id}/${action}`,
65
65
  data: { data: { type: '', attributes: {} }, fields: { Conversation: 'muted' } },
66
66
  })
@@ -104,12 +104,12 @@ export const useConversationUpdate = ({ conversation_id }: { conversation_id: nu
104
104
  mutationKey: ['mutateConversation'],
105
105
  mutationFn: async (update: Partial<ConversationResource>) => {
106
106
  const postArgs = transformGetToPost(requestArgs).data
107
- return apiClient.chat.patch({
107
+ return apiClient.chat.patch<ApiResource<ConversationResource>>({
108
108
  url: `/me/conversations/${conversation_id}/`,
109
109
  data: { data: { type: '', attributes: update }, ...postArgs },
110
110
  })
111
111
  },
112
- onSuccess: (response: ApiResource<ConversationResource>) => {
112
+ onSuccess: response => {
113
113
  queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, () => response)
114
114
  },
115
115
  })
@@ -153,7 +153,7 @@ export const useConversationDisableReplies = ({ conversation_id }: { conversatio
153
153
  },
154
154
  mutationKey: ['disableRepliesConversation'],
155
155
  mutationFn: async (repliesDisabled: boolean) => {
156
- return apiClient.chat.patch({
156
+ return apiClient.chat.patch<ApiResource<ConversationResource>>({
157
157
  url: `/me/conversations/${conversation_id}`,
158
158
  data: {
159
159
  data: {
@@ -38,7 +38,7 @@ export const useJoltClient = (): JoltClient | undefined => {
38
38
 
39
39
  const fetchSubscribeTokenFn: FetchSubscribeToken = (channel: string, connectionId: string) => {
40
40
  return apiClient.chat
41
- .post({
41
+ .post<ApiResource<JoltResponse>>({
42
42
  url: '/me/jolt_subscribe',
43
43
  data: {
44
44
  data: {
@@ -47,7 +47,7 @@ export const useJoltClient = (): JoltClient | undefined => {
47
47
  },
48
48
  },
49
49
  })
50
- .then((res: ApiResource<JoltResponse>) => res.data.id)
50
+ .then(res => res.data.id)
51
51
  }
52
52
 
53
53
  const { data: joltClient } = useQuery({
@@ -8,7 +8,7 @@ import React from 'react'
8
8
  import { Icon } from '../components'
9
9
  import { ConversationDetailsScreen } from '../screens/conversation_details_screen'
10
10
  import { ConversationScreen } from '../screens/conversation_screen'
11
- import { ConversationsScreen } from '../screens/conversations_screen'
11
+ import { ConversationsScreen } from '../screens/conversations/conversations_screen'
12
12
  import { ConversationCreateScreen } from '../screens/create/conversation_create_screen'
13
13
  import {
14
14
  ConversationFilterReceipientsScreenOptions,
@@ -27,6 +27,7 @@ import {
27
27
  ConversationFiltersScreen,
28
28
  ConversationFiltersScreenOptions,
29
29
  } from '../screens/conversation_filters_screen'
30
+ import { ConversationFiltersParams } from '../screens/conversation_filters/screen_props'
30
31
 
31
32
  export const CreateConversationStack = createNativeStackNavigator({
32
33
  initialRouteName: 'ConversationSelectRecipients',
@@ -85,6 +86,23 @@ export const ChatStack = createNativeStackNavigator({
85
86
  },
86
87
  Conversation: {
87
88
  screen: ConversationScreen,
89
+ options: ({ route, navigation }) => ({
90
+ headerTitle: (route.params as { title?: string })?.title ?? 'Chat',
91
+ headerLeft: props => (
92
+ <HeaderBackButton
93
+ displayMode="minimal"
94
+ onPress={() => {
95
+ if ((route.params as ConversationFiltersParams)?.chat_group_graph_id) {
96
+ // Ensure that conversations with a graph id pass them back to the conversation list
97
+ navigation.popTo('Conversations', route.params)
98
+ } else {
99
+ navigation.goBack()
100
+ }
101
+ }}
102
+ {...props}
103
+ />
104
+ ),
105
+ }),
88
106
  },
89
107
  ConversationDetails: {
90
108
  screen: ConversationDetailsScreen,
@@ -1,5 +1,5 @@
1
1
  import React, { useContext, useMemo } from 'react'
2
- import { FlatList, RefreshControl, StyleSheet, View, ViewStyle } from 'react-native'
2
+ import { FlatList, StyleSheet, View, ViewStyle } from 'react-native'
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
4
  import { Heading } from '../../../components'
5
5
  import { useTheme } from '../../../hooks'
@@ -14,13 +14,22 @@ import { ConversationFilterStackParamList } from '../screen_props'
14
14
  export type FilterProps = { group_source_app_name?: string; isActive: boolean; filter: FilterTypes }
15
15
 
16
16
  export const FilterRow = ({ group_source_app_name, isActive, filter }: FilterProps) => {
17
- const styles = useRowStyles()
17
+ const styles = useRowStyles({ isActive })
18
18
  const { setAppFilter } = useContext(FilterContext)
19
19
 
20
20
  return (
21
- <PressableRow style={styles.row} onPress={() => setAppFilter({ group_source_app_name })}>
21
+ <PressableRow
22
+ style={styles.row}
23
+ onPress={() => setAppFilter({ group_source_app_name })}
24
+ isActive={isActive}
25
+ >
22
26
  <Text>{filter}</Text>
23
- {isActive ? <Icon name="general.check" size={16} style={styles.rowIconRight} /> : null}
27
+ <Icon
28
+ name="general.check"
29
+ size={16}
30
+ style={styles.rowIconRight}
31
+ accessibilityElementsHidden
32
+ />
24
33
  </PressableRow>
25
34
  )
26
35
  }
@@ -31,7 +40,7 @@ export type GroupRowProps = {
31
40
  }
32
41
 
33
42
  export const GroupRow = ({ group, isActive }: GroupRowProps) => {
34
- const styles = useRowStyles()
43
+ const styles = useRowStyles({ isActive })
35
44
  const { setGroupFilter } = useContext(FilterContext)
36
45
 
37
46
  const handleFilterByGroup = () => {
@@ -41,7 +50,7 @@ export const GroupRow = ({ group, isActive }: GroupRowProps) => {
41
50
  const { headerImage, membershipsCount } = group
42
51
 
43
52
  return (
44
- <PressableRow style={styles.row} onPress={handleFilterByGroup}>
53
+ <PressableRow style={styles.row} onPress={handleFilterByGroup} isActive={isActive}>
45
54
  {headerImage?.thumbnail && (
46
55
  <Image
47
56
  source={{ uri: headerImage?.thumbnail }}
@@ -50,13 +59,18 @@ export const GroupRow = ({ group, isActive }: GroupRowProps) => {
50
59
  alt={`Image for ${group.name}`}
51
60
  />
52
61
  )}
53
- <View>
62
+ <View style={styles.rowContent}>
54
63
  <Heading variant="h3" style={styles.rowTitle}>
55
64
  {group.name}
56
65
  </Heading>
57
66
  {membershipsCount && <Text>{group.membershipsCount} members</Text>}
58
67
  </View>
59
- {isActive ? <Icon name="general.check" size={16} style={styles.rowIconRight} /> : null}
68
+ <Icon
69
+ name="general.check"
70
+ size={16}
71
+ style={styles.rowIconRight}
72
+ accessibilityElementsHidden
73
+ />
60
74
  </PressableRow>
61
75
  )
62
76
  }
@@ -67,12 +81,12 @@ export type TeamRowProps = {
67
81
  }
68
82
 
69
83
  export const TeamRow = ({ team }: TeamRowProps) => {
70
- const styles = useRowStyles()
71
84
  const { setGroupFilter } = useContext(FilterContext)
72
85
  const servicesTeams = useServicesTeamsMap()
73
86
  const { params } = useContext(FilterContext)
74
87
  const { chat_group_graph_id } = params
75
88
  const isActive = chat_group_graph_id === team.id.toString()
89
+ const styles = useRowStyles({ isActive })
76
90
 
77
91
  const handleFilterByGroup = () => {
78
92
  setGroupFilter({ chat_group_graph_id: team.id })
@@ -82,14 +96,19 @@ export const TeamRow = ({ team }: TeamRowProps) => {
82
96
  const serviceTypeName = servicesTeam?.serviceTypeName || servicesTeam?.group
83
97
 
84
98
  return (
85
- <PressableRow style={styles.row} onPress={handleFilterByGroup}>
86
- <View>
99
+ <PressableRow style={styles.row} onPress={handleFilterByGroup} isActive={isActive}>
100
+ <View style={styles.rowContent}>
87
101
  <Heading variant="h3" style={styles.rowTitle}>
88
102
  {team.name}
89
103
  </Heading>
90
104
  <Text>{serviceTypeName}</Text>
91
105
  </View>
92
- {isActive ? <Icon name="general.check" size={16} style={styles.rowIconRight} /> : null}
106
+ <Icon
107
+ name="general.check"
108
+ size={16}
109
+ style={styles.rowIconRight}
110
+ accessibilityElementsHidden
111
+ />
93
112
  </PressableRow>
94
113
  )
95
114
  }
@@ -109,12 +128,18 @@ export const ViewMore = ({ routeName }: ViewMoreRowProps) => {
109
128
 
110
129
  export const PressableRow = ({
111
130
  children,
131
+ isActive,
112
132
  onPress,
113
133
  style,
114
- }: PropsWithChildren<{ onPress: () => void; style?: ViewStyle }>) => {
134
+ }: PropsWithChildren<{ isActive: boolean; onPress: () => void; style?: ViewStyle }>) => {
115
135
  const styles = useRowStyles()
116
136
  return (
117
- <PlatformPressable style={styles.container} onPress={onPress}>
137
+ <PlatformPressable
138
+ style={styles.container}
139
+ onPress={onPress}
140
+ accessibilityRole="radio"
141
+ accessibilityState={{ selected: isActive }}
142
+ >
118
143
  <View style={[styles.innerContainer, style]}>{children}</View>
119
144
  </PlatformPressable>
120
145
  )
@@ -124,7 +149,7 @@ const ASPECT_RATIO = 16 / 9
124
149
  const THUMBNAIL_WIDTH = 80
125
150
  const THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH / ASPECT_RATIO
126
151
 
127
- const useRowStyles = () => {
152
+ const useRowStyles = ({ isActive = false }: { isActive?: boolean } = {}) => {
128
153
  const theme = useTheme()
129
154
  return StyleSheet.create({
130
155
  container: {
@@ -153,12 +178,17 @@ const useRowStyles = () => {
153
178
  height: THUMBNAIL_HEIGHT,
154
179
  borderRadius: 4,
155
180
  },
181
+ rowContent: {
182
+ flexShrink: 1,
183
+ },
156
184
  rowIconRight: {
157
185
  marginLeft: 'auto',
158
186
  color: theme.colors.statusSuccessIcon,
187
+ opacity: isActive ? 1 : 0,
159
188
  },
160
189
  rowTitle: {
161
190
  fontSize: 16,
191
+ flexShrink: 1,
162
192
  },
163
193
  })
164
194
  }
@@ -0,0 +1,47 @@
1
+ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
2
+ import React, { useCallback } from 'react'
3
+ import { StyleSheet, View } from 'react-native'
4
+ import { Badge } from '../../../components'
5
+ import { useApiGet } from '../../../hooks/use_api'
6
+ import { GroupResource } from '../../../types/resources/group_resource'
7
+ import { ConversationScreenProps } from '../conversations_screen'
8
+
9
+ export const ChatGroupBadge = () => {
10
+ const styles = useStyles()
11
+ const navigation = useNavigation()
12
+ const route = useRoute<RouteProp<ConversationScreenProps['route']>>()
13
+ const { chat_group_graph_id } = route.params || {}
14
+ const { data: group } = useApiGet<GroupResource>({
15
+ url: `/me/groups/${chat_group_graph_id}`,
16
+ data: {
17
+ fields: {
18
+ Group: [],
19
+ },
20
+ },
21
+ enabled: !!chat_group_graph_id,
22
+ app: 'chat',
23
+ })
24
+
25
+ const handleBadgePress = useCallback(() => {
26
+ navigation.setParams({
27
+ chat_group_graph_id: undefined,
28
+ group_source_app_name: undefined,
29
+ })
30
+ }, [navigation])
31
+
32
+ if (!group) return null
33
+
34
+ return (
35
+ <View style={styles.row}>
36
+ <Badge iconNameRight="general.x" label={group?.name || ''} onPress={handleBadgePress} />
37
+ </View>
38
+ )
39
+ }
40
+
41
+ const useStyles = () => {
42
+ return StyleSheet.create({
43
+ row: {
44
+ flexDirection: 'row',
45
+ },
46
+ })
47
+ }