@planningcenter/chat-react-native 3.38.1 → 3.38.2-qa-778.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 (220) hide show
  1. package/build/components/conversation/messages_disabled_banners.d.ts +6 -1
  2. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -1
  3. package/build/components/conversation/messages_disabled_banners.js +8 -2
  4. package/build/components/conversation/messages_disabled_banners.js.map +1 -1
  5. package/build/components/primitive/form_sheet.d.ts +1 -0
  6. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  7. package/build/components/primitive/form_sheet.js +17 -7
  8. package/build/components/primitive/form_sheet.js.map +1 -1
  9. package/build/contexts/api_provider.js +2 -1
  10. package/build/contexts/api_provider.js.map +1 -1
  11. package/build/contexts/conversations_context.d.ts.map +1 -1
  12. package/build/contexts/conversations_context.js +6 -2
  13. package/build/contexts/conversations_context.js.map +1 -1
  14. package/build/hooks/direct_messages/use_direct_message_affiliations.d.ts +5 -0
  15. package/build/hooks/direct_messages/use_direct_message_affiliations.d.ts.map +1 -0
  16. package/build/hooks/direct_messages/use_direct_message_affiliations.js +20 -0
  17. package/build/hooks/direct_messages/use_direct_message_affiliations.js.map +1 -0
  18. package/build/hooks/direct_messages/use_direct_message_candidates.d.ts +4 -0
  19. package/build/hooks/direct_messages/use_direct_message_candidates.d.ts.map +1 -0
  20. package/build/hooks/direct_messages/use_direct_message_candidates.js +28 -0
  21. package/build/hooks/direct_messages/use_direct_message_candidates.js.map +1 -0
  22. package/build/hooks/direct_messages/use_direct_message_conversation_validate.d.ts +11 -0
  23. package/build/hooks/direct_messages/use_direct_message_conversation_validate.d.ts.map +1 -0
  24. package/build/hooks/direct_messages/use_direct_message_conversation_validate.js +25 -0
  25. package/build/hooks/direct_messages/use_direct_message_conversation_validate.js.map +1 -0
  26. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.d.ts +13 -0
  27. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.d.ts.map +1 -0
  28. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.js +51 -0
  29. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.js.map +1 -0
  30. package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -1
  31. package/build/hooks/groups/use_group_chat_conversation_payload.js +2 -1
  32. package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -1
  33. package/build/hooks/index.d.ts +2 -0
  34. package/build/hooks/index.d.ts.map +1 -1
  35. package/build/hooks/index.js +2 -0
  36. package/build/hooks/index.js.map +1 -1
  37. package/build/hooks/services/use_services_chat_conversation_payload.d.ts.map +1 -1
  38. package/build/hooks/services/use_services_chat_conversation_payload.js +2 -1
  39. package/build/hooks/services/use_services_chat_conversation_payload.js.map +1 -1
  40. package/build/hooks/use_api.d.ts +5 -1
  41. package/build/hooks/use_api.d.ts.map +1 -1
  42. package/build/hooks/use_api.js +2 -1
  43. package/build/hooks/use_api.js.map +1 -1
  44. package/build/hooks/use_chat_configuration.d.ts.map +1 -1
  45. package/build/hooks/use_chat_configuration.js +2 -1
  46. package/build/hooks/use_chat_configuration.js.map +1 -1
  47. package/build/hooks/use_chat_permissions.d.ts.map +1 -1
  48. package/build/hooks/use_chat_permissions.js +4 -1
  49. package/build/hooks/use_chat_permissions.js.map +1 -1
  50. package/build/hooks/use_conversation.d.ts.map +1 -1
  51. package/build/hooks/use_conversation.js +1 -0
  52. package/build/hooks/use_conversation.js.map +1 -1
  53. package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
  54. package/build/hooks/use_conversation_jolt_events.js +2 -2
  55. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  56. package/build/hooks/use_current_person.d.ts.map +1 -1
  57. package/build/hooks/use_current_person.js +5 -2
  58. package/build/hooks/use_current_person.js.map +1 -1
  59. package/build/hooks/use_direct_messages_enabled.d.ts +2 -0
  60. package/build/hooks/use_direct_messages_enabled.d.ts.map +1 -0
  61. package/build/hooks/use_direct_messages_enabled.js +16 -0
  62. package/build/hooks/use_direct_messages_enabled.js.map +1 -0
  63. package/build/hooks/use_features.d.ts +1 -0
  64. package/build/hooks/use_features.d.ts.map +1 -1
  65. package/build/hooks/use_features.js +3 -1
  66. package/build/hooks/use_features.js.map +1 -1
  67. package/build/hooks/use_groups.d.ts.map +1 -1
  68. package/build/hooks/use_groups.js +2 -1
  69. package/build/hooks/use_groups.js.map +1 -1
  70. package/build/hooks/use_groups_groups.d.ts.map +1 -1
  71. package/build/hooks/use_groups_groups.js +2 -1
  72. package/build/hooks/use_groups_groups.js.map +1 -1
  73. package/build/hooks/use_has_direct_messages.d.ts +2 -0
  74. package/build/hooks/use_has_direct_messages.d.ts.map +1 -0
  75. package/build/hooks/use_has_direct_messages.js +17 -0
  76. package/build/hooks/use_has_direct_messages.js.map +1 -0
  77. package/build/hooks/use_jolt.d.ts.map +1 -1
  78. package/build/hooks/use_jolt.js +2 -1
  79. package/build/hooks/use_jolt.js.map +1 -1
  80. package/build/hooks/use_organization.d.ts.map +1 -1
  81. package/build/hooks/use_organization.js +4 -1
  82. package/build/hooks/use_organization.js.map +1 -1
  83. package/build/hooks/use_person_settings.d.ts +6 -0
  84. package/build/hooks/use_person_settings.d.ts.map +1 -0
  85. package/build/hooks/use_person_settings.js +26 -0
  86. package/build/hooks/use_person_settings.js.map +1 -0
  87. package/build/navigation/index.d.ts +39 -0
  88. package/build/navigation/index.d.ts.map +1 -1
  89. package/build/navigation/index.js +19 -1
  90. package/build/navigation/index.js.map +1 -1
  91. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  92. package/build/screens/conversation_details_screen.js +2 -1
  93. package/build/screens/conversation_details_screen.js.map +1 -1
  94. package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -1
  95. package/build/screens/conversation_filters/components/conversation_filters.js +20 -5
  96. package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
  97. package/build/screens/conversation_filters/components/rows.d.ts +2 -1
  98. package/build/screens/conversation_filters/components/rows.d.ts.map +1 -1
  99. package/build/screens/conversation_filters/components/rows.js +2 -2
  100. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  101. package/build/screens/conversation_filters/context/conversation_filter_context.d.ts.map +1 -1
  102. package/build/screens/conversation_filters/context/conversation_filter_context.js +4 -0
  103. package/build/screens/conversation_filters/context/conversation_filter_context.js.map +1 -1
  104. package/build/screens/conversation_filters/filter_types.d.ts +1 -0
  105. package/build/screens/conversation_filters/filter_types.d.ts.map +1 -1
  106. package/build/screens/conversation_filters/filter_types.js +1 -0
  107. package/build/screens/conversation_filters/filter_types.js.map +1 -1
  108. package/build/screens/conversation_filters/screen_props.d.ts +1 -0
  109. package/build/screens/conversation_filters/screen_props.d.ts.map +1 -1
  110. package/build/screens/conversation_filters/screen_props.js.map +1 -1
  111. package/build/screens/conversation_screen.d.ts.map +1 -1
  112. package/build/screens/conversation_screen.js +1 -1
  113. package/build/screens/conversation_screen.js.map +1 -1
  114. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts +3 -0
  115. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts.map +1 -0
  116. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js +334 -0
  117. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -0
  118. package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
  119. package/build/screens/conversation_select_type_screen.js +11 -17
  120. package/build/screens/conversation_select_type_screen.js.map +1 -1
  121. package/build/screens/conversations/components/list_header_component.d.ts.map +1 -1
  122. package/build/screens/conversations/components/list_header_component.js +35 -17
  123. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  124. package/build/screens/conversations/conversations_screen.d.ts +1 -0
  125. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  126. package/build/screens/conversations/conversations_screen.js.map +1 -1
  127. package/build/screens/get_help_screen.d.ts.map +1 -1
  128. package/build/screens/get_help_screen.js +2 -18
  129. package/build/screens/get_help_screen.js.map +1 -1
  130. package/build/screens/index.d.ts +1 -0
  131. package/build/screens/index.d.ts.map +1 -1
  132. package/build/screens/index.js +1 -0
  133. package/build/screens/index.js.map +1 -1
  134. package/build/screens/settings_screen.d.ts +5 -0
  135. package/build/screens/settings_screen.d.ts.map +1 -0
  136. package/build/screens/settings_screen.js +56 -0
  137. package/build/screens/settings_screen.js.map +1 -0
  138. package/build/types/jolt_events/conversation_events.d.ts +2 -0
  139. package/build/types/jolt_events/conversation_events.d.ts.map +1 -1
  140. package/build/types/jolt_events/conversation_events.js.map +1 -1
  141. package/build/types/resources/conversation.d.ts +2 -0
  142. package/build/types/resources/conversation.d.ts.map +1 -1
  143. package/build/types/resources/conversation.js.map +1 -1
  144. package/build/types/resources/direct_message_affiliations.d.ts +8 -0
  145. package/build/types/resources/direct_message_affiliations.d.ts.map +1 -0
  146. package/build/types/resources/direct_message_affiliations.js +2 -0
  147. package/build/types/resources/direct_message_affiliations.js.map +1 -0
  148. package/build/types/resources/direct_message_candidate.d.ts +12 -0
  149. package/build/types/resources/direct_message_candidate.d.ts.map +1 -0
  150. package/build/types/resources/direct_message_candidate.js +2 -0
  151. package/build/types/resources/direct_message_candidate.js.map +1 -0
  152. package/build/types/resources/index.d.ts +2 -0
  153. package/build/types/resources/index.d.ts.map +1 -1
  154. package/build/types/resources/index.js +2 -0
  155. package/build/types/resources/index.js.map +1 -1
  156. package/build/types/resources/organization.d.ts +1 -0
  157. package/build/types/resources/organization.d.ts.map +1 -1
  158. package/build/types/resources/organization.js.map +1 -1
  159. package/build/types/resources/person.d.ts +2 -0
  160. package/build/types/resources/person.d.ts.map +1 -1
  161. package/build/types/resources/person.js.map +1 -1
  162. package/build/utils/request/conversation.d.ts +2 -1
  163. package/build/utils/request/conversation.d.ts.map +1 -1
  164. package/build/utils/request/conversation.js +15 -6
  165. package/build/utils/request/conversation.js.map +1 -1
  166. package/build/utils/time.d.ts +8 -0
  167. package/build/utils/time.d.ts.map +1 -0
  168. package/build/utils/time.js +15 -0
  169. package/build/utils/time.js.map +1 -0
  170. package/package.json +3 -3
  171. package/src/components/conversation/messages_disabled_banners.tsx +16 -2
  172. package/src/components/primitive/form_sheet.tsx +23 -6
  173. package/src/contexts/api_provider.tsx +4 -1
  174. package/src/contexts/conversations_context.tsx +6 -2
  175. package/src/hooks/direct_messages/use_direct_message_affiliations.ts +39 -0
  176. package/src/hooks/direct_messages/use_direct_message_candidates.ts +31 -0
  177. package/src/hooks/direct_messages/use_direct_message_conversation_validate.ts +35 -0
  178. package/src/hooks/direct_messages/use_find_or_create_direct_message_conversation.ts +66 -0
  179. package/src/hooks/groups/use_group_chat_conversation_payload.ts +2 -1
  180. package/src/hooks/index.ts +2 -0
  181. package/src/hooks/services/use_services_chat_conversation_payload.ts +2 -1
  182. package/src/hooks/use_api.ts +10 -1
  183. package/src/hooks/use_chat_configuration.ts +2 -1
  184. package/src/hooks/use_chat_permissions.ts +4 -1
  185. package/src/hooks/use_conversation.ts +1 -0
  186. package/src/hooks/use_conversation_jolt_events.ts +3 -2
  187. package/src/hooks/use_current_person.ts +5 -2
  188. package/src/hooks/use_direct_messages_enabled.ts +23 -0
  189. package/src/hooks/use_features.ts +3 -1
  190. package/src/hooks/use_groups.ts +16 -12
  191. package/src/hooks/use_groups_groups.ts +19 -15
  192. package/src/hooks/use_has_direct_messages.ts +21 -0
  193. package/src/hooks/use_jolt.ts +2 -1
  194. package/src/hooks/use_organization.ts +4 -1
  195. package/src/hooks/use_person_settings.ts +29 -0
  196. package/src/navigation/index.tsx +27 -1
  197. package/src/screens/conversation_details_screen.tsx +2 -1
  198. package/src/screens/conversation_filters/components/conversation_filters.tsx +25 -5
  199. package/src/screens/conversation_filters/components/rows.tsx +16 -3
  200. package/src/screens/conversation_filters/context/conversation_filter_context.tsx +4 -0
  201. package/src/screens/conversation_filters/filter_types.ts +1 -0
  202. package/src/screens/conversation_filters/screen_props.ts +1 -0
  203. package/src/screens/conversation_screen.tsx +2 -1
  204. package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +536 -0
  205. package/src/screens/conversation_select_type_screen.tsx +26 -17
  206. package/src/screens/conversations/components/list_header_component.tsx +84 -34
  207. package/src/screens/conversations/conversations_screen.tsx +1 -0
  208. package/src/screens/get_help_screen.tsx +2 -29
  209. package/src/screens/index.ts +1 -0
  210. package/src/screens/settings_screen.tsx +67 -0
  211. package/src/types/jolt_events/conversation_events.ts +2 -0
  212. package/src/types/resources/conversation.ts +3 -0
  213. package/src/types/resources/direct_message_affiliations.ts +8 -0
  214. package/src/types/resources/direct_message_candidate.ts +12 -0
  215. package/src/types/resources/index.ts +2 -0
  216. package/src/types/resources/organization.ts +1 -0
  217. package/src/types/resources/person.ts +2 -0
  218. package/src/utils/__tests__/conversation.test.ts +14 -0
  219. package/src/utils/request/conversation.ts +13 -5
  220. package/src/utils/time.ts +17 -0
@@ -0,0 +1,536 @@
1
+ import { PlatformPressable } from '@react-navigation/elements'
2
+ import { StackActions, useNavigation } from '@react-navigation/native'
3
+ import { useEffect, useState } from 'react'
4
+ import { FlatList, Pressable, StyleSheet, TextInput, View } from 'react-native'
5
+ import { Avatar, Badge, Banner, Icon, Spinner, Text } from '../../components'
6
+ import { ActionButton } from '../../components/display/action_button'
7
+ import { ComponentErrorBoundary } from '../../components/page/component_error_boundary'
8
+ import { useTheme } from '../../hooks'
9
+ import { useDirectMessageAffiliations } from '../../hooks/direct_messages/use_direct_message_affiliations'
10
+ import { useDirectMessageCandidates } from '../../hooks/direct_messages/use_direct_message_candidates'
11
+ import { useDirectMessageConversationValidate } from '../../hooks/direct_messages/use_direct_message_conversation_validate'
12
+ import { useFindOrCreateDirectMessageConversation } from '../../hooks/direct_messages/use_find_or_create_direct_message_conversation'
13
+ import { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'
14
+ import { ConversationSelectRecipientsScreenProps } from './types/screen_props'
15
+
16
+ const MAX_RECIPIENTS = 9
17
+
18
+ export function ConversationSelectDirectMessageRecipientsScreen(
19
+ _props: ConversationSelectRecipientsScreenProps
20
+ ) {
21
+ const styles = useStyles()
22
+ const navigation = useNavigation()
23
+ const [searchTerm, setSearchTerm] = useState('')
24
+ const [debouncedTerm, setDebouncedTerm] = useState('')
25
+ const [selectedPeople, setSelectedPeople] = useState<Map<string, DirectMessageCandidateResource>>(
26
+ new Map()
27
+ )
28
+
29
+ useEffect(() => {
30
+ const timer = setTimeout(() => setDebouncedTerm(searchTerm), 200)
31
+ return () => clearTimeout(timer)
32
+ }, [searchTerm])
33
+
34
+ const selectPerson = (person: DirectMessageCandidateResource) => {
35
+ if (selectedPeople.size >= MAX_RECIPIENTS) return
36
+ setSelectedPeople(prev => new Map(prev).set(person.id, person))
37
+ setSearchTerm('')
38
+ setDebouncedTerm('')
39
+ }
40
+
41
+ const removePerson = (id: string) => {
42
+ setSelectedPeople(prev => {
43
+ const next = new Map(prev)
44
+ next.delete(id)
45
+ return next
46
+ })
47
+ }
48
+
49
+ const recipientIds = [...selectedPeople.keys()]
50
+ const { warnings, validationPending } = useDirectMessageConversationValidate({ recipientIds })
51
+
52
+ const handleSuccess = (conversationId: number) => {
53
+ navigation.getParent()?.goBack()
54
+ navigation.dispatch(StackActions.push('Conversation', { conversation_id: conversationId }))
55
+ }
56
+
57
+ const { findOrCreateConversation, directMessageExists, isFinding, isError } =
58
+ useFindOrCreateDirectMessageConversation({ recipientIds, onSuccess: handleSuccess })
59
+
60
+ const buttonText = (() => {
61
+ if (selectedPeople.size === 0) return 'Start conversation'
62
+ if (selectedPeople.size === 1) {
63
+ const firstName = selectedPeople.values().next().value!.name.split(' ')[0]
64
+ return directMessageExists ? `Open message with ${firstName}` : `Message ${firstName}`
65
+ }
66
+ return directMessageExists
67
+ ? `Open direct message (${selectedPeople.size})`
68
+ : `Start direct message (${selectedPeople.size})`
69
+ })()
70
+
71
+ return (
72
+ <View style={styles.container}>
73
+ {isError && (
74
+ <Banner
75
+ appearance="error"
76
+ description="There was an issue starting the conversation. Please try again."
77
+ style={styles.errorBanner}
78
+ />
79
+ )}
80
+ <ToField
81
+ selectedPeople={selectedPeople}
82
+ searchTerm={searchTerm}
83
+ onSearchChange={setSearchTerm}
84
+ onRemove={removePerson}
85
+ onClear={() => setSelectedPeople(new Map())}
86
+ />
87
+ <View style={styles.candidateResultsContainer}>
88
+ <ComponentErrorBoundary
89
+ key={debouncedTerm}
90
+ fallback={
91
+ <Banner
92
+ appearance="error"
93
+ description="Couldn't load people right now. Try again."
94
+ style={styles.errorBanner}
95
+ />
96
+ }
97
+ >
98
+ <CandidateResults
99
+ term={debouncedTerm}
100
+ selectedPeople={selectedPeople}
101
+ onSelect={selectPerson}
102
+ />
103
+ </ComponentErrorBoundary>
104
+ </View>
105
+ <ActionButton
106
+ title={buttonText}
107
+ disabled={
108
+ selectedPeople.size === 0 || validationPending || warnings.length > 0 || isFinding
109
+ }
110
+ loading={validationPending || isFinding}
111
+ onPress={() => findOrCreateConversation(recipientIds)}
112
+ warnings={warnings}
113
+ />
114
+ </View>
115
+ )
116
+ }
117
+
118
+ function ToField({
119
+ selectedPeople,
120
+ searchTerm,
121
+ onSearchChange,
122
+ onRemove,
123
+ onClear,
124
+ }: {
125
+ selectedPeople: Map<string, DirectMessageCandidateResource>
126
+ searchTerm: string
127
+ onSearchChange: (term: string) => void
128
+ onRemove: (id: string) => void
129
+ onClear: () => void
130
+ }) {
131
+ const styles = useStyles()
132
+ const { colors } = useTheme()
133
+ const hasSelections = selectedPeople.size > 0
134
+
135
+ return (
136
+ <View style={styles.toField}>
137
+ <View style={styles.toFieldHeader}>
138
+ <Text style={styles.toLabel}>To</Text>
139
+ {hasSelections && (
140
+ <Pressable onPress={onClear} hitSlop={8}>
141
+ <Text style={styles.clearButton}>Clear</Text>
142
+ </Pressable>
143
+ )}
144
+ </View>
145
+ <View style={styles.toInputArea}>
146
+ {[...selectedPeople.values()].map(person => (
147
+ <PersonChip key={person.id} person={person} onRemove={onRemove} />
148
+ ))}
149
+ <TextInput
150
+ autoFocus
151
+ placeholder="Search"
152
+ placeholderTextColor={colors.textColorDefaultPlaceholder}
153
+ value={searchTerm}
154
+ onChangeText={onSearchChange}
155
+ style={styles.toTextInput}
156
+ returnKeyType="search"
157
+ />
158
+ </View>
159
+ </View>
160
+ )
161
+ }
162
+
163
+ function PersonChip({
164
+ person,
165
+ onRemove,
166
+ }: {
167
+ person: DirectMessageCandidateResource
168
+ onRemove: (id: string) => void
169
+ }) {
170
+ const styles = useStyles()
171
+
172
+ return (
173
+ <View style={styles.chip}>
174
+ <Avatar sourceUri={person.avatar} size="sm" showFallback />
175
+ <Text style={styles.chipName}>{person.name}</Text>
176
+ <Pressable
177
+ onPress={() => onRemove(person.id)}
178
+ hitSlop={8}
179
+ accessibilityLabel={`Remove ${person.name}`}
180
+ >
181
+ <Icon name="general.x" size={12} style={styles.chipRemoveIcon} />
182
+ </Pressable>
183
+ </View>
184
+ )
185
+ }
186
+
187
+ function CandidateResults({
188
+ term,
189
+ selectedPeople,
190
+ onSelect,
191
+ }: {
192
+ term: string
193
+ selectedPeople: Map<string, DirectMessageCandidateResource>
194
+ onSelect: (candidate: DirectMessageCandidateResource) => void
195
+ }) {
196
+ const styles = useStyles()
197
+ const { data, isFetching } = useDirectMessageCandidates(term)
198
+ const { groups, teams } = useDirectMessageAffiliations()
199
+
200
+ if (term.length < 1) {
201
+ if (selectedPeople.size > 0) return null
202
+
203
+ return (
204
+ <View style={styles.emptyState}>
205
+ <Icon name="churchCenter.people" size={40} style={styles.emptyStateIcon} />
206
+ <Text style={styles.emptyStateTitle}>Search for people to direct message</Text>
207
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
208
+ Start a conversation with people in your groups and teams.
209
+ </Text>
210
+ </View>
211
+ )
212
+ }
213
+
214
+ const isAtLimit = selectedPeople.size >= MAX_RECIPIENTS
215
+
216
+ if (isAtLimit) {
217
+ return (
218
+ <View style={styles.emptyState}>
219
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
220
+ You can add up to {MAX_RECIPIENTS} people to a direct message.
221
+ </Text>
222
+ </View>
223
+ )
224
+ }
225
+
226
+ const unselectedCandidates = (data?.data ?? []).filter(c => !selectedPeople.has(c.id))
227
+ const hasTruncated = (data?.meta.totalCount ?? 0) > (data?.data.length ?? 0)
228
+
229
+ if (isFetching && unselectedCandidates.length === 0) {
230
+ return (
231
+ <View style={styles.emptyState}>
232
+ <Spinner />
233
+ </View>
234
+ )
235
+ }
236
+
237
+ if (!isFetching && unselectedCandidates.length === 0) {
238
+ return (
239
+ <View style={styles.emptyState}>
240
+ <Icon name="general.search" size={32} style={styles.emptyStateIcon} />
241
+ <Text style={styles.emptyStateTitle}>No matches for "{term}"</Text>
242
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
243
+ Try a different name, or check that you share a group or team with them.
244
+ </Text>
245
+ </View>
246
+ )
247
+ }
248
+
249
+ return (
250
+ <CandidateList
251
+ candidates={unselectedCandidates}
252
+ hasTruncated={hasTruncated}
253
+ groups={groups}
254
+ teams={teams}
255
+ onSelect={onSelect}
256
+ />
257
+ )
258
+ }
259
+
260
+ function candidateBadges(candidate: DirectMessageCandidateResource) {
261
+ const badges = []
262
+ if (candidate.directMessagesDisabled) badges.push({ title: 'DMs off' })
263
+ if (candidate.under_18) badges.push({ title: 'Under 18' })
264
+ return badges
265
+ }
266
+
267
+ function candidateAffiliationNames(
268
+ candidate: DirectMessageCandidateResource,
269
+ groups: Map<number, string>,
270
+ teams: Map<number, string>
271
+ ) {
272
+ return [
273
+ ...candidate.groups.flatMap(id => {
274
+ const name = groups.get(id)
275
+ return name ? [name] : []
276
+ }),
277
+ ...candidate.teams.flatMap(id => {
278
+ const name = teams.get(id)
279
+ return name ? [name] : []
280
+ }),
281
+ ]
282
+ }
283
+
284
+ function CandidateList({
285
+ candidates,
286
+ hasTruncated,
287
+ groups,
288
+ teams,
289
+ onSelect,
290
+ }: {
291
+ candidates: DirectMessageCandidateResource[]
292
+ hasTruncated: boolean
293
+ groups: Map<number, string>
294
+ teams: Map<number, string>
295
+ onSelect: (candidate: DirectMessageCandidateResource) => void
296
+ }) {
297
+ const styles = useStyles()
298
+ const available = candidates.filter(c => !c.directMessagesDisabled)
299
+ const dmDisabled = candidates.filter(c => c.directMessagesDisabled)
300
+
301
+ return (
302
+ <FlatList<DirectMessageCandidateResource>
303
+ data={available}
304
+ keyExtractor={item => item.id}
305
+ renderItem={({ item }) => (
306
+ <CandidateItem
307
+ candidate={item}
308
+ badges={candidateBadges(item)}
309
+ affiliationNames={candidateAffiliationNames(item, groups, teams)}
310
+ onSelect={() => onSelect(item)}
311
+ />
312
+ )}
313
+ ListHeaderComponent={
314
+ <View style={styles.resultsHeader}>
315
+ <Text style={styles.resultsHeading}>Results</Text>
316
+ </View>
317
+ }
318
+ ListFooterComponent={
319
+ <>
320
+ {dmDisabled.length > 0 && (
321
+ <>
322
+ <View style={styles.unavailableHeader}>
323
+ <Text variant="secondary" style={styles.unavailableLabel}>
324
+ Unavailable for direct messages
325
+ </Text>
326
+ </View>
327
+ {dmDisabled.map(candidate => (
328
+ <CandidateItem
329
+ key={candidate.id}
330
+ candidate={candidate}
331
+ badges={candidateBadges(candidate)}
332
+ affiliationNames={candidateAffiliationNames(candidate, groups, teams)}
333
+ disabled
334
+ />
335
+ ))}
336
+ </>
337
+ )}
338
+ {hasTruncated && (
339
+ <View style={styles.truncatedFooter}>
340
+ <Text variant="secondary" style={styles.truncatedText}>
341
+ Keep typing to narrow your results.
342
+ </Text>
343
+ </View>
344
+ )}
345
+ </>
346
+ }
347
+ />
348
+ )
349
+ }
350
+
351
+ function CandidateItem({
352
+ candidate,
353
+ badges,
354
+ affiliationNames,
355
+ onSelect,
356
+ disabled = false,
357
+ }: {
358
+ candidate: DirectMessageCandidateResource
359
+ badges: { title: string }[]
360
+ affiliationNames: string[]
361
+ onSelect?: () => void
362
+ disabled?: boolean
363
+ }) {
364
+ const styles = useStyles()
365
+
366
+ return (
367
+ <PlatformPressable
368
+ onPress={disabled ? undefined : onSelect}
369
+ accessibilityRole="button"
370
+ accessibilityLabel={`Select ${candidate.name}`}
371
+ accessibilityState={{ disabled }}
372
+ >
373
+ <View style={[styles.row, disabled && styles.candidateDisabled]}>
374
+ <Avatar sourceUri={candidate.avatar} size="lg" showFallback />
375
+ <View style={styles.rowInfo}>
376
+ <Text style={styles.rowName}>{candidate.name}</Text>
377
+ {affiliationNames.length > 0 && (
378
+ <Text style={styles.rowAffiliations} numberOfLines={2}>
379
+ {affiliationNames.join(', ')}
380
+ </Text>
381
+ )}
382
+ {badges.length > 0 && (
383
+ <View style={styles.badgeRow}>
384
+ {badges.map(({ title }) => (
385
+ <Badge key={title} label={title} />
386
+ ))}
387
+ </View>
388
+ )}
389
+ </View>
390
+ </View>
391
+ </PlatformPressable>
392
+ )
393
+ }
394
+
395
+ const useStyles = () => {
396
+ const { colors } = useTheme()
397
+
398
+ return StyleSheet.create({
399
+ container: {
400
+ flex: 1,
401
+ },
402
+ candidateResultsContainer: {
403
+ flex: 1,
404
+ },
405
+ toField: {
406
+ paddingHorizontal: 16,
407
+ paddingTop: 16,
408
+ paddingBottom: 8,
409
+ },
410
+ toFieldHeader: {
411
+ flexDirection: 'row',
412
+ justifyContent: 'space-between',
413
+ alignItems: 'center',
414
+ marginBottom: 8,
415
+ },
416
+ toLabel: {
417
+ fontWeight: '600',
418
+ color: colors.textColorDefaultSecondary,
419
+ },
420
+ clearButton: {
421
+ color: colors.interaction,
422
+ },
423
+ toInputArea: {
424
+ flexDirection: 'row',
425
+ flexWrap: 'wrap',
426
+ alignItems: 'center',
427
+ gap: 6,
428
+ borderWidth: 1,
429
+ borderColor: colors.borderColorDefaultBase,
430
+ borderRadius: 8,
431
+ paddingHorizontal: 10,
432
+ paddingVertical: 8,
433
+ },
434
+ toTextInput: {
435
+ flex: 1,
436
+ minWidth: 80,
437
+ color: colors.textColorDefaultPrimary,
438
+ fontSize: 16,
439
+ paddingVertical: 2,
440
+ },
441
+ chip: {
442
+ flexDirection: 'row',
443
+ alignItems: 'center',
444
+ gap: 4,
445
+ borderWidth: 1,
446
+ borderColor: colors.borderColorDefaultBase,
447
+ borderRadius: 100,
448
+ paddingLeft: 4,
449
+ paddingRight: 6,
450
+ paddingVertical: 3,
451
+ },
452
+ chipName: {
453
+ fontSize: 14,
454
+ },
455
+ chipRemoveIcon: {
456
+ color: colors.iconColorDefaultSecondary,
457
+ },
458
+ errorBanner: {
459
+ margin: 16,
460
+ },
461
+ emptyState: {
462
+ alignItems: 'center',
463
+ paddingTop: 32,
464
+ paddingHorizontal: 32,
465
+ gap: 8,
466
+ },
467
+ emptyStateIcon: {
468
+ color: colors.iconColorDefaultSecondary,
469
+ marginBottom: 4,
470
+ },
471
+ emptyStateTitle: {
472
+ fontWeight: '600',
473
+ textAlign: 'center',
474
+ },
475
+ emptyStateSubtitle: {
476
+ textAlign: 'center',
477
+ fontSize: 14,
478
+ },
479
+ row: {
480
+ flexDirection: 'row',
481
+ alignItems: 'flex-start',
482
+ gap: 12,
483
+ paddingHorizontal: 16,
484
+ paddingVertical: 12,
485
+ borderBottomWidth: 1,
486
+ borderColor: colors.borderColorDefaultDim,
487
+ },
488
+ rowInfo: {
489
+ flex: 1,
490
+ gap: 2,
491
+ },
492
+ rowName: {
493
+ fontSize: 15,
494
+ fontWeight: '500',
495
+ },
496
+ rowAffiliations: {
497
+ fontSize: 12,
498
+ color: colors.textColorDefaultSecondary,
499
+ },
500
+ badgeRow: {
501
+ flexDirection: 'row',
502
+ flexWrap: 'wrap',
503
+ gap: 4,
504
+ marginTop: 4,
505
+ },
506
+
507
+ candidateDisabled: {
508
+ opacity: 0.5,
509
+ },
510
+ resultsHeader: {
511
+ paddingHorizontal: 16,
512
+ paddingTop: 16,
513
+ paddingBottom: 8,
514
+ },
515
+ resultsHeading: {
516
+ fontWeight: '600',
517
+ },
518
+ unavailableHeader: {
519
+ paddingHorizontal: 16,
520
+ paddingTop: 16,
521
+ paddingBottom: 8,
522
+ },
523
+ unavailableLabel: {
524
+ fontSize: 13,
525
+ },
526
+ truncatedFooter: {
527
+ paddingHorizontal: 16,
528
+ paddingTop: 12,
529
+ paddingBottom: 8,
530
+ },
531
+ truncatedText: {
532
+ fontSize: 13,
533
+ textAlign: 'center',
534
+ },
535
+ })
536
+ }
@@ -1,6 +1,8 @@
1
1
  import { StaticScreenProps, useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
3
  import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
4
+ import { useCurrentPerson } from '../hooks/use_current_person'
5
+ import { availableFeatures, useFeatures } from '../hooks/use_features'
4
6
  import { useNewConversationEntry } from '../hooks/use_new_conversation_entry'
5
7
  import type { NewConversationFromFilter } from '../hooks/use_new_conversation_from_filter'
6
8
  import { AppName } from '../types/resources/app_name'
@@ -17,15 +19,23 @@ export type ConversationSelectTypeScreenProps = StaticScreenProps<{
17
19
  newConversationFromFilter?: NewConversationFromFilter
18
20
  }>
19
21
 
22
+ type NewConversationTypeScreen =
23
+ | 'ConversationSelectGroupRecipients'
24
+ | 'ConversationSelectTeamsILeadRecipients'
25
+ | 'ConversationSelectDirectMessageRecipients'
26
+
20
27
  export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeScreenProps) {
21
28
  const navigation = useNavigation()
22
29
  const entryMode = useNewConversationEntry()
30
+ const { featureEnabled } = useFeatures()
31
+ const { directMessagesDisabled, under_18 } = useCurrentPerson()
23
32
  const { newConversationFromFilter, chat_group_graph_id, group_source_app_name } =
24
33
  route.params || {}
25
34
 
26
35
  const filterParams = { chat_group_graph_id, group_source_app_name }
27
36
  const canCreateGroups = entryMode === 'select_type' || entryMode === 'groups'
28
37
  const canCreateTeams = entryMode === 'select_type' || entryMode === 'teams'
38
+ const dmFeatureEnabled = featureEnabled(availableFeatures.direct_messages_eap)
29
39
 
30
40
  const handleFilterPress = () => {
31
41
  if (!newConversationFromFilter) return
@@ -55,27 +65,17 @@ export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeSc
55
65
  })
56
66
  }
57
67
 
58
- const handleGroupPress = () => {
68
+ const navigateToNewConversationType = (screen: NewConversationTypeScreen) => {
59
69
  Haptic.impactLight()
60
70
  navigation.goBack()
61
- requestAnimationFrame(() => {
62
- navigation.navigate('New', {
63
- screen: 'ConversationSelectGroupRecipients',
64
- params: filterParams,
65
- })
66
- })
71
+ requestAnimationFrame(() => navigation.navigate('New', { screen, params: filterParams }))
67
72
  }
68
73
 
69
- const handleTeamPress = () => {
70
- Haptic.impactLight()
71
- navigation.goBack()
72
- requestAnimationFrame(() => {
73
- navigation.navigate('New', {
74
- screen: 'ConversationSelectTeamsILeadRecipients',
75
- params: filterParams,
76
- })
77
- })
78
- }
74
+ const handleGroupPress = () => navigateToNewConversationType('ConversationSelectGroupRecipients')
75
+ const handleTeamPress = () =>
76
+ navigateToNewConversationType('ConversationSelectTeamsILeadRecipients')
77
+ const handleDirectMessagePress = () =>
78
+ navigateToNewConversationType('ConversationSelectDirectMessageRecipients')
79
79
 
80
80
  return (
81
81
  <FormSheet.Root>
@@ -103,6 +103,15 @@ export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeSc
103
103
  accessibilityHint="Opens team selection screen"
104
104
  />
105
105
  )}
106
+ {dmFeatureEnabled && !under_18 && (
107
+ <FormSheet.Action
108
+ title="Direct message"
109
+ onPress={handleDirectMessagePress}
110
+ disabled={directMessagesDisabled}
111
+ disabledReason={directMessagesDisabled ? 'Disabled in chat settings' : undefined}
112
+ accessibilityHint="Opens direct message recipient selection screen"
113
+ />
114
+ )}
106
115
  </FormSheet.Root>
107
116
  )
108
117
  }