@planningcenter/chat-react-native 3.39.0-rc.0 → 3.39.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/build/hooks/direct_messages/use_direct_message_affiliations.d.ts +5 -0
  2. package/build/hooks/direct_messages/use_direct_message_affiliations.d.ts.map +1 -0
  3. package/build/hooks/direct_messages/use_direct_message_affiliations.js +20 -0
  4. package/build/hooks/direct_messages/use_direct_message_affiliations.js.map +1 -0
  5. package/build/hooks/direct_messages/use_direct_message_candidates.d.ts +4 -0
  6. package/build/hooks/direct_messages/use_direct_message_candidates.d.ts.map +1 -0
  7. package/build/hooks/direct_messages/use_direct_message_candidates.js +28 -0
  8. package/build/hooks/direct_messages/use_direct_message_candidates.js.map +1 -0
  9. package/build/hooks/direct_messages/use_direct_message_conversation_validate.d.ts +11 -0
  10. package/build/hooks/direct_messages/use_direct_message_conversation_validate.d.ts.map +1 -0
  11. package/build/hooks/direct_messages/use_direct_message_conversation_validate.js +25 -0
  12. package/build/hooks/direct_messages/use_direct_message_conversation_validate.js.map +1 -0
  13. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.d.ts +13 -0
  14. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.d.ts.map +1 -0
  15. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.js +51 -0
  16. package/build/hooks/direct_messages/use_find_or_create_direct_message_conversation.js.map +1 -0
  17. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts +1 -2
  18. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.d.ts.map +1 -1
  19. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js +244 -8
  20. package/build/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.js.map +1 -1
  21. package/build/types/resources/direct_message_affiliations.d.ts +8 -0
  22. package/build/types/resources/direct_message_affiliations.d.ts.map +1 -0
  23. package/build/types/resources/direct_message_affiliations.js +2 -0
  24. package/build/types/resources/direct_message_affiliations.js.map +1 -0
  25. package/build/types/resources/direct_message_candidate.d.ts +12 -0
  26. package/build/types/resources/direct_message_candidate.d.ts.map +1 -0
  27. package/build/types/resources/direct_message_candidate.js +2 -0
  28. package/build/types/resources/direct_message_candidate.js.map +1 -0
  29. package/build/types/resources/index.d.ts +2 -0
  30. package/build/types/resources/index.d.ts.map +1 -1
  31. package/build/types/resources/index.js +2 -0
  32. package/build/types/resources/index.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/hooks/direct_messages/use_direct_message_affiliations.ts +39 -0
  35. package/src/hooks/direct_messages/use_direct_message_candidates.ts +31 -0
  36. package/src/hooks/direct_messages/use_direct_message_conversation_validate.ts +35 -0
  37. package/src/hooks/direct_messages/use_find_or_create_direct_message_conversation.ts +66 -0
  38. package/src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx +386 -8
  39. package/src/types/resources/direct_message_affiliations.ts +8 -0
  40. package/src/types/resources/direct_message_candidate.ts +12 -0
  41. package/src/types/resources/index.ts +2 -0
@@ -1,4 +1,6 @@
1
1
  export * from './analytics_metadata';
2
+ export * from './direct_message_affiliations';
3
+ export * from './direct_message_candidate';
2
4
  export * from './conversation';
3
5
  export * from './conversation_membership';
4
6
  export * from './conversation_validate';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA","sourcesContent":["export * from './analytics_metadata'\nexport * from './conversation'\nexport * from './conversation_membership'\nexport * from './conversation_validate'\nexport * from './member'\nexport * from './message'\nexport * from './oauth_token'\nexport * from './person'\nexport * from './groups'\nexport * from './app_grant'\nexport * from './services'\nexport * from './organization'\nexport * from './group_membership'\nexport * from './group_resource'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA","sourcesContent":["export * from './analytics_metadata'\nexport * from './direct_message_affiliations'\nexport * from './direct_message_candidate'\nexport * from './conversation'\nexport * from './conversation_membership'\nexport * from './conversation_validate'\nexport * from './member'\nexport * from './message'\nexport * from './oauth_token'\nexport * from './person'\nexport * from './groups'\nexport * from './app_grant'\nexport * from './services'\nexport * from './organization'\nexport * from './group_membership'\nexport * from './group_resource'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.39.0-rc.0",
3
+ "version": "3.39.0-rc.2",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "react-native": "./src/index.tsx",
@@ -72,5 +72,5 @@
72
72
  "react-native-url-polyfill": "^2.0.0",
73
73
  "typescript": "~5.9.2"
74
74
  },
75
- "gitHead": "698101c907dc57af0ca09aa2427328ab23a28431"
75
+ "gitHead": "2008d809d6e5be72841b9c1b015ee18c84c66a05"
76
76
  }
@@ -0,0 +1,39 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useMemo } from 'react'
3
+ import { ApiResource } from '../../types'
4
+ import { DirectMessageAffiliationsResource } from '../../types/resources/direct_message_affiliations'
5
+ import { useApiClient } from '../use_api_client'
6
+
7
+ export function useDirectMessageAffiliations() {
8
+ const apiClient = useApiClient()
9
+
10
+ const { data } = useQuery({
11
+ queryKey: ['chat', '/me/direct_message_affiliations'],
12
+ queryFn: () =>
13
+ apiClient.chat.get({
14
+ url: '/me/direct_message_affiliations',
15
+ data: {
16
+ fields: { DirectMessageAffiliations: ['groups', 'teams'] },
17
+ },
18
+ }) as Promise<ApiResource<DirectMessageAffiliationsResource>>,
19
+ staleTime: 5 * 60 * 1000,
20
+ })
21
+
22
+ const groups = useMemo(
23
+ () =>
24
+ new Map<number, string>(
25
+ Object.entries(data?.data?.groups ?? {}).map(([id, name]) => [+id, name])
26
+ ),
27
+ [data]
28
+ )
29
+
30
+ const teams = useMemo(
31
+ () =>
32
+ new Map<number, string>(
33
+ Object.entries(data?.data?.teams ?? {}).map(([id, name]) => [+id, name])
34
+ ),
35
+ [data]
36
+ )
37
+
38
+ return { groups, teams }
39
+ }
@@ -0,0 +1,31 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { ApiCollection } from '../../types'
3
+ import { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'
4
+ import { useApiClient } from '../use_api_client'
5
+
6
+ export function useDirectMessageCandidates(term: string) {
7
+ const apiClient = useApiClient()
8
+ return useQuery({
9
+ queryKey: ['chat', '/me/direct_message_candidates', term],
10
+ queryFn: () =>
11
+ apiClient.chat.get({
12
+ url: '/me/direct_message_candidates',
13
+ data: {
14
+ q: term,
15
+ fields: {
16
+ DirectMessageCandidate: [
17
+ 'name',
18
+ 'avatar',
19
+ 'groups',
20
+ 'teams',
21
+ 'under_18',
22
+ 'direct_messages_disabled',
23
+ ],
24
+ },
25
+ },
26
+ }) as Promise<ApiCollection<DirectMessageCandidateResource>>,
27
+ enabled: term.length >= 1,
28
+ staleTime: 1000 * 60,
29
+ throwOnError: true,
30
+ })
31
+ }
@@ -0,0 +1,35 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { ApiResource, ConversationValidateResource, ConversationWarning } from '../../types'
3
+ import { useApiClient } from '../use_api_client'
4
+
5
+ interface Props {
6
+ recipientIds: string[]
7
+ enabled?: boolean
8
+ }
9
+
10
+ export function useDirectMessageConversationValidate({ recipientIds, enabled = true }: Props) {
11
+ const apiClient = useApiClient()
12
+
13
+ const { data, isLoading } = useQuery({
14
+ queryKey: ['chat', '/me/conversation_validate', { recipient_ids: recipientIds }],
15
+ queryFn: () =>
16
+ apiClient.chat.post<ApiResource<ConversationValidateResource>>({
17
+ url: '/me/conversation_validate',
18
+ data: {
19
+ data: {
20
+ type: 'ConversationValidate',
21
+ attributes: { recipient_ids: recipientIds },
22
+ },
23
+ },
24
+ }),
25
+ enabled: enabled && recipientIds.length > 0,
26
+ retry: false,
27
+ })
28
+
29
+ const warnings: ConversationWarning[] = data?.data.warnings ?? []
30
+
31
+ return {
32
+ warnings,
33
+ validationPending: enabled && recipientIds.length > 0 && isLoading,
34
+ }
35
+ }
@@ -0,0 +1,66 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useState, useCallback } from 'react'
3
+ import { ApiCollection, ApiResource, ConversationResource } from '../../types'
4
+ import { ApiClient, useApiClient } from '../use_api_client'
5
+
6
+ const findDirectMessageConversation = (apiClient: ApiClient, participantIds: string[]) =>
7
+ apiClient.chat.get({
8
+ url: '/me/conversations',
9
+ data: {
10
+ fields: { Conversation: ['title'] },
11
+ filter: 'with_exact_direct_message_participants',
12
+ participant_ids: participantIds.join(','),
13
+ },
14
+ }) as Promise<ApiCollection<ConversationResource>>
15
+
16
+ const createDirectMessageConversation = (apiClient: ApiClient, ids: string[]) =>
17
+ apiClient.chat.post({
18
+ url: '/me/direct_messages',
19
+ data: {
20
+ data: {
21
+ type: 'DirectMessage',
22
+ attributes: { recipients: ids },
23
+ },
24
+ },
25
+ }) as Promise<ApiResource<ConversationResource>>
26
+
27
+ interface Props {
28
+ recipientIds: string[]
29
+ onSuccess: (conversationId: ConversationResource['id']) => void
30
+ }
31
+
32
+ export function useFindOrCreateDirectMessageConversation({ recipientIds, onSuccess }: Props) {
33
+ const apiClient = useApiClient()
34
+ const [isFinding, setIsFinding] = useState(false)
35
+ const [isError, setIsError] = useState(false)
36
+
37
+ const { data } = useQuery({
38
+ queryKey: ['chat', '/me/conversations', 'with_exact_direct_message_participants', recipientIds],
39
+ queryFn: () => findDirectMessageConversation(apiClient, recipientIds),
40
+ enabled: recipientIds.length > 0,
41
+ })
42
+
43
+ const directMessageExists = (data?.data.length ?? 0) > 0
44
+
45
+ const findOrCreateConversation = useCallback(
46
+ async (participantIds: string[]) => {
47
+ setIsFinding(true)
48
+ setIsError(false)
49
+ try {
50
+ const findResponse = await findDirectMessageConversation(apiClient, participantIds)
51
+ if (findResponse.data.length > 0) {
52
+ onSuccess(findResponse.data[0].id)
53
+ } else {
54
+ const createResponse = await createDirectMessageConversation(apiClient, participantIds)
55
+ onSuccess(createResponse.data.id)
56
+ }
57
+ } catch {
58
+ setIsError(true)
59
+ setIsFinding(false)
60
+ }
61
+ },
62
+ [apiClient, onSuccess]
63
+ )
64
+
65
+ return { findOrCreateConversation, directMessageExists, isFinding, isError }
66
+ }
@@ -1,33 +1,411 @@
1
- import React from 'react'
2
- import { StyleSheet, View } from 'react-native'
1
+ import { PlatformPressable } from '@react-navigation/elements'
2
+ import { useEffect, useState } from 'react'
3
+ import { FlatList, Pressable, StyleSheet, TextInput, View } from 'react-native'
3
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
- import { Heading, Text } from '../../components'
5
+ import { Avatar, Banner, Icon, Spinner, Text } from '../../components'
6
+ import { ComponentErrorBoundary } from '../../components/page/component_error_boundary'
7
+ import { useTheme } from '../../hooks'
8
+ import { useDirectMessageCandidates } from '../../hooks/direct_messages/use_direct_message_candidates'
9
+ import { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'
5
10
  import { ConversationSelectRecipientsScreenProps } from './types/screen_props'
6
11
 
12
+ const MAX_RECIPIENTS = 9
13
+
7
14
  export function ConversationSelectDirectMessageRecipientsScreen(
8
15
  _props: ConversationSelectRecipientsScreenProps
9
16
  ) {
10
17
  const styles = useStyles()
18
+ const [searchTerm, setSearchTerm] = useState('')
19
+ const [debouncedTerm, setDebouncedTerm] = useState('')
20
+ const [selectedPeople, setSelectedPeople] = useState<Map<string, DirectMessageCandidateResource>>(
21
+ new Map()
22
+ )
23
+
24
+ useEffect(() => {
25
+ const timer = setTimeout(() => setDebouncedTerm(searchTerm), 200)
26
+ return () => clearTimeout(timer)
27
+ }, [searchTerm])
28
+
29
+ const selectPerson = (person: DirectMessageCandidateResource) => {
30
+ if (selectedPeople.size >= MAX_RECIPIENTS) return
31
+ setSelectedPeople(prev => new Map(prev).set(person.id, person))
32
+ setSearchTerm('')
33
+ setDebouncedTerm('')
34
+ }
35
+
36
+ const removePerson = (id: string) => {
37
+ setSelectedPeople(prev => {
38
+ const next = new Map(prev)
39
+ next.delete(id)
40
+ return next
41
+ })
42
+ }
11
43
 
12
44
  return (
13
45
  <View style={styles.container}>
14
- <Heading variant="h2">New direct message</Heading>
15
- <Text variant="secondary">Coming soon</Text>
46
+ <ToField
47
+ selectedPeople={selectedPeople}
48
+ searchTerm={searchTerm}
49
+ onSearchChange={setSearchTerm}
50
+ onRemove={removePerson}
51
+ onClear={() => setSelectedPeople(new Map())}
52
+ />
53
+ <ComponentErrorBoundary
54
+ key={debouncedTerm}
55
+ fallback={
56
+ <Banner
57
+ appearance="error"
58
+ description="Couldn't load people right now. Try again."
59
+ style={styles.errorBanner}
60
+ />
61
+ }
62
+ >
63
+ <CandidateResults
64
+ term={debouncedTerm}
65
+ selectedPeople={selectedPeople}
66
+ onSelect={selectPerson}
67
+ />
68
+ </ComponentErrorBoundary>
69
+ </View>
70
+ )
71
+ }
72
+
73
+ function ToField({
74
+ selectedPeople,
75
+ searchTerm,
76
+ onSearchChange,
77
+ onRemove,
78
+ onClear,
79
+ }: {
80
+ selectedPeople: Map<string, DirectMessageCandidateResource>
81
+ searchTerm: string
82
+ onSearchChange: (term: string) => void
83
+ onRemove: (id: string) => void
84
+ onClear: () => void
85
+ }) {
86
+ const styles = useStyles()
87
+ const { colors } = useTheme()
88
+ const hasSelections = selectedPeople.size > 0
89
+
90
+ return (
91
+ <View style={styles.toField}>
92
+ <View style={styles.toFieldHeader}>
93
+ <Text style={styles.toLabel}>To</Text>
94
+ {hasSelections && (
95
+ <Pressable onPress={onClear} hitSlop={8}>
96
+ <Text style={styles.clearButton}>Clear</Text>
97
+ </Pressable>
98
+ )}
99
+ </View>
100
+ <View style={styles.toInputArea}>
101
+ {[...selectedPeople.values()].map(person => (
102
+ <PersonChip key={person.id} person={person} onRemove={onRemove} />
103
+ ))}
104
+ <TextInput
105
+ autoFocus
106
+ placeholder="Search"
107
+ placeholderTextColor={colors.textColorDefaultPlaceholder}
108
+ value={searchTerm}
109
+ onChangeText={onSearchChange}
110
+ style={styles.toTextInput}
111
+ returnKeyType="search"
112
+ />
113
+ </View>
114
+ </View>
115
+ )
116
+ }
117
+
118
+ function PersonChip({
119
+ person,
120
+ onRemove,
121
+ }: {
122
+ person: DirectMessageCandidateResource
123
+ onRemove: (id: string) => void
124
+ }) {
125
+ const styles = useStyles()
126
+
127
+ return (
128
+ <View style={styles.chip}>
129
+ <Avatar sourceUri={person.avatar} size="sm" showFallback />
130
+ <Text style={styles.chipName}>{person.name}</Text>
131
+ <Pressable
132
+ onPress={() => onRemove(person.id)}
133
+ hitSlop={8}
134
+ accessibilityLabel={`Remove ${person.name}`}
135
+ >
136
+ <Icon name="general.x" size={12} style={styles.chipRemoveIcon} />
137
+ </Pressable>
16
138
  </View>
17
139
  )
18
140
  }
19
141
 
142
+ function CandidateResults({
143
+ term,
144
+ selectedPeople,
145
+ onSelect,
146
+ }: {
147
+ term: string
148
+ selectedPeople: Map<string, DirectMessageCandidateResource>
149
+ onSelect: (candidate: DirectMessageCandidateResource) => void
150
+ }) {
151
+ const styles = useStyles()
152
+ const { data, isFetching } = useDirectMessageCandidates(term)
153
+
154
+ if (term.length < 1) {
155
+ if (selectedPeople.size > 0) return null
156
+
157
+ return (
158
+ <View style={styles.emptyState}>
159
+ <Icon name="churchCenter.people" size={40} style={styles.emptyStateIcon} />
160
+ <Text style={styles.emptyStateTitle}>Search for people to direct message</Text>
161
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
162
+ Start a conversation with people in your groups and teams.
163
+ </Text>
164
+ </View>
165
+ )
166
+ }
167
+
168
+ if (selectedPeople.size >= MAX_RECIPIENTS) {
169
+ return (
170
+ <View style={styles.emptyState}>
171
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
172
+ You can add up to {MAX_RECIPIENTS} people to a direct message.
173
+ </Text>
174
+ </View>
175
+ )
176
+ }
177
+
178
+ const unselectedCandidates = (data?.data ?? []).filter(c => !selectedPeople.has(c.id))
179
+
180
+ if (isFetching && unselectedCandidates.length === 0) {
181
+ return (
182
+ <View style={styles.emptyState}>
183
+ <Spinner />
184
+ </View>
185
+ )
186
+ }
187
+
188
+ if (!isFetching && unselectedCandidates.length === 0) {
189
+ return (
190
+ <View style={styles.emptyState}>
191
+ <Icon name="general.search" size={32} style={styles.emptyStateIcon} />
192
+ <Text style={styles.emptyStateTitle}>No matches for "{term}"</Text>
193
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
194
+ Try a different name, or check that you share a group or team with them.
195
+ </Text>
196
+ </View>
197
+ )
198
+ }
199
+
200
+ const hasTruncated = (data?.meta.totalCount ?? 0) > (data?.data.length ?? 0)
201
+
202
+ return (
203
+ <CandidateList
204
+ candidates={unselectedCandidates}
205
+ hasTruncated={hasTruncated}
206
+ onSelect={onSelect}
207
+ />
208
+ )
209
+ }
210
+
211
+ function CandidateList({
212
+ candidates,
213
+ hasTruncated,
214
+ onSelect,
215
+ }: {
216
+ candidates: DirectMessageCandidateResource[]
217
+ hasTruncated: boolean
218
+ onSelect: (candidate: DirectMessageCandidateResource) => void
219
+ }) {
220
+ const styles = useStyles()
221
+ const available = candidates.filter(c => !c.directMessagesDisabled)
222
+ const dmDisabled = candidates.filter(c => c.directMessagesDisabled)
223
+
224
+ return (
225
+ <FlatList<DirectMessageCandidateResource>
226
+ data={available}
227
+ keyExtractor={item => item.id}
228
+ renderItem={({ item }) => <CandidateItem candidate={item} onPress={() => onSelect(item)} />}
229
+ ListHeaderComponent={
230
+ <View style={styles.resultsHeader}>
231
+ <Text style={styles.resultsHeading}>Results</Text>
232
+ </View>
233
+ }
234
+ ListFooterComponent={
235
+ <>
236
+ {dmDisabled.length > 0 && (
237
+ <>
238
+ <View style={styles.unavailableHeader}>
239
+ <Text variant="secondary" style={styles.unavailableLabel}>
240
+ Unavailable for direct messages
241
+ </Text>
242
+ </View>
243
+ {dmDisabled.map(candidate => (
244
+ <CandidateItem key={candidate.id} candidate={candidate} disabled />
245
+ ))}
246
+ </>
247
+ )}
248
+ {hasTruncated && (
249
+ <View style={styles.truncatedFooter}>
250
+ <Text variant="secondary" style={styles.truncatedText}>
251
+ Keep typing to narrow your results.
252
+ </Text>
253
+ </View>
254
+ )}
255
+ </>
256
+ }
257
+ />
258
+ )
259
+ }
260
+
261
+ function CandidateItem({
262
+ candidate,
263
+ onPress,
264
+ disabled = false,
265
+ }: {
266
+ candidate: DirectMessageCandidateResource
267
+ onPress?: () => void
268
+ disabled?: boolean
269
+ }) {
270
+ const styles = useStyles()
271
+
272
+ return (
273
+ <PlatformPressable
274
+ onPress={disabled ? undefined : onPress}
275
+ accessibilityRole="button"
276
+ accessibilityLabel={`Select ${candidate.name}`}
277
+ accessibilityState={{ disabled }}
278
+ >
279
+ <View style={[styles.row, disabled && styles.candidateDisabled]}>
280
+ <Avatar sourceUri={candidate.avatar} size="lg" showFallback />
281
+ <Text style={styles.rowName}>{candidate.name}</Text>
282
+ </View>
283
+ </PlatformPressable>
284
+ )
285
+ }
286
+
20
287
  const useStyles = () => {
21
288
  const { bottom } = useSafeAreaInsets()
289
+ const { colors } = useTheme()
22
290
 
23
291
  return StyleSheet.create({
24
292
  container: {
25
293
  flex: 1,
26
- padding: 16,
27
- paddingBottom: 16 + bottom,
294
+ paddingBottom: bottom,
295
+ },
296
+ toField: {
297
+ paddingHorizontal: 16,
298
+ paddingTop: 16,
299
+ paddingBottom: 8,
300
+ },
301
+ toFieldHeader: {
302
+ flexDirection: 'row',
303
+ justifyContent: 'space-between',
304
+ alignItems: 'center',
305
+ marginBottom: 8,
306
+ },
307
+ toLabel: {
308
+ fontWeight: '600',
309
+ color: colors.textColorDefaultSecondary,
310
+ },
311
+ clearButton: {
312
+ color: colors.interaction,
313
+ },
314
+ toInputArea: {
315
+ flexDirection: 'row',
316
+ flexWrap: 'wrap',
317
+ alignItems: 'center',
318
+ gap: 6,
319
+ borderWidth: 1,
320
+ borderColor: colors.borderColorDefaultBase,
321
+ borderRadius: 8,
322
+ paddingHorizontal: 10,
323
+ paddingVertical: 8,
324
+ },
325
+ toTextInput: {
326
+ flex: 1,
327
+ minWidth: 80,
328
+ color: colors.textColorDefaultPrimary,
329
+ fontSize: 16,
330
+ paddingVertical: 2,
331
+ },
332
+ chip: {
333
+ flexDirection: 'row',
334
+ alignItems: 'center',
335
+ gap: 4,
336
+ borderWidth: 1,
337
+ borderColor: colors.borderColorDefaultBase,
338
+ borderRadius: 100,
339
+ paddingLeft: 4,
340
+ paddingRight: 6,
341
+ paddingVertical: 3,
342
+ },
343
+ chipName: {
344
+ fontSize: 14,
345
+ },
346
+ chipRemoveIcon: {
347
+ color: colors.iconColorDefaultSecondary,
348
+ },
349
+ errorBanner: {
350
+ margin: 16,
351
+ },
352
+ emptyState: {
28
353
  alignItems: 'center',
29
- justifyContent: 'center',
354
+ paddingTop: 32,
355
+ paddingHorizontal: 32,
30
356
  gap: 8,
31
357
  },
358
+ emptyStateIcon: {
359
+ color: colors.iconColorDefaultSecondary,
360
+ marginBottom: 4,
361
+ },
362
+ emptyStateTitle: {
363
+ fontWeight: '600',
364
+ textAlign: 'center',
365
+ },
366
+ emptyStateSubtitle: {
367
+ textAlign: 'center',
368
+ fontSize: 14,
369
+ },
370
+ row: {
371
+ flexDirection: 'row',
372
+ alignItems: 'center',
373
+ gap: 12,
374
+ paddingHorizontal: 16,
375
+ paddingVertical: 12,
376
+ borderBottomWidth: 1,
377
+ borderColor: colors.fillColorNeutral050Base,
378
+ },
379
+ rowName: {
380
+ flex: 1,
381
+ },
382
+ candidateDisabled: {
383
+ opacity: 0.5,
384
+ },
385
+ resultsHeader: {
386
+ paddingHorizontal: 16,
387
+ paddingTop: 16,
388
+ paddingBottom: 8,
389
+ },
390
+ resultsHeading: {
391
+ fontWeight: '600',
392
+ },
393
+ unavailableHeader: {
394
+ paddingHorizontal: 16,
395
+ paddingTop: 16,
396
+ paddingBottom: 8,
397
+ },
398
+ unavailableLabel: {
399
+ fontSize: 13,
400
+ },
401
+ truncatedFooter: {
402
+ paddingHorizontal: 16,
403
+ paddingTop: 12,
404
+ paddingBottom: 8,
405
+ },
406
+ truncatedText: {
407
+ fontSize: 13,
408
+ textAlign: 'center',
409
+ },
32
410
  })
33
411
  }
@@ -0,0 +1,8 @@
1
+ import { ResourceObject } from '../api_primitives'
2
+
3
+ export interface DirectMessageAffiliationsResource extends ResourceObject {
4
+ type: 'DirectMessageAffiliations'
5
+ id: string
6
+ groups: Record<string, string>
7
+ teams: Record<string, string>
8
+ }
@@ -0,0 +1,12 @@
1
+ import { ResourceObject } from '../api_primitives'
2
+
3
+ export interface DirectMessageCandidateResource extends ResourceObject {
4
+ type: 'DirectMessageCandidate'
5
+ id: string
6
+ name: string
7
+ avatar: string
8
+ groups: number[]
9
+ teams: number[]
10
+ under_18: boolean
11
+ directMessagesDisabled: boolean
12
+ }
@@ -1,4 +1,6 @@
1
1
  export * from './analytics_metadata'
2
+ export * from './direct_message_affiliations'
3
+ export * from './direct_message_candidate'
2
4
  export * from './conversation'
3
5
  export * from './conversation_membership'
4
6
  export * from './conversation_validate'