@planningcenter/chat-react-native 1.5.0 → 1.6.0

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 (206) hide show
  1. package/build/components/conversations.d.ts.map +1 -1
  2. package/build/components/conversations.js +29 -8
  3. package/build/components/conversations.js.map +1 -1
  4. package/build/components/display/button.d.ts +71 -0
  5. package/build/components/display/button.d.ts.map +1 -0
  6. package/build/components/display/button.js +136 -0
  7. package/build/components/display/button.js.map +1 -0
  8. package/build/components/display/button_color_utils.d.ts +24 -0
  9. package/build/components/display/button_color_utils.d.ts.map +1 -0
  10. package/build/components/display/button_color_utils.js +43 -0
  11. package/build/components/display/button_color_utils.js.map +1 -0
  12. package/build/components/display/heading.d.ts +4 -0
  13. package/build/components/display/heading.d.ts.map +1 -1
  14. package/build/components/display/heading.js +3 -0
  15. package/build/components/display/heading.js.map +1 -1
  16. package/build/components/display/icon.d.ts +8 -4
  17. package/build/components/display/icon.d.ts.map +1 -1
  18. package/build/components/display/icon.js +21 -13
  19. package/build/components/display/icon.js.map +1 -1
  20. package/build/components/display/image.d.ts +7 -2
  21. package/build/components/display/image.d.ts.map +1 -1
  22. package/build/components/display/image.js +5 -5
  23. package/build/components/display/image.js.map +1 -1
  24. package/build/components/display/index.d.ts +10 -7
  25. package/build/components/display/index.d.ts.map +1 -1
  26. package/build/components/display/index.js +10 -7
  27. package/build/components/display/index.js.map +1 -1
  28. package/build/components/display/spinner.d.ts +5 -1
  29. package/build/components/display/spinner.d.ts.map +1 -1
  30. package/build/components/display/spinner.js +19 -13
  31. package/build/components/display/spinner.js.map +1 -1
  32. package/build/components/display/text.d.ts +13 -3
  33. package/build/components/display/text.d.ts.map +1 -1
  34. package/build/components/display/text.js +17 -5
  35. package/build/components/display/text.js.map +1 -1
  36. package/build/components/display/text_button.d.ts +37 -0
  37. package/build/components/display/text_button.d.ts.map +1 -0
  38. package/build/components/display/text_button.js +36 -0
  39. package/build/components/display/text_button.js.map +1 -0
  40. package/build/components/display/text_inline_button.d.ts +12 -0
  41. package/build/components/display/text_inline_button.d.ts.map +1 -0
  42. package/build/components/display/text_inline_button.js +53 -0
  43. package/build/components/display/text_inline_button.js.map +1 -0
  44. package/build/components/index.d.ts +1 -0
  45. package/build/components/index.d.ts.map +1 -1
  46. package/build/components/index.js +1 -0
  47. package/build/components/index.js.map +1 -1
  48. package/build/components/primitive/avatar_primitive.d.ts +1 -1
  49. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  50. package/build/components/primitive/avatar_primitive.js +6 -9
  51. package/build/components/primitive/avatar_primitive.js.map +1 -1
  52. package/build/contexts/api_provider.d.ts +4 -6
  53. package/build/contexts/api_provider.d.ts.map +1 -1
  54. package/build/contexts/api_provider.js +13 -20
  55. package/build/contexts/api_provider.js.map +1 -1
  56. package/build/contexts/chat_context.d.ts +7 -5
  57. package/build/contexts/chat_context.d.ts.map +1 -1
  58. package/build/contexts/chat_context.js +40 -4
  59. package/build/contexts/chat_context.js.map +1 -1
  60. package/build/hooks/index.d.ts +4 -0
  61. package/build/hooks/index.d.ts.map +1 -1
  62. package/build/hooks/index.js +4 -0
  63. package/build/hooks/index.js.map +1 -1
  64. package/build/hooks/use_create_android_ripple_color.d.ts +4 -0
  65. package/build/hooks/use_create_android_ripple_color.d.ts.map +1 -0
  66. package/build/hooks/use_create_android_ripple_color.js +15 -0
  67. package/build/hooks/use_create_android_ripple_color.js.map +1 -0
  68. package/build/hooks/use_current_person.d.ts +3 -0
  69. package/build/hooks/use_current_person.d.ts.map +1 -0
  70. package/build/hooks/use_current_person.js +13 -0
  71. package/build/hooks/use_current_person.js.map +1 -0
  72. package/build/hooks/use_font_scale.d.ts +4 -0
  73. package/build/hooks/use_font_scale.d.ts.map +1 -0
  74. package/build/hooks/use_font_scale.js +8 -0
  75. package/build/hooks/use_font_scale.js.map +1 -0
  76. package/build/hooks/use_suspense_api.d.ts +61 -0
  77. package/build/hooks/use_suspense_api.d.ts.map +1 -0
  78. package/build/hooks/use_suspense_api.js +39 -0
  79. package/build/hooks/use_suspense_api.js.map +1 -0
  80. package/build/navigation/index.d.ts +1 -0
  81. package/build/navigation/index.d.ts.map +1 -1
  82. package/build/navigation/index.js +7 -4
  83. package/build/navigation/index.js.map +1 -1
  84. package/build/screens/conversation_screen.d.ts.map +1 -1
  85. package/build/screens/conversation_screen.js +59 -6
  86. package/build/screens/conversation_screen.js.map +1 -1
  87. package/build/screens/display.d.ts.map +1 -1
  88. package/build/screens/display.js +277 -51
  89. package/build/screens/display.js.map +1 -1
  90. package/build/screens/not_found.js +1 -1
  91. package/build/screens/not_found.js.map +1 -1
  92. package/build/types/api_primitives.d.ts +23 -0
  93. package/build/types/api_primitives.d.ts.map +1 -0
  94. package/build/types/api_primitives.js +2 -0
  95. package/build/types/api_primitives.js.map +1 -0
  96. package/build/types/index.d.ts +4 -0
  97. package/build/types/index.d.ts.map +1 -0
  98. package/build/types/index.js +4 -0
  99. package/build/types/index.js.map +1 -0
  100. package/build/types/resources/conversation.d.ts +15 -0
  101. package/build/types/resources/conversation.d.ts.map +1 -0
  102. package/build/types/resources/conversation.js +2 -0
  103. package/build/types/resources/conversation.js.map +1 -0
  104. package/build/types/resources/index.d.ts +5 -0
  105. package/build/types/resources/index.d.ts.map +1 -0
  106. package/build/types/resources/index.js +5 -0
  107. package/build/types/resources/index.js.map +1 -0
  108. package/build/types/resources/message.d.ts +16 -0
  109. package/build/types/resources/message.d.ts.map +1 -0
  110. package/build/types/resources/message.js +2 -0
  111. package/build/types/resources/message.js.map +1 -0
  112. package/build/types/resources/oauth_token.d.ts +9 -0
  113. package/build/types/resources/oauth_token.d.ts.map +1 -0
  114. package/build/types/resources/oauth_token.js +2 -0
  115. package/build/types/resources/oauth_token.js.map +1 -0
  116. package/build/types/resources/person.d.ts +9 -0
  117. package/build/types/resources/person.d.ts.map +1 -0
  118. package/build/types/resources/person.js +2 -0
  119. package/build/types/resources/person.js.map +1 -0
  120. package/build/types/resources/reaction.d.ts +10 -0
  121. package/build/types/resources/reaction.d.ts.map +1 -0
  122. package/build/types/resources/reaction.js +2 -0
  123. package/build/types/resources/reaction.js.map +1 -0
  124. package/build/types/utils/index.d.ts +4 -0
  125. package/build/types/utils/index.d.ts.map +1 -0
  126. package/build/types/utils/index.js +4 -0
  127. package/build/types/utils/index.js.map +1 -0
  128. package/build/utils/client/client.d.ts +21 -12
  129. package/build/utils/client/client.d.ts.map +1 -1
  130. package/build/utils/client/client.js +24 -22
  131. package/build/utils/client/client.js.map +1 -1
  132. package/build/utils/index.d.ts +1 -0
  133. package/build/utils/index.d.ts.map +1 -1
  134. package/build/utils/index.js +1 -0
  135. package/build/utils/index.js.map +1 -1
  136. package/build/utils/session.d.ts +0 -5
  137. package/build/utils/session.d.ts.map +1 -1
  138. package/build/utils/session.js +0 -10
  139. package/build/utils/session.js.map +1 -1
  140. package/build/utils/styles.d.ts +5 -0
  141. package/build/utils/styles.d.ts.map +1 -1
  142. package/build/utils/styles.js +9 -0
  143. package/build/utils/styles.js.map +1 -1
  144. package/build/utils/theme.d.ts +3 -1
  145. package/build/utils/theme.d.ts.map +1 -1
  146. package/build/utils/theme.js +6 -2
  147. package/build/utils/theme.js.map +1 -1
  148. package/build/vendor/tapestry/alias_tokens_color_map.d.ts +8 -0
  149. package/build/vendor/tapestry/alias_tokens_color_map.d.ts.map +1 -1
  150. package/build/vendor/tapestry/alias_tokens_color_map.js +8 -0
  151. package/build/vendor/tapestry/alias_tokens_color_map.js.map +1 -1
  152. package/build/vendor/tapestry/tokens.d.ts +21 -0
  153. package/build/vendor/tapestry/tokens.d.ts.map +1 -1
  154. package/build/vendor/tapestry/tokens.js +21 -0
  155. package/build/vendor/tapestry/tokens.js.map +1 -1
  156. package/package.json +4 -3
  157. package/src/__tests__/client.ts +72 -19
  158. package/src/__tests__/session.ts +0 -11
  159. package/src/__utils__/handlers.ts +1 -1
  160. package/src/components/conversations.tsx +33 -11
  161. package/src/components/display/button.tsx +293 -0
  162. package/src/components/display/button_color_utils.ts +72 -0
  163. package/src/components/display/heading.tsx +12 -0
  164. package/src/components/display/icon.tsx +35 -16
  165. package/src/components/display/image.tsx +29 -7
  166. package/src/components/display/index.ts +10 -7
  167. package/src/components/display/spinner.tsx +42 -13
  168. package/src/components/display/text.tsx +34 -13
  169. package/src/components/display/text_button.tsx +102 -0
  170. package/src/components/display/text_inline_button.tsx +91 -0
  171. package/src/components/index.tsx +1 -0
  172. package/src/components/primitive/avatar_primitive.tsx +12 -6
  173. package/src/contexts/api_provider.tsx +18 -26
  174. package/src/contexts/chat_context.tsx +61 -7
  175. package/src/hooks/index.ts +4 -0
  176. package/src/hooks/use_create_android_ripple_color.ts +18 -0
  177. package/src/hooks/use_current_person.ts +15 -0
  178. package/src/hooks/use_font_scale.ts +9 -0
  179. package/src/hooks/use_suspense_api.ts +60 -0
  180. package/src/navigation/index.tsx +14 -4
  181. package/src/screens/conversation_screen.tsx +83 -7
  182. package/src/screens/display.tsx +447 -51
  183. package/src/screens/not_found.tsx +1 -1
  184. package/src/types/api_primitives.ts +24 -0
  185. package/src/types/index.ts +3 -0
  186. package/src/types/resources/conversation.ts +15 -0
  187. package/src/types/resources/index.ts +4 -0
  188. package/src/types/resources/message.ts +18 -0
  189. package/src/types/resources/oauth_token.ts +8 -0
  190. package/src/types/resources/person.ts +9 -0
  191. package/src/types/resources/reaction.ts +9 -0
  192. package/src/types/utils/index.ts +6 -0
  193. package/src/utils/client/client.ts +41 -34
  194. package/src/utils/client/types.d.ts +2 -0
  195. package/src/utils/index.ts +1 -0
  196. package/src/utils/session.ts +0 -13
  197. package/src/utils/styles.ts +12 -0
  198. package/src/utils/theme.ts +9 -3
  199. package/src/vendor/tapestry/alias_tokens_color_map.ts +12 -0
  200. package/src/vendor/tapestry/tokens.ts +21 -0
  201. package/build/utils/api.d.ts +0 -9
  202. package/build/utils/api.d.ts.map +0 -1
  203. package/build/utils/api.js +0 -36
  204. package/build/utils/api.js.map +0 -1
  205. package/src/types.d.ts +0 -35
  206. package/src/utils/api.ts +0 -47
@@ -1,24 +1,41 @@
1
1
  import { useTheme } from '../../hooks'
2
2
  import React from 'react'
3
- import {
4
- Platform,
5
- StyleSheet,
6
- Text as ReactNativeText,
7
- TextProps as ReactNativeTextProps,
8
- } from 'react-native'
3
+ import { Platform, StyleSheet, Text as ReactNativeText } from 'react-native'
4
+ import type { TextStyle, TextProps as ReactNativeTextProps } from 'react-native'
9
5
  import { tokens } from '../../vendor/tapestry/tokens'
10
6
 
11
- interface TextProps extends ReactNativeTextProps {
12
- variant?: 'plain' | 'secondary' | 'tertiary' | 'footnote'
7
+ // =================================
8
+ // ====== Constants ================
9
+ // =================================
10
+
11
+ const VARIANTS = {
12
+ plain: 'plain',
13
+ secondary: 'secondary',
14
+ tertiary: 'tertiary',
15
+ footnote: 'footnote',
16
+ } as const
17
+
18
+ type VariantUnion = (typeof VARIANTS)[keyof typeof VARIANTS]
19
+ type VariantStyles = Record<VariantUnion, TextStyle>
20
+
21
+ // =================================
22
+ // ====== Component ================
23
+ // =================================
24
+
25
+ export interface TextProps extends ReactNativeTextProps {
26
+ /**
27
+ * Changes the styles and size of the text.
28
+ */
29
+ variant?: VariantUnion
13
30
  }
14
31
 
15
32
  export function Text({ style, variant = 'plain', children, ...props }: TextProps) {
16
33
  const styles = useStyles()
17
- const variantStyleMap = {
18
- plain: styles.plain,
19
- secondary: styles.secondary,
20
- tertiary: styles.tertiary,
21
- footnote: styles.footnote,
34
+ const variantStyleMap: VariantStyles = {
35
+ [VARIANTS.plain]: styles.plain,
36
+ [VARIANTS.secondary]: styles.secondary,
37
+ [VARIANTS.tertiary]: styles.tertiary,
38
+ [VARIANTS.footnote]: styles.footnote,
22
39
  }
23
40
 
24
41
  return (
@@ -28,6 +45,10 @@ export function Text({ style, variant = 'plain', children, ...props }: TextProps
28
45
  )
29
46
  }
30
47
 
48
+ // =================================
49
+ // ====== Styles ===================
50
+ // =================================
51
+
31
52
  const useStyles = () => {
32
53
  const { colors } = useTheme()
33
54
  return StyleSheet.create({
@@ -0,0 +1,102 @@
1
+ import React from 'react'
2
+ import { Pressable, StyleSheet } from 'react-native'
3
+ import type { PressableProps } from 'react-native'
4
+ import { useTheme } from '../../hooks'
5
+ import { platformFontWeightMedium } from '../../utils'
6
+ import { Text } from './text'
7
+ import type { TextProps } from './text'
8
+ import { getColorKey, useColorOptionMap } from './button_color_utils'
9
+ import type { AppearanceUnion } from './button_color_utils'
10
+
11
+ // =================================
12
+ // ====== Component ================
13
+ // =================================
14
+
15
+ interface TextButtonProps extends PressableProps {
16
+ /**
17
+ * Specifies whether fonts should be scaled down automatically to fit given style constraints.
18
+ */
19
+ adjustsFontSizeToFit?: boolean
20
+ /**
21
+ * Specifies whether fonts should scale to respect the device's text size accessibility settings. The default is true.
22
+ */
23
+ allowFontScaling?: boolean
24
+ /**
25
+ * Updates the button's colors
26
+ */
27
+ appearance?: AppearanceUnion
28
+ /**
29
+ * Renders text within a `Text`
30
+ */
31
+ children?: TextProps['children']
32
+ /**
33
+ * Specifies the maximum size a font can reach when allowFontScaling is enabled.
34
+ */
35
+ maxFontSizeMultiplier?: number
36
+ /**
37
+ * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
38
+ */
39
+ minimumFontScale?: number
40
+ /**
41
+ * Fill buttons are visually more propmonitent then outline buttons
42
+ */
43
+ variant?: TextProps['variant']
44
+ }
45
+
46
+ export function TextButton({
47
+ adjustsFontSizeToFit = false,
48
+ allowFontScaling = true,
49
+ appearance = 'primary',
50
+ disabled = false,
51
+ children,
52
+ maxFontSizeMultiplier,
53
+ minimumFontScale,
54
+ variant = 'plain',
55
+ ...props
56
+ }: TextButtonProps) {
57
+ const styles = useStyles({ appearance, disabled })
58
+
59
+ return (
60
+ <Pressable
61
+ accessibilityRole="button"
62
+ disabled={disabled}
63
+ style={({ pressed }) => pressed && styles.pressed} // Android & iOS intentionally get the same pressed style
64
+ {...props}
65
+ >
66
+ <Text
67
+ adjustsFontSizeToFit={adjustsFontSizeToFit}
68
+ allowFontScaling={allowFontScaling}
69
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
70
+ minimumFontScale={minimumFontScale}
71
+ numberOfLines={1}
72
+ style={[styles.text, disabled && styles.disabled]}
73
+ variant={variant}
74
+ >
75
+ {children}
76
+ </Text>
77
+ </Pressable>
78
+ )
79
+ }
80
+
81
+ // =================================
82
+ // ====== Styles ===================
83
+ // =================================
84
+
85
+ const useStyles = ({ appearance = 'primary', disabled = false }: Partial<TextButtonProps>) => {
86
+ const { colors } = useTheme()
87
+ const colorKey = getColorKey({ disabled, appearance })
88
+ const colorOptionMap = useColorOptionMap()
89
+
90
+ return StyleSheet.create({
91
+ text: {
92
+ fontWeight: platformFontWeightMedium,
93
+ color: colorOptionMap[colorKey],
94
+ },
95
+ disabled: {
96
+ color: colors.textColorDefaultDisabled,
97
+ },
98
+ pressed: {
99
+ opacity: 0.5,
100
+ },
101
+ })
102
+ }
@@ -0,0 +1,91 @@
1
+ import React, { useState } from 'react'
2
+ import { Platform, StyleSheet } from 'react-native'
3
+ import { useTheme } from '../../hooks'
4
+ import { Text } from './text'
5
+ import type { TextProps } from './text'
6
+ import { platformFontWeightMedium } from '../../utils'
7
+ import colorFunction from 'color'
8
+ import { getColorKey, useColorOptionMap } from './button_color_utils'
9
+ import type { AppearanceUnion } from './button_color_utils'
10
+
11
+ // =================================
12
+ // ====== Component ================
13
+ // =================================
14
+
15
+ interface TextInlineButtonProps extends TextProps {
16
+ /**
17
+ * Updates the button's colors
18
+ */
19
+ appearance?: AppearanceUnion
20
+ }
21
+
22
+ export function TextInlineButton({
23
+ appearance = 'primary',
24
+ children,
25
+ disabled = false,
26
+ onPress,
27
+ ...props
28
+ }: TextInlineButtonProps) {
29
+ const styles = useStyles({ appearance, disabled })
30
+ const [isPressed, setIsPressed] = useState(false)
31
+
32
+ const platformPressedStyle = Platform.select({
33
+ ios: null,
34
+ android: styles.pressedAndroid,
35
+ })
36
+
37
+ return (
38
+ <Text
39
+ accessibilityRole="button"
40
+ numberOfLines={1}
41
+ style={[
42
+ styles.text,
43
+ styles.underline,
44
+ disabled && styles.disabled,
45
+ isPressed && platformPressedStyle,
46
+ ]}
47
+ onPress={!disabled ? onPress : undefined}
48
+ onPressIn={() => setIsPressed(true)}
49
+ onPressOut={() => setIsPressed(false)}
50
+ {...props}
51
+ >
52
+ {children}
53
+ </Text>
54
+ )
55
+ }
56
+
57
+ // =================================
58
+ // ====== Styles ===================
59
+ // =================================
60
+
61
+ const useStyles = ({
62
+ appearance = 'primary',
63
+ disabled = false,
64
+ }: Partial<TextInlineButtonProps>) => {
65
+ const { colors } = useTheme()
66
+ const colorKey = getColorKey({ disabled, appearance })
67
+ const colorOptionMap = useColorOptionMap()
68
+
69
+ // Android doesn't show a "pressed" state and so we need to simulate it.
70
+ // Lowering the Text's opacity doesn't work as it inherits it's parent's Text's opacity.
71
+ // We can simulate the effect by changing the color's opacity instead.
72
+ const pressedColor = colorFunction(colorOptionMap[colorKey]).alpha(0.5).string()
73
+
74
+ return StyleSheet.create({
75
+ text: {
76
+ fontWeight: platformFontWeightMedium,
77
+ color: colorOptionMap[colorKey],
78
+ },
79
+ underline: {
80
+ textDecorationLine: 'underline',
81
+ textDecorationColor: colorOptionMap[colorKey],
82
+ textDecorationStyle: 'solid',
83
+ },
84
+ disabled: {
85
+ color: colors.textColorDefaultDisabled,
86
+ },
87
+ pressedAndroid: {
88
+ color: pressedColor,
89
+ },
90
+ })
91
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './conversations'
2
2
  export * from './error_boundary'
3
+ export * from './display'
@@ -94,7 +94,7 @@ interface AvatarRootProps {
94
94
 
95
95
  function AvatarRoot({ children, size = 'md' }: AvatarRootProps) {
96
96
  const [allImagesLoaded, setAllImagesLoaded] = useState(false)
97
- const styles = useStyles(size)
97
+ const styles = useStyles({ size })
98
98
 
99
99
  return (
100
100
  <AvatarContext.Provider value={{ size, allImagesLoaded, setAllImagesLoaded }}>
@@ -127,14 +127,14 @@ AvatarMask.displayName = 'Avatar.Mask'
127
127
  // ====== AvatarImage ============
128
128
  // =================================
129
129
 
130
- interface AvatarImageProps extends Omit<ImageProps, 'source'> {
130
+ interface AvatarImageProps extends Omit<ImageProps, 'source' | 'alt'> {
131
131
  sourceUri: string
132
132
  }
133
133
 
134
134
  function AvatarImage({ sourceUri, ...props }: AvatarImageProps) {
135
135
  const { size } = useAvatarContext()
136
136
 
137
- return <Image source={{ uri: sourceUri }} loaderSize={AVATAR_PX[size]} {...props} />
137
+ return <Image source={{ uri: sourceUri }} loaderSize={AVATAR_PX[size]} {...props} alt="" />
138
138
  }
139
139
 
140
140
  AvatarImage.displayName = 'Avatar.Image'
@@ -152,6 +152,7 @@ function AvatarGroupImage({ sourceUri, style, onLoad }: AvatarGroupImageProps) {
152
152
  loadingEnabled={false}
153
153
  wrapperStyle={style}
154
154
  onLoad={onLoad}
155
+ alt=""
155
156
  />
156
157
  )
157
158
  }
@@ -279,7 +280,7 @@ AvatarGroup.displayName = 'Avatar.Group'
279
280
 
280
281
  function AvatarGroupLoader() {
281
282
  const { size, allImagesLoaded } = useAvatarContext()
282
- const styles = useStyles(size)
283
+ const styles = useStyles({ size })
283
284
 
284
285
  if (allImagesLoaded) return null
285
286
 
@@ -302,7 +303,7 @@ interface AvatarPresenceProps extends ViewProps {
302
303
 
303
304
  function AvatarPresence({ presence, ...props }: AvatarPresenceProps) {
304
305
  const { size } = useAvatarContext()
305
- const styles = useStyles(size, presence)
306
+ const styles = useStyles({ size, presence })
306
307
 
307
308
  return <View style={styles.presence} {...props} />
308
309
  }
@@ -313,7 +314,12 @@ AvatarPresence.displayName = 'Avatar.Presence'
313
314
  // ====== Styles ===================
314
315
  // =================================
315
316
 
316
- const useStyles = (size: AvatarSize = 'md', presence: AvatarPresenceType = 'offline') => {
317
+ interface Styles {
318
+ size?: AvatarSize
319
+ presence?: AvatarPresenceType
320
+ }
321
+
322
+ const useStyles = ({ size = 'md', presence = 'offline' }: Styles = {}) => {
317
323
  const { colors } = useTheme()
318
324
  const PRESENCE_COLOR = {
319
325
  online: colors.fillColorInteractionOnlineDefault,
@@ -1,53 +1,45 @@
1
- import { JSONAPIResponse } from '@planningcenter/chat-core'
2
1
  import { QueryClient, QueryClientProvider, QueryKey } from '@tanstack/react-query'
3
2
  import React, { useEffect } from 'react'
4
3
  import { ViewProps } from 'react-native'
5
- import { OAuthToken } from '../types'
6
- import apiRequest from '../utils/api'
7
- import { ENV, session } from '../utils/session'
8
- import Uri from '../utils/uri'
4
+ import { Client } from '../utils'
5
+ import { GetRequest } from '../utils/client/types'
9
6
 
10
- let handleTokenExpired: () => void
7
+ let apiClient: Client | undefined
11
8
 
12
9
  const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
13
- if (!session.token) {
10
+ if (!apiClient) {
14
11
  throw new Error('No token present')
15
12
  }
16
13
 
17
- const uri = new Uri({ session })
18
- const url = uri.api(queryKey[0])
19
-
20
- return apiRequest<JSONAPIResponse>(url)
21
- .then(r => r.json)
22
- .catch(error => {
23
- if (error.message === 'Token expired') {
24
- handleTokenExpired()
25
- }
26
- return null
27
- })
14
+ const data = queryKey[0] as GetRequest
15
+
16
+ return apiClient.get(data)
28
17
  }
29
18
 
30
19
  export const queryClient = new QueryClient({
31
20
  defaultOptions: {
32
21
  queries: {
33
22
  queryFn: defaultQueryFn,
23
+ retry: 3,
34
24
  },
35
25
  },
36
26
  })
37
27
 
38
28
  export function ApiProvider({
39
29
  children,
40
- env = 'production',
41
- token,
42
- onTokenExpired,
43
- }: ViewProps & { env?: ENV; token?: OAuthToken; onTokenExpired: () => void }) {
44
- session.env = env
45
- session.token = token
46
- handleTokenExpired = onTokenExpired
30
+ client,
31
+ sessionChanged,
32
+ }: ViewProps & { client: Client; sessionChanged: boolean }) {
33
+ useEffect(() => {
34
+ apiClient = client
35
+ }, [client])
47
36
 
37
+ // TODO: CREATE CALLBACK TO INVALIDATE QUERIES
48
38
  useEffect(() => {
39
+ if (!sessionChanged) return
40
+
49
41
  queryClient.invalidateQueries()
50
- }, [env, token])
42
+ }, [sessionChanged])
51
43
 
52
44
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
53
45
  }
@@ -1,39 +1,93 @@
1
1
  import { merge } from 'lodash'
2
- import React, { createContext, useMemo } from 'react'
2
+ import React, { createContext, useEffect, useMemo, useRef } from 'react'
3
3
  import { ColorSchemeName, useColorScheme } from 'react-native'
4
4
  import { DeepPartial, OAuthToken } from '../types'
5
5
  import { defaultTheme, DefaultTheme } from '../utils/theme'
6
6
  import { aliasTokensColorMap } from '../vendor/tapestry/alias_tokens_color_map'
7
7
  import { ApiProvider } from './api_provider'
8
+ import { Client, ENV, Session } from '../utils'
8
9
 
9
- type ContextValue = {
10
+ type ChatContextValue = {
10
11
  token?: OAuthToken
11
12
  onTokenExpired: () => void
12
13
  theme: any
13
- env?: 'production' | 'staging' | 'development'
14
+ env?: ENV
15
+ client: Client
14
16
  }
15
17
 
16
- export const ChatContext = createContext<ContextValue>({
18
+ const defaultChatClient = new Client({
19
+ app: 'chat',
20
+ session: new Session(),
21
+ version: '2018-11-01',
22
+ onTokenExpired: () => null,
23
+ })
24
+
25
+ export const ChatContext = createContext<ChatContextValue>({
17
26
  theme: undefined,
18
27
  token: undefined,
19
28
  env: undefined,
20
29
  onTokenExpired: () => {},
30
+ client: defaultChatClient,
21
31
  })
22
32
 
23
- export function ChatProvider({ children, value }: { children: any; value: ContextValue }) {
33
+ export function ChatProvider({
34
+ children,
35
+ value,
36
+ }: {
37
+ children: any
38
+ value: Omit<ChatContextValue, 'client'>
39
+ }) {
40
+ const { env, token, onTokenExpired } = value
24
41
  const theme = useCreateChatTheme(value.theme)
42
+ const session = useMemo(() => new Session({ token, env }), [env, token])
43
+ const client = useMemo(
44
+ () =>
45
+ new Client({
46
+ app: 'chat',
47
+ session,
48
+ version: '2018-11-01',
49
+ onTokenExpired,
50
+ }),
51
+ [onTokenExpired, session]
52
+ )
53
+ const sessionChanged = useSessionChanged({ token, env })
54
+
55
+ const contextValue: ChatContextValue = {
56
+ env,
57
+ token,
58
+ onTokenExpired,
59
+ theme,
60
+ client,
61
+ }
25
62
 
26
63
  if (!Object.keys(value.token || {}).length) return null
27
64
 
28
65
  return (
29
- <ChatContext.Provider value={{ ...value, theme }}>
30
- <ApiProvider env={value.env} token={value.token} onTokenExpired={value.onTokenExpired}>
66
+ <ChatContext.Provider value={contextValue}>
67
+ <ApiProvider sessionChanged={sessionChanged} client={client}>
31
68
  {children}
32
69
  </ApiProvider>
33
70
  </ChatContext.Provider>
34
71
  )
35
72
  }
36
73
 
74
+ function useSessionChanged(value: Pick<ChatContextValue, 'token' | 'env'>): boolean {
75
+ const { token: newToken, env: newEnv } = value
76
+ const { token: prevToken, env: prevEnv } = usePrevious<typeof value>(value)
77
+
78
+ return Boolean(prevToken && newToken !== prevToken) || Boolean(prevEnv && newEnv !== prevEnv)
79
+ }
80
+
81
+ function usePrevious<T>(value) {
82
+ const ref = useRef<T>(value)
83
+
84
+ useEffect(() => {
85
+ ref.current = value
86
+ }, [value])
87
+
88
+ return ref.current
89
+ }
90
+
37
91
  interface CreateChatThemeProps {
38
92
  theme?: DeepPartial<DefaultTheme>
39
93
  colorScheme?: ColorSchemeName
@@ -1,2 +1,6 @@
1
1
  export * from './use_async_storage'
2
2
  export * from './use_theme'
3
+ export * from './use_suspense_api'
4
+ export * from './use_current_person'
5
+ export * from './use_font_scale'
6
+ export * from './use_create_android_ripple_color'
@@ -0,0 +1,18 @@
1
+ import { useMemo } from 'react'
2
+ import colorFunction from 'color'
3
+ import { useTheme } from './use_theme'
4
+
5
+ export const useCreateAndroidRippleColor = ({ color }: { color: string }) => {
6
+ const { colors } = useTheme()
7
+ const intensity = colors.name === 'dark' ? 0.4 : 0.35
8
+
9
+ const androidRippleColor = useMemo(() => {
10
+ return colorFunction(color)
11
+ .hsl()
12
+ [colors.name === 'dark' ? 'lighten' : 'darken'](intensity)
13
+ .alpha(intensity)
14
+ .string()
15
+ }, [color, colors.name, intensity])
16
+
17
+ return androidRippleColor
18
+ }
@@ -0,0 +1,15 @@
1
+ import { PersonResource } from '../types'
2
+ import { useSuspenseGet } from './use_suspense_api'
3
+
4
+ export const useCurrentPerson = () => {
5
+ const { data: person } = useSuspenseGet<PersonResource>({
6
+ url: '/me',
7
+ data: {
8
+ fields: {
9
+ Person: ['id', 'name', 'avatar', 'unread_count', 'pco_chat_enabled'],
10
+ },
11
+ },
12
+ })
13
+
14
+ return person
15
+ }
@@ -0,0 +1,9 @@
1
+ import { useWindowDimensions } from 'react-native'
2
+
3
+ export const useFontScale = ({ maxFontSizeMultiplier }: { maxFontSizeMultiplier?: number }) => {
4
+ const { fontScale: nativeFontScale } = useWindowDimensions()
5
+ const scaleLimit = maxFontSizeMultiplier || nativeFontScale
6
+ const fontScale = Math.min(scaleLimit, nativeFontScale)
7
+
8
+ return fontScale
9
+ }
@@ -0,0 +1,60 @@
1
+ import { InfiniteData, useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'
2
+ import { useContext } from 'react'
3
+ import { ChatContext } from '../contexts'
4
+ import { ApiCollection, ApiResource, ResourceObject } from '../types'
5
+ import { GetRequest, RequestData } from '../utils/client/types'
6
+
7
+ export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(args: GetRequest) => {
8
+ type Resource = T extends ResourceObject ? ApiResource<T> : ApiCollection<T>
9
+
10
+ const { data, ...query } = useSuspenseQuery<Resource, Response>({
11
+ queryKey: [args],
12
+ })
13
+
14
+ return { ...data, ...query }
15
+ }
16
+
17
+ type NextMeta = Partial<{
18
+ offset: string
19
+ idLt: string
20
+ }>
21
+
22
+ export const useSuspensePaginator = <T extends ResourceObject>(args: GetRequest) => {
23
+ const { client } = useContext(ChatContext)
24
+ const query = useSuspenseInfiniteQuery<
25
+ ApiCollection<T>,
26
+ Response,
27
+ InfiniteData<ApiCollection<T>>,
28
+ any,
29
+ Partial<RequestData> | undefined
30
+ >({
31
+ queryKey: [args.url, args.data],
32
+ queryFn: ({ pageParam }) => {
33
+ const pageParmWhere = pageParam?.where || {}
34
+ const argsWhere = args.data.where || {}
35
+ const where = { ...argsWhere, ...pageParmWhere }
36
+
37
+ const offset = pageParam?.offset || args.data.offset
38
+ const data = { ...args.data, where, offset }
39
+
40
+ return client.get({
41
+ url: args.url,
42
+ data,
43
+ })
44
+ },
45
+ initialPageParam: {} as Partial<RequestData>,
46
+ getNextPageParam: lastPage => {
47
+ const next: NextMeta = lastPage.meta?.next || {}
48
+ const { offset, idLt } = next
49
+
50
+ if (idLt) return { where: { id_lt: idLt } }
51
+ if (offset) return { offset: Number(offset) }
52
+
53
+ return undefined
54
+ },
55
+ })
56
+
57
+ const data: T[] = query.data?.pages.flatMap(page => page.data) || []
58
+
59
+ return { ...query, data }
60
+ }
@@ -5,7 +5,8 @@ import { NotFound } from '../screens/not_found'
5
5
  import { ScreenLayout } from './screenLayout'
6
6
  import { ConversationsScreen } from '../screens/conversations_screen'
7
7
  import { ConversationScreen } from '../screens/conversation_screen'
8
- import { HeaderBackButton } from '@react-navigation/elements'
8
+ import { HeaderBackButton, PlatformPressable } from '@react-navigation/elements'
9
+ import { Icon } from '../components'
9
10
 
10
11
  export const ChatStack = createNativeStackNavigator({
11
12
  screenLayout: ScreenLayout,
@@ -13,9 +14,18 @@ export const ChatStack = createNativeStackNavigator({
13
14
  Conversations: {
14
15
  screen: ConversationsScreen,
15
16
  options: ({ route, navigation }) => ({
16
- headerTitle: (route.params as { title?: string })?.title ?? 'Conversations',
17
- // This goes back on the parent so it doesn't automatically show up on the first screen
18
- headerLeft: () => <HeaderBackButton onPress={navigation.goBack} />,
17
+ headerTitle: (route.params as { title?: string })?.title ?? 'Chat',
18
+ headerLeft: () => (
19
+ <PlatformPressable onPress={() => null}>
20
+ <Icon name="general.threeReducingHorizontalBars" size={18} />
21
+ </PlatformPressable>
22
+ ),
23
+ headerRight: () => (
24
+ <HeaderBackButton
25
+ onPress={navigation.goBack}
26
+ backImage={() => <Icon name="general.x" size={18} />}
27
+ />
28
+ ),
19
29
  }),
20
30
  },
21
31
  Conversation: {