@planningcenter/chat-react-native 3.1.0-rc.13 → 3.1.0-rc.14

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 (218) hide show
  1. package/build/components/conversation/attachments/attachment_card.d.ts +9 -0
  2. package/build/components/conversation/attachments/attachment_card.d.ts.map +1 -0
  3. package/build/components/conversation/attachments/attachment_card.js +35 -0
  4. package/build/components/conversation/attachments/attachment_card.js.map +1 -0
  5. package/build/components/conversation/attachments/audio_attachment.d.ts +6 -0
  6. package/build/components/conversation/attachments/audio_attachment.d.ts.map +1 -0
  7. package/build/components/conversation/attachments/audio_attachment.js +84 -0
  8. package/build/components/conversation/attachments/audio_attachment.js.map +1 -0
  9. package/build/components/conversation/attachments/constants.d.ts +3 -0
  10. package/build/components/conversation/attachments/constants.d.ts.map +1 -0
  11. package/build/components/conversation/attachments/constants.js +4 -0
  12. package/build/components/conversation/attachments/constants.js.map +1 -0
  13. package/build/components/conversation/attachments/download_attachment_button.d.ts +9 -0
  14. package/build/components/conversation/attachments/download_attachment_button.d.ts.map +1 -0
  15. package/build/components/conversation/attachments/download_attachment_button.js +29 -0
  16. package/build/components/conversation/attachments/download_attachment_button.js.map +1 -0
  17. package/build/components/conversation/attachments/expanded_link.d.ts +5 -0
  18. package/build/components/conversation/attachments/expanded_link.d.ts.map +1 -0
  19. package/build/components/conversation/attachments/expanded_link.js +44 -0
  20. package/build/components/conversation/attachments/expanded_link.js.map +1 -0
  21. package/build/components/conversation/attachments/generic_file_attachment.d.ts +7 -0
  22. package/build/components/conversation/attachments/generic_file_attachment.d.ts.map +1 -0
  23. package/build/components/conversation/attachments/generic_file_attachment.js +63 -0
  24. package/build/components/conversation/attachments/generic_file_attachment.js.map +1 -0
  25. package/build/components/conversation/attachments/giphy_attachment.d.ts +6 -0
  26. package/build/components/conversation/attachments/giphy_attachment.d.ts.map +1 -0
  27. package/build/components/conversation/attachments/giphy_attachment.js +42 -0
  28. package/build/components/conversation/attachments/giphy_attachment.js.map +1 -0
  29. package/build/components/conversation/attachments/image_attachment.d.ts +5 -0
  30. package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -0
  31. package/build/components/conversation/attachments/image_attachment.js +24 -0
  32. package/build/components/conversation/attachments/image_attachment.js.map +1 -0
  33. package/build/components/conversation/attachments/video_attachment.d.ts +6 -0
  34. package/build/components/conversation/attachments/video_attachment.d.ts.map +1 -0
  35. package/build/components/conversation/attachments/video_attachment.js +64 -0
  36. package/build/components/conversation/attachments/video_attachment.js.map +1 -0
  37. package/build/components/conversation/message.d.ts +1 -1
  38. package/build/components/conversation/message.d.ts.map +1 -1
  39. package/build/components/conversation/message.js +10 -6
  40. package/build/components/conversation/message.js.map +1 -1
  41. package/build/components/conversation/message_attachments.d.ts +6 -0
  42. package/build/components/conversation/message_attachments.d.ts.map +1 -0
  43. package/build/components/conversation/message_attachments.js +52 -0
  44. package/build/components/conversation/message_attachments.js.map +1 -0
  45. package/build/components/conversations.d.ts +6 -1
  46. package/build/components/conversations.d.ts.map +1 -1
  47. package/build/components/conversations.js +19 -24
  48. package/build/components/conversations.js.map +1 -1
  49. package/build/components/display/badge.d.ts +9 -1
  50. package/build/components/display/badge.d.ts.map +1 -1
  51. package/build/components/display/badge.js +22 -10
  52. package/build/components/display/badge.js.map +1 -1
  53. package/build/components/display/button.d.ts +1 -1
  54. package/build/components/display/button.d.ts.map +1 -1
  55. package/build/components/display/button.js.map +1 -1
  56. package/build/components/display/child_notice.js +2 -2
  57. package/build/components/display/child_notice.js.map +1 -1
  58. package/build/components/display/icon.d.ts.map +1 -1
  59. package/build/components/display/icon.js +10 -8
  60. package/build/components/display/icon.js.map +1 -1
  61. package/build/components/page/error_boundary.d.ts +1 -1
  62. package/build/hooks/use_api.d.ts +6 -4
  63. package/build/hooks/use_api.d.ts.map +1 -1
  64. package/build/hooks/use_api.js +5 -2
  65. package/build/hooks/use_api.js.map +1 -1
  66. package/build/hooks/use_api_client.d.ts +1 -2
  67. package/build/hooks/use_api_client.d.ts.map +1 -1
  68. package/build/hooks/use_api_client.js.map +1 -1
  69. package/build/hooks/use_conversation.js +1 -1
  70. package/build/hooks/use_conversation.js.map +1 -1
  71. package/build/hooks/use_conversation_jolt_events.js +1 -1
  72. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  73. package/build/hooks/use_conversation_messages_jolt_events.js +1 -1
  74. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  75. package/build/hooks/use_conversations.d.ts +2 -3
  76. package/build/hooks/use_conversations.d.ts.map +1 -1
  77. package/build/hooks/use_conversations.js +3 -29
  78. package/build/hooks/use_conversations.js.map +1 -1
  79. package/build/hooks/use_groups.d.ts +214 -0
  80. package/build/hooks/use_groups.d.ts.map +1 -0
  81. package/build/hooks/use_groups.js +22 -0
  82. package/build/hooks/use_groups.js.map +1 -0
  83. package/build/hooks/use_groups_groups.d.ts +208 -0
  84. package/build/hooks/use_groups_groups.d.ts.map +1 -0
  85. package/build/hooks/use_groups_groups.js +18 -0
  86. package/build/hooks/use_groups_groups.js.map +1 -0
  87. package/build/hooks/use_services_team.d.ts +4 -0
  88. package/build/hooks/use_services_team.d.ts.map +1 -0
  89. package/build/hooks/use_services_team.js +22 -0
  90. package/build/hooks/use_services_team.js.map +1 -0
  91. package/build/hooks/use_suspense_api.d.ts +2 -1
  92. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  93. package/build/hooks/use_suspense_api.js +2 -1
  94. package/build/hooks/use_suspense_api.js.map +1 -1
  95. package/build/hooks/use_teams.d.ts +208 -0
  96. package/build/hooks/use_teams.d.ts.map +1 -0
  97. package/build/hooks/use_teams.js +22 -0
  98. package/build/hooks/use_teams.js.map +1 -0
  99. package/build/navigation/index.d.ts +11 -0
  100. package/build/navigation/index.d.ts.map +1 -1
  101. package/build/navigation/index.js +5 -0
  102. package/build/navigation/index.js.map +1 -1
  103. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  104. package/build/screens/conversation_details_screen.js +86 -62
  105. package/build/screens/conversation_details_screen.js.map +1 -1
  106. package/build/screens/conversation_filters_screen.d.ts +13 -0
  107. package/build/screens/conversation_filters_screen.d.ts.map +1 -0
  108. package/build/screens/conversation_filters_screen.js +346 -0
  109. package/build/screens/conversation_filters_screen.js.map +1 -0
  110. package/build/screens/conversation_screen.js +2 -1
  111. package/build/screens/conversation_screen.js.map +1 -1
  112. package/build/screens/conversations_screen.d.ts +4 -2
  113. package/build/screens/conversations_screen.d.ts.map +1 -1
  114. package/build/screens/conversations_screen.js +139 -11
  115. package/build/screens/conversations_screen.js.map +1 -1
  116. package/build/screens/create/conversation_select_recipients_screen.d.ts.map +1 -1
  117. package/build/screens/create/conversation_select_recipients_screen.js +3 -11
  118. package/build/screens/create/conversation_select_recipients_screen.js.map +1 -1
  119. package/build/types/resources/group_resource.d.ts +4 -2
  120. package/build/types/resources/group_resource.d.ts.map +1 -1
  121. package/build/types/resources/group_resource.js.map +1 -1
  122. package/build/types/resources/index.d.ts +1 -0
  123. package/build/types/resources/index.d.ts.map +1 -1
  124. package/build/types/resources/index.js +1 -0
  125. package/build/types/resources/index.js.map +1 -1
  126. package/build/types/resources/services/index.d.ts +2 -0
  127. package/build/types/resources/services/index.d.ts.map +1 -0
  128. package/build/types/resources/services/index.js +2 -0
  129. package/build/types/resources/services/index.js.map +1 -0
  130. package/build/types/resources/services/team_resource.d.ts +48 -0
  131. package/build/types/resources/services/team_resource.d.ts.map +1 -0
  132. package/build/types/resources/services/team_resource.js +7 -0
  133. package/build/types/resources/services/team_resource.js.map +1 -0
  134. package/build/utils/index.d.ts +1 -0
  135. package/build/utils/index.d.ts.map +1 -1
  136. package/build/utils/index.js +1 -0
  137. package/build/utils/index.js.map +1 -1
  138. package/build/utils/native_adapters/audio.d.ts +13 -0
  139. package/build/utils/native_adapters/audio.d.ts.map +1 -0
  140. package/build/utils/native_adapters/audio.js +7 -0
  141. package/build/utils/native_adapters/audio.js.map +1 -0
  142. package/build/utils/native_adapters/configuration.d.ts +7 -1
  143. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  144. package/build/utils/native_adapters/configuration.js +17 -1
  145. package/build/utils/native_adapters/configuration.js.map +1 -1
  146. package/build/utils/native_adapters/index.d.ts +2 -0
  147. package/build/utils/native_adapters/index.d.ts.map +1 -1
  148. package/build/utils/native_adapters/index.js +2 -0
  149. package/build/utils/native_adapters/index.js.map +1 -1
  150. package/build/utils/native_adapters/video.d.ts +25 -0
  151. package/build/utils/native_adapters/video.d.ts.map +1 -0
  152. package/build/utils/native_adapters/video.js +7 -0
  153. package/build/utils/native_adapters/video.js.map +1 -0
  154. package/build/utils/pluralize.d.ts +2 -0
  155. package/build/utils/pluralize.d.ts.map +1 -0
  156. package/build/utils/pluralize.js +10 -0
  157. package/build/utils/pluralize.js.map +1 -0
  158. package/build/utils/request/conversation.d.ts +10 -0
  159. package/build/utils/request/conversation.d.ts.map +1 -0
  160. package/build/utils/request/conversation.js +32 -0
  161. package/build/utils/request/conversation.js.map +1 -0
  162. package/build/utils/request/messages.d.ts +15 -0
  163. package/build/utils/request/messages.d.ts.map +1 -0
  164. package/build/utils/request/messages.js +22 -0
  165. package/build/utils/request/messages.js.map +1 -0
  166. package/build/utils/theme.d.ts +1 -1
  167. package/build/utils/theme.d.ts.map +1 -1
  168. package/build/utils/theme.js +1 -1
  169. package/build/utils/theme.js.map +1 -1
  170. package/package.json +2 -2
  171. package/src/__tests__/utils/pluralize.tsx +17 -0
  172. package/src/components/conversation/attachments/attachment_card.tsx +46 -0
  173. package/src/components/conversation/attachments/audio_attachment.tsx +102 -0
  174. package/src/components/conversation/attachments/constants.ts +5 -0
  175. package/src/components/conversation/attachments/download_attachment_button.tsx +46 -0
  176. package/src/components/conversation/attachments/expanded_link.tsx +54 -0
  177. package/src/components/conversation/attachments/generic_file_attachment.tsx +75 -0
  178. package/src/components/conversation/attachments/giphy_attachment.tsx +56 -0
  179. package/src/components/conversation/attachments/image_attachment.tsx +31 -0
  180. package/src/components/conversation/attachments/video_attachment.tsx +92 -0
  181. package/src/components/conversation/message.tsx +10 -5
  182. package/src/components/conversation/message_attachments.tsx +61 -0
  183. package/src/components/conversations.tsx +37 -35
  184. package/src/components/display/badge.tsx +41 -10
  185. package/src/components/display/button.tsx +1 -1
  186. package/src/components/display/child_notice.tsx +2 -2
  187. package/src/components/display/icon.tsx +10 -8
  188. package/src/hooks/use_api.ts +11 -10
  189. package/src/hooks/use_api_client.ts +1 -1
  190. package/src/hooks/use_conversation.ts +1 -1
  191. package/src/hooks/use_conversation_jolt_events.ts +1 -1
  192. package/src/hooks/use_conversation_messages_jolt_events.ts +1 -1
  193. package/src/hooks/use_conversations.ts +3 -31
  194. package/src/hooks/use_groups.ts +31 -0
  195. package/src/hooks/use_groups_groups.ts +20 -0
  196. package/src/hooks/use_services_team.ts +30 -0
  197. package/src/hooks/use_suspense_api.ts +4 -4
  198. package/src/hooks/use_teams.ts +25 -0
  199. package/src/navigation/index.tsx +8 -0
  200. package/src/screens/conversation_details_screen.tsx +149 -117
  201. package/src/screens/conversation_filters_screen.tsx +488 -0
  202. package/src/screens/conversation_screen.tsx +8 -1
  203. package/src/screens/conversations_screen.tsx +210 -13
  204. package/src/screens/create/conversation_select_recipients_screen.tsx +3 -11
  205. package/src/types/resources/group_resource.ts +5 -2
  206. package/src/types/resources/index.ts +1 -0
  207. package/src/types/resources/services/index.ts +1 -0
  208. package/src/types/resources/services/team_resource.ts +60 -0
  209. package/src/utils/client/types.d.ts +2 -1
  210. package/src/utils/index.ts +1 -0
  211. package/src/utils/native_adapters/audio.ts +15 -0
  212. package/src/utils/native_adapters/configuration.ts +24 -1
  213. package/src/utils/native_adapters/index.ts +2 -0
  214. package/src/utils/native_adapters/video.ts +30 -0
  215. package/src/utils/pluralize.ts +11 -0
  216. package/src/utils/request/conversation.ts +46 -0
  217. package/src/utils/request/messages.ts +21 -0
  218. package/src/utils/theme.ts +2 -2
@@ -0,0 +1,56 @@
1
+ import React from 'react'
2
+ import { Text, Image, StyleSheet, Linking, Pressable } from 'react-native'
3
+ import { tokens } from '../../../vendor/tapestry/tokens'
4
+ import { useTheme } from '../../../hooks'
5
+ import { DenormalizedGiphyAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
6
+
7
+ export function GiphyAttachment({
8
+ attachment,
9
+ }: {
10
+ attachment: DenormalizedGiphyAttachmentResource
11
+ }) {
12
+ const styles = useStyles()
13
+ const { title, titleLink, giphy } = attachment
14
+ const { url, width, height } = giphy.fixedWidth
15
+
16
+ function handlePress() {
17
+ if (titleLink) {
18
+ // Open Giphy link in the default web browser
19
+ Linking.openURL(titleLink)
20
+ }
21
+ }
22
+
23
+ return (
24
+ <Pressable onPress={handlePress} style={styles.container}>
25
+ <Image
26
+ source={{ uri: url }}
27
+ style={[styles.image, { width, height }]}
28
+ accessibilityLabel={title}
29
+ />
30
+ <Text style={styles.link}>
31
+ <Text style={styles.tag}>/giphy</Text> {title}
32
+ </Text>
33
+ </Pressable>
34
+ )
35
+ }
36
+
37
+ const useStyles = () => {
38
+ const { colors } = useTheme()
39
+ return StyleSheet.create({
40
+ container: {
41
+ gap: 4,
42
+ },
43
+ image: {
44
+ borderRadius: 8,
45
+ },
46
+ link: {
47
+ fontSize: tokens.fontSizeSm,
48
+ color: colors.fillColorInteractionDefault,
49
+ paddingHorizontal: 8,
50
+ paddingVertical: 6,
51
+ },
52
+ tag: {
53
+ fontWeight: 'bold',
54
+ },
55
+ })
56
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+ import { View, Image, StyleSheet } from 'react-native'
3
+
4
+ export function ImageAttachment({ attachment }: { attachment: any }) {
5
+ const styles = useStyles()
6
+ const { attributes } = attachment
7
+ const { url, urlMedium, filename, metadata = {} } = attributes
8
+ const width = metadata.width || 100
9
+ const height = metadata.height || 100
10
+ return (
11
+ <View style={styles.container}>
12
+ <Image
13
+ source={{ uri: urlMedium || url }}
14
+ style={[styles.image, { aspectRatio: width / height }]}
15
+ accessibilityLabel={filename}
16
+ />
17
+ </View>
18
+ )
19
+ }
20
+
21
+ const useStyles = () => {
22
+ return StyleSheet.create({
23
+ container: {
24
+ maxWidth: '100%',
25
+ },
26
+ image: {
27
+ borderRadius: 8,
28
+ minWidth: 200,
29
+ },
30
+ })
31
+ }
@@ -0,0 +1,92 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import { View, StyleSheet, Pressable } from 'react-native'
3
+ import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
4
+ import { MESSAGE_ATTACHMENT_WIDTH_SINGLE } from './constants'
5
+ import { IconButton } from '../../display'
6
+ import { Video, VideoPlayerHandle } from '../../../utils/native_adapters'
7
+
8
+ export function VideoAttachment({
9
+ attachment,
10
+ }: {
11
+ attachment: DenormalizedMessageAttachmentResource
12
+ }) {
13
+ const { attributes } = attachment
14
+ const { width = MESSAGE_ATTACHMENT_WIDTH_SINGLE, height = MESSAGE_ATTACHMENT_WIDTH_SINGLE } =
15
+ attributes.metadata || {}
16
+ const { url } = attributes
17
+
18
+ const videoRef = useRef<VideoPlayerHandle>(null)
19
+ const [isOpen, setIsOpen] = useState(false)
20
+
21
+ function openVideo() {
22
+ if (!isOpen && videoRef.current) {
23
+ videoRef.current.presentFullscreenPlayer()
24
+ videoRef.current.play()
25
+ setIsOpen(true)
26
+ }
27
+ }
28
+
29
+ function onFullscreenPlayerWillDismiss() {
30
+ videoRef.current?.pause()
31
+ setIsOpen(false)
32
+ }
33
+
34
+ const viewRef = useRef<View>(null)
35
+
36
+ return (
37
+ <View style={styles.container} ref={viewRef}>
38
+ <Pressable onPress={openVideo}>
39
+ <Video.Player
40
+ ref={videoRef}
41
+ source={{ uri: url }}
42
+ aspectRatio={width / height}
43
+ style={styles.video}
44
+ onFullscreenPlayerWillDismiss={onFullscreenPlayerWillDismiss}
45
+ />
46
+ {!isOpen && (
47
+ <View style={styles.playButtonWrapper}>
48
+ <IconButton
49
+ name="services.play"
50
+ size="md"
51
+ accessibilityLabel="Play Video"
52
+ onPress={openVideo}
53
+ style={styles.button}
54
+ />
55
+ </View>
56
+ )}
57
+ </Pressable>
58
+ </View>
59
+ )
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ container: {
64
+ maxWidth: 320,
65
+ maxHeight: 320,
66
+ },
67
+ video: {
68
+ width: '100%',
69
+ height: 'auto',
70
+ backgroundColor: 'black',
71
+ borderRadius: 8,
72
+ },
73
+ playButtonWrapper: {
74
+ position: 'absolute',
75
+ top: 0,
76
+ left: 0,
77
+ right: 0,
78
+ bottom: 0,
79
+ justifyContent: 'center',
80
+ alignItems: 'center',
81
+ },
82
+ button: {
83
+ width: 42,
84
+ height: 42,
85
+ justifyContent: 'center',
86
+ alignItems: 'center',
87
+ borderRadius: 25,
88
+ backgroundColor: '#eee',
89
+ borderColor: '#aaa',
90
+ borderWidth: 1,
91
+ },
92
+ })
@@ -8,6 +8,8 @@ import { Avatar, Text } from '../../components/display'
8
8
  import { useTheme } from '../../hooks'
9
9
  import { MessageResource } from '../../types'
10
10
  import { ReactionCountResource } from '../../types/resources/reaction'
11
+ import { MessageAttachments } from './message_attachments'
12
+ import ErrorBoundary from '../page/error_boundary'
11
13
 
12
14
  /** Message
13
15
  * Component for display of a message within a conversation list
@@ -29,7 +31,6 @@ export function Message(props: MessageResource & { conversation_id: number }) {
29
31
  reaction_value: reaction.value,
30
32
  })
31
33
  }
32
- if (!text) return null
33
34
 
34
35
  return (
35
36
  <View style={styles.message}>
@@ -41,7 +42,10 @@ export function Message(props: MessageResource & { conversation_id: number }) {
41
42
  <View style={styles.messageContent}>
42
43
  {!props.mine && <Text variant="tertiary">{props.author.name}</Text>}
43
44
  <PlatformPressable style={styles.messageBubble} onLongPress={handleMessagePress}>
44
- <Text style={styles.messageText}>{text}</Text>
45
+ <ErrorBoundary>
46
+ <MessageAttachments attachments={props.attachments} />
47
+ </ErrorBoundary>
48
+ {text && <Text style={styles.messageText}>{text}</Text>}
45
49
  </PlatformPressable>
46
50
  <View style={styles.messageReactions}>
47
51
  {reactionCounts.map(reaction => (
@@ -76,12 +80,13 @@ const useMessageStyles = ({ mine }: MessageResource) => {
76
80
  },
77
81
  messageBubble: {
78
82
  backgroundColor: mine ? activeColor : colors.fillColorNeutral070,
79
- borderRadius: 12,
80
- paddingVertical: 6,
81
- paddingHorizontal: 8,
83
+ borderRadius: 8,
84
+ maxWidth: 232,
82
85
  },
83
86
  messageText: {
84
87
  color: colors.textColorDefaultPrimary,
88
+ paddingVertical: 6,
89
+ paddingHorizontal: 8,
85
90
  },
86
91
  messageReactions: {
87
92
  flexDirection: 'row',
@@ -0,0 +1,61 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet } from 'react-native'
3
+ import { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'
4
+ import { AudioAttachment } from './attachments/audio_attachment'
5
+ import { VideoAttachment } from './attachments/video_attachment'
6
+ import { GiphyAttachment } from './attachments/giphy_attachment'
7
+ import { GenericFileAttachment } from './attachments/generic_file_attachment'
8
+ import { ExpandedLink } from './attachments/expanded_link'
9
+ import { ImageAttachment } from './attachments/image_attachment'
10
+
11
+ export function MessageAttachments(props: { attachments: DenormalizedAttachmentResource[] }) {
12
+ const styles = useStyles()
13
+ const { attachments } = props
14
+ if (!attachments || attachments.length === 0) return null
15
+ return (
16
+ <View style={styles.attachmentsContainer}>
17
+ {attachments.map(attachment => {
18
+ switch (attachment.type) {
19
+ case 'MessageAttachment':
20
+ return <MessageAttachment key={attachment.id} attachment={attachment} />
21
+ case 'giphy':
22
+ return (
23
+ <GiphyAttachment
24
+ key={attachment.id || attachment.titleLink}
25
+ attachment={attachment}
26
+ />
27
+ )
28
+ case 'ExpandedLink':
29
+ return <ExpandedLink key={attachment.id} attachment={attachment} />
30
+ default:
31
+ return null
32
+ }
33
+ })}
34
+ </View>
35
+ )
36
+ }
37
+
38
+ function MessageAttachment({ attachment }: { attachment: any }) {
39
+ const { attributes } = attachment
40
+ const contentType = attributes?.contentType
41
+ const basicType = contentType ? contentType.split('/')[0] : ''
42
+ switch (basicType) {
43
+ case 'image':
44
+ return <ImageAttachment attachment={attachment} />
45
+ case 'video':
46
+ return <VideoAttachment attachment={attachment} />
47
+ case 'audio':
48
+ return <AudioAttachment attachment={attachment} />
49
+ default:
50
+ return <GenericFileAttachment attachment={attachment} />
51
+ }
52
+ }
53
+
54
+ const useStyles = () => {
55
+ return StyleSheet.create({
56
+ attachmentsContainer: {
57
+ gap: 4,
58
+ padding: 2,
59
+ },
60
+ })
61
+ }
@@ -1,26 +1,37 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import { FlashList } from '@shopify/flash-list'
3
- import React, { useMemo } from 'react'
3
+ import React from 'react'
4
4
  import { Pressable, StyleSheet, View } from 'react-native'
5
5
  import { useTheme } from '../hooks'
6
6
  import { useConversationsJoltEvents } from '../hooks/use_conversation_jolt_events'
7
7
  import { useConversations } from '../hooks/use_conversations'
8
8
  import { formatDatePreview } from '../utils/date'
9
- import { AvatarGroup, Badge, Heading, Text, TextButton } from './display'
10
- import { ActionButton } from './display/action_button'
11
- import { useCanCreateConversations } from '../hooks/use_chat_permissions'
9
+ import { AvatarGroup, Badge, Heading, Text } from './display'
10
+ import { ConversationRequestArgs } from '../utils/request/conversation'
12
11
 
13
- export const Conversations = () => {
14
- const styles = useStyles()
12
+ interface ConversationsProps extends Partial<ConversationRequestArgs> {
13
+ ListHeaderComponent?:
14
+ | React.ComponentType<any>
15
+ | React.ReactElement<any, string | React.JSXElementConstructor<any>>
16
+ | null
17
+ | undefined
18
+ }
15
19
 
16
- const { conversations, fetchNextPage, refetch, isRefetching } = useConversations()
17
- const canCreateConversations = useCanCreateConversations()
20
+ export const Conversations = ({
21
+ ListHeaderComponent,
22
+ filter,
23
+ group,
24
+ gids,
25
+ group_source_app_name,
26
+ }: ConversationsProps) => {
27
+ const styles = useStyles()
18
28
 
19
- // TODO: Filter using the API
20
- const nonEmptyConversations = useMemo(
21
- () => conversations.filter(c => c.lastMessageTextPreview) || [],
22
- [conversations]
23
- )
29
+ const { conversations, fetchNextPage, refetch, isRefetching, isFetched } = useConversations({
30
+ filter,
31
+ group,
32
+ gids,
33
+ group_source_app_name,
34
+ })
24
35
  const navigation = useNavigation()
25
36
 
26
37
  useConversationsJoltEvents()
@@ -28,20 +39,17 @@ export const Conversations = () => {
28
39
  return (
29
40
  <View style={styles.container}>
30
41
  <FlashList
31
- data={nonEmptyConversations}
42
+ data={conversations}
32
43
  estimatedItemSize={97}
33
44
  contentContainerStyle={styles.contentContainer}
34
45
  onRefresh={refetch}
35
- refreshing={isRefetching}
36
- ListHeaderComponent={
37
- <View style={styles.header}>
38
- <Heading numberOfLines={1} variant="h2">
39
- Conversations
40
- </Heading>
41
- <TextButton>Mark all read</TextButton>
46
+ refreshing={!isFetched && isRefetching}
47
+ ListHeaderComponent={ListHeaderComponent}
48
+ ListEmptyComponent={
49
+ <View style={styles.listEmpty}>
50
+ <Text variant="secondary">No conversations found</Text>
42
51
  </View>
43
52
  }
44
- ListEmptyComponent={<Text>No conversations found</Text>}
45
53
  renderItem={({ item }) => (
46
54
  <Pressable
47
55
  style={styles.conversation}
@@ -75,11 +83,6 @@ export const Conversations = () => {
75
83
  )}
76
84
  onEndReached={() => fetchNextPage()}
77
85
  />
78
- <ActionButton
79
- visible={canCreateConversations}
80
- title="New conversation"
81
- onPress={() => navigation.navigate('Create')}
82
- />
83
86
  </View>
84
87
  )
85
88
  }
@@ -101,13 +104,6 @@ const useStyles = () => {
101
104
  const { colors } = useTheme()
102
105
 
103
106
  return StyleSheet.create({
104
- header: {
105
- flexDirection: 'row',
106
- justifyContent: 'space-between',
107
- paddingTop: 8,
108
- paddingBottom: 8,
109
- paddingHorizontal: 16,
110
- },
111
107
  container: { flex: 1 },
112
108
  contentContainer: { paddingVertical: 16 },
113
109
  listItem: { color: colors.fillColorNeutral020 },
@@ -116,7 +112,7 @@ const useStyles = () => {
116
112
  gap: 8,
117
113
  borderBottomWidth: 1,
118
114
  borderBottomColor: colors.fillColorNeutral060,
119
- paddingTop: 4,
115
+ paddingTop: 12,
120
116
  paddingBottom: 12,
121
117
  paddingHorizontal: 16,
122
118
  },
@@ -140,5 +136,11 @@ const useStyles = () => {
140
136
  borderRadius: 24,
141
137
  color: 'white',
142
138
  },
139
+ listEmpty: {
140
+ flex: 1,
141
+ justifyContent: 'center',
142
+ alignItems: 'center',
143
+ paddingVertical: 16,
144
+ },
143
145
  })
144
146
  }
@@ -1,12 +1,12 @@
1
1
  import React from 'react'
2
- import { StyleSheet, View } from 'react-native'
3
- import type { ViewStyle, TextStyle } from 'react-native'
4
- import Svg, { Path, Defs, LinearGradient, Stop } from 'react-native-svg'
2
+ import type { TextStyle, ViewProps, ViewStyle } from 'react-native'
3
+ import { Pressable, StyleSheet, View } from 'react-native'
4
+ import Svg, { Defs, LinearGradient, Path, Stop } from 'react-native-svg'
5
5
  import { useFontScale, useTheme } from '../../hooks'
6
- import { Icon } from './icon'
7
- import { Text } from './text'
8
6
  import { platformFontWeightMedium, space } from '../../utils'
9
7
  import { tokens } from '../../vendor/tapestry/tokens'
8
+ import { Icon } from './icon'
9
+ import { Text } from './text'
10
10
  import { useStatusColorAppearanceMap, type StatusAppearanceUnion } from './utils/status_colors'
11
11
 
12
12
  // =================================
@@ -72,6 +72,10 @@ interface BadgeProps {
72
72
  * Shows an icon of the user choice to the left of the text.
73
73
  */
74
74
  iconName?: string
75
+ /**
76
+ * Shows an icon of the user choice to the right of the text.
77
+ */
78
+ iconNameRight?: string
75
79
  /**
76
80
  * Styles badge wrapper.
77
81
  */
@@ -80,6 +84,10 @@ interface BadgeProps {
80
84
  * Specifies the maximum size a font can reach when allowFontScaling is enabled.
81
85
  */
82
86
  maxFontSizeMultiplier?: number
87
+ /**
88
+ * Callback function when the badge is pressed.
89
+ */
90
+ onPress?: () => void
83
91
  }
84
92
 
85
93
  export function Badge({
@@ -87,40 +95,59 @@ export function Badge({
87
95
  metaLabel,
88
96
  style,
89
97
  iconName,
98
+ iconNameRight,
90
99
  label,
91
100
  productLogoName,
92
101
  variant = 'default',
93
102
  maxFontSizeMultiplier,
103
+ onPress,
94
104
  }: BadgeProps) {
95
105
  const styles = useStyles({ appearance, maxFontSizeMultiplier, variant })
96
- const { showBadgeLogo } = useTheme()
106
+ const { showBadgeProductLogo } = useTheme()
97
107
 
98
108
  const isMetaSubtle = variant === 'metaSubtle'
99
- const isMeta = variant === 'meta'
100
109
  const hasMetaLabel = Boolean(metaLabel)
101
110
 
102
- const showLogo = showBadgeLogo && productLogoName && isMeta
111
+ const showLogo = showBadgeProductLogo && productLogoName
103
112
  const ProductLogoSvg = showLogo && PRODUCT_LOGO_COMPONENT_MAP[productLogoName?.toLowerCase()]
104
113
  const badgeLabel = isMetaSubtle && hasMetaLabel ? `${label}:` : label
105
114
 
106
115
  return (
107
- <View style={[styles.badgeWrapper, style]}>
116
+ <BadgeWrapper style={[styles.badgeWrapper, style]} onPress={onPress} accessibilityRole="button">
108
117
  <View style={styles.badge}>
109
118
  {showLogo && ProductLogoSvg && <ProductLogoSvg logoSize={styles.logo.fontSize} />}
110
119
  {iconName && <Icon name={iconName} style={styles.icon} />}
111
120
  <Text variant="footnote" style={styles.label}>
112
121
  {badgeLabel}
113
122
  </Text>
123
+ {iconNameRight && <Icon name={iconNameRight} style={styles.iconRight} />}
114
124
  </View>
115
125
  {hasMetaLabel && (
116
126
  <Text variant="footnote" style={styles.metaLabel} numberOfLines={1}>
117
127
  {metaLabel}
118
128
  </Text>
119
129
  )}
120
- </View>
130
+ </BadgeWrapper>
121
131
  )
122
132
  }
123
133
 
134
+ const BadgeWrapper = ({
135
+ children,
136
+ onPress,
137
+ accessibilityRole,
138
+ ...restProps
139
+ }: ViewProps & Pick<BadgeProps, 'onPress'>) => {
140
+ if (onPress) {
141
+ return (
142
+ <Pressable accessibilityRole={accessibilityRole} onPress={onPress} {...restProps}>
143
+ {children}
144
+ </Pressable>
145
+ )
146
+ }
147
+
148
+ return <View {...restProps}>{children}</View>
149
+ }
150
+
124
151
  // =================================
125
152
  // ====== Styles ===================
126
153
  // =================================
@@ -199,6 +226,10 @@ const useStyles = ({
199
226
  color: statusColorMap[appearance].icon,
200
227
  fontSize: badgeFontSize,
201
228
  },
229
+ iconRight: {
230
+ color: statusColorMap[appearance].icon,
231
+ fontSize: badgeFontSize - 2,
232
+ },
202
233
  logo: {
203
234
  fontSize: badgeFontSize * fontScale,
204
235
  },
@@ -50,7 +50,7 @@ type VariantColors = Record<
50
50
  // ====== Component ================
51
51
  // =================================
52
52
 
53
- interface ButtonProps extends PressableProps {
53
+ export interface ButtonProps extends PressableProps {
54
54
  /**
55
55
  * Specifies whether fonts should be scaled down automatically to fit given style constraints.
56
56
  */
@@ -4,7 +4,7 @@ import { BannerCollapsible } from './banner_collapsible'
4
4
  import BannerPrimitive from '../primitive/banner_primitive'
5
5
  import { View, StyleSheet } from 'react-native'
6
6
  import { Avatar } from './avatar'
7
- import { platformFontWeightMedium } from '../../utils'
7
+ import { platformFontWeightMedium, pluralize } from '../../utils'
8
8
  import { Text } from './text'
9
9
 
10
10
  // =================================
@@ -17,7 +17,7 @@ interface ChildNoticeProps {
17
17
 
18
18
  export function ChildNotice({ childMembers }: ChildNoticeProps) {
19
19
  const styles = useStyles()
20
- const heading = `${childMembers.length} members under age 13`
20
+ const heading = `${pluralize(childMembers.length, 'member')} under age 13`
21
21
 
22
22
  return (
23
23
  <BannerCollapsible appearance="warning" iconName="general.shieldExclamation" heading={heading}>
@@ -4,14 +4,15 @@ 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
- import * as general from '@planningcenter/icons/paths/general'
8
- import * as groups from '@planningcenter/icons/paths/groups'
7
+ import * as api from '@planningcenter/icons/paths/api'
8
+ import * as brand from '@planningcenter/icons/paths/brand'
9
9
  import * as calendar from '@planningcenter/icons/paths/calendar'
10
- import * as people from '@planningcenter/icons/paths/people'
11
10
  import * as churchCenter from '@planningcenter/icons/paths/church-center'
11
+ import * as general from '@planningcenter/icons/paths/general'
12
+ import * as groups from '@planningcenter/icons/paths/groups'
12
13
  import * as logomark from '@planningcenter/icons/paths/logomark'
13
- import * as brand from '@planningcenter/icons/paths/brand'
14
- import * as api from '@planningcenter/icons/paths/api'
14
+ import * as people from '@planningcenter/icons/paths/people'
15
+ import * as services from '@planningcenter/icons/paths/services'
15
16
 
16
17
  // =================================
17
18
  // ====== Constants ================
@@ -21,13 +22,14 @@ const FALLBACK_SIZE = 12
21
22
 
22
23
  const ICONS = {
23
24
  api,
25
+ brand,
24
26
  calendar,
25
- people,
26
27
  churchCenter,
27
- groups,
28
28
  general,
29
+ groups,
29
30
  logomark,
30
- brand,
31
+ people,
32
+ services,
31
33
  } as const
32
34
 
33
35
  type IconStyle = ViewStyle & {
@@ -6,20 +6,20 @@ import {
6
6
  } from '@tanstack/react-query'
7
7
  import { ApiCollection, ApiResource, ResourceObject } from '../types'
8
8
  import { GetRequest, RequestData } from '../utils/client/types'
9
- import { useApiClient } from './use_api_client'
9
+ import { App, useApiClient } from './use_api_client'
10
10
  import { getRequestQueryKey } from './use_suspense_api'
11
11
 
12
- interface SuspenseGetOptions extends GetRequest {
13
- app?: 'chat' | 'groups'
12
+ interface ApiGetOptions extends GetRequest {
13
+ app?: App
14
+ enabled?: boolean
14
15
  }
15
16
 
16
- export const useApiGet = <T extends ResourceObject | ResourceObject[]>(
17
- args: SuspenseGetOptions
18
- ) => {
17
+ export const useApiGet = <T extends ResourceObject | ResourceObject[]>(args: ApiGetOptions) => {
19
18
  type Resource = ApiResource<T>
20
19
 
21
20
  const { data, ...query } = useQuery<Resource, Response>({
22
21
  queryKey: getRequestQueryKey(args),
22
+ enabled: args.enabled,
23
23
  })
24
24
 
25
25
  return { ...data, ...query }
@@ -36,7 +36,7 @@ export type SuspensePaginatorOptions = Omit<
36
36
  >
37
37
 
38
38
  export const useApiPaginator = <T extends ResourceObject>(
39
- args: SuspenseGetOptions,
39
+ args: ApiGetOptions,
40
40
  opts?: SuspensePaginatorOptions
41
41
  ) => {
42
42
  const apiClient = useApiClient()
@@ -52,11 +52,11 @@ export const useApiPaginator = <T extends ResourceObject>(
52
52
  const pageParmWhere = pageParam?.where || {}
53
53
  const argsWhere = args.data.where || {}
54
54
  const where = { ...argsWhere, ...pageParmWhere }
55
-
55
+ const app = args.app || 'chat'
56
56
  const offset = pageParam?.offset || args.data.offset
57
57
  const data = { ...args.data, where, offset }
58
58
 
59
- return apiClient.chat.get({
59
+ return apiClient[app].get({
60
60
  url: args.url,
61
61
  data,
62
62
  })
@@ -71,10 +71,11 @@ export const useApiPaginator = <T extends ResourceObject>(
71
71
 
72
72
  return undefined
73
73
  },
74
+ enabled: args.enabled,
74
75
  ...(opts || {}),
75
76
  })
76
77
 
77
- const data: T[] = query.data?.pages.flatMap(page => page.data) || []
78
+ const data: T[] = query.data?.pages?.flatMap(page => page.data) || []
78
79
 
79
80
  return { ...query, data }
80
81
  }
@@ -2,7 +2,7 @@ import { useContext, useMemo } from 'react'
2
2
  import { ChatContext } from '../contexts/chat_context'
3
3
  import { Client } from '../utils/client'
4
4
 
5
- type App = 'chat' | 'groups' | 'services'
5
+ export type App = 'chat' | 'groups' | 'services'
6
6
  const apps: App[] = ['chat', 'groups', 'services']
7
7
 
8
8
  export type ApiClient = { [_K in App]: Client }
@@ -4,7 +4,7 @@ import { ApiResource, ConversationResource } from '../types'
4
4
  import { transformGetToPost } from '../utils/client/request_helpers'
5
5
  import { useApiClient } from './use_api_client'
6
6
  import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
7
- import { getConversationsRequestArgs } from './use_conversations'
7
+ import { getConversationsRequestArgs } from '../utils/request/conversation'
8
8
 
9
9
  export const getConversationRequestArgs = ({ conversation_id }: { conversation_id: number }) => ({
10
10
  url: `/me/conversations/${conversation_id}`,