@planningcenter/chat-react-native 3.39.0-rc.1 → 3.39.0-rc.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"use_direct_message_candidates.d.ts","sourceRoot":"","sources":["../../../src/hooks/direct_messages/use_direct_message_candidates.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,8BAA8B,EAAE,MAAM,gDAAgD,CAAA;AAG/F,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,wGAwBtD"}
1
+ {"version":3,"file":"use_direct_message_candidates.d.ts","sourceRoot":"","sources":["../../../src/hooks/direct_messages/use_direct_message_candidates.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,8BAA8B,EAAE,MAAM,gDAAgD,CAAA;AAG/F,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,wGAyBtD"}
@@ -22,6 +22,7 @@ export function useDirectMessageCandidates(term) {
22
22
  }),
23
23
  enabled: term.length >= 1,
24
24
  staleTime: 1000 * 60,
25
+ throwOnError: true,
25
26
  });
26
27
  }
27
28
  //# sourceMappingURL=use_direct_message_candidates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use_direct_message_candidates.js","sourceRoot":"","sources":["../../../src/hooks/direct_messages/use_direct_message_candidates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,MAAM,UAAU,0BAA0B,CAAC,IAAY;IACrD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAChC,OAAO,QAAQ,CAAC;QACd,QAAQ,EAAE,CAAC,MAAM,EAAE,+BAA+B,EAAE,IAAI,CAAC;QACzD,OAAO,EAAE,GAAG,EAAE,CACZ,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;YACjB,GAAG,EAAE,+BAA+B;YACpC,IAAI,EAAE;gBACJ,CAAC,EAAE,IAAI;gBACP,MAAM,EAAE;oBACN,sBAAsB,EAAE;wBACtB,MAAM;wBACN,QAAQ;wBACR,QAAQ;wBACR,OAAO;wBACP,UAAU;wBACV,0BAA0B;qBAC3B;iBACF;aACF;SACF,CAA2D;QAC9D,OAAO,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC;QACzB,SAAS,EAAE,IAAI,GAAG,EAAE;KACrB,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { useQuery } from '@tanstack/react-query'\nimport { ApiCollection } from '../../types'\nimport { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'\nimport { useApiClient } from '../use_api_client'\n\nexport function useDirectMessageCandidates(term: string) {\n const apiClient = useApiClient()\n return useQuery({\n queryKey: ['chat', '/me/direct_message_candidates', term],\n queryFn: () =>\n apiClient.chat.get({\n url: '/me/direct_message_candidates',\n data: {\n q: term,\n fields: {\n DirectMessageCandidate: [\n 'name',\n 'avatar',\n 'groups',\n 'teams',\n 'under_18',\n 'direct_messages_disabled',\n ],\n },\n },\n }) as Promise<ApiCollection<DirectMessageCandidateResource>>,\n enabled: term.length >= 1,\n staleTime: 1000 * 60,\n })\n}\n"]}
1
+ {"version":3,"file":"use_direct_message_candidates.js","sourceRoot":"","sources":["../../../src/hooks/direct_messages/use_direct_message_candidates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,MAAM,UAAU,0BAA0B,CAAC,IAAY;IACrD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAChC,OAAO,QAAQ,CAAC;QACd,QAAQ,EAAE,CAAC,MAAM,EAAE,+BAA+B,EAAE,IAAI,CAAC;QACzD,OAAO,EAAE,GAAG,EAAE,CACZ,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;YACjB,GAAG,EAAE,+BAA+B;YACpC,IAAI,EAAE;gBACJ,CAAC,EAAE,IAAI;gBACP,MAAM,EAAE;oBACN,sBAAsB,EAAE;wBACtB,MAAM;wBACN,QAAQ;wBACR,QAAQ;wBACR,OAAO;wBACP,UAAU;wBACV,0BAA0B;qBAC3B;iBACF;aACF;SACF,CAA2D;QAC9D,OAAO,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC;QACzB,SAAS,EAAE,IAAI,GAAG,EAAE;QACpB,YAAY,EAAE,IAAI;KACnB,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { useQuery } from '@tanstack/react-query'\nimport { ApiCollection } from '../../types'\nimport { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'\nimport { useApiClient } from '../use_api_client'\n\nexport function useDirectMessageCandidates(term: string) {\n const apiClient = useApiClient()\n return useQuery({\n queryKey: ['chat', '/me/direct_message_candidates', term],\n queryFn: () =>\n apiClient.chat.get({\n url: '/me/direct_message_candidates',\n data: {\n q: term,\n fields: {\n DirectMessageCandidate: [\n 'name',\n 'avatar',\n 'groups',\n 'teams',\n 'under_18',\n 'direct_messages_disabled',\n ],\n },\n },\n }) as Promise<ApiCollection<DirectMessageCandidateResource>>,\n enabled: term.length >= 1,\n staleTime: 1000 * 60,\n throwOnError: true,\n })\n}\n"]}
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import { ConversationSelectRecipientsScreenProps } from './types/screen_props';
3
- export declare function ConversationSelectDirectMessageRecipientsScreen(_props: ConversationSelectRecipientsScreenProps): React.JSX.Element;
2
+ export declare function ConversationSelectDirectMessageRecipientsScreen(_props: ConversationSelectRecipientsScreenProps): import("react").JSX.Element;
4
3
  //# sourceMappingURL=conversation_select_direct_message_recipients_screen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"conversation_select_direct_message_recipients_screen.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,EAAE,uCAAuC,EAAE,MAAM,sBAAsB,CAAA;AAE9E,wBAAgB,+CAA+C,CAC7D,MAAM,EAAE,uCAAuC,qBAUhD"}
1
+ {"version":3,"file":"conversation_select_direct_message_recipients_screen.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAE,uCAAuC,EAAE,MAAM,sBAAsB,CAAA;AAI9E,wBAAgB,+CAA+C,CAC7D,MAAM,EAAE,uCAAuC,+BAwDhD"}
@@ -1,25 +1,307 @@
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, Badge, Banner, Icon, Spinner, Text } from '../../components';
6
+ import { ComponentErrorBoundary } from '../../components/page/component_error_boundary';
7
+ import { useTheme } from '../../hooks';
8
+ import { useDirectMessageAffiliations } from '../../hooks/direct_messages/use_direct_message_affiliations';
9
+ import { useDirectMessageCandidates } from '../../hooks/direct_messages/use_direct_message_candidates';
10
+ const MAX_RECIPIENTS = 9;
5
11
  export function ConversationSelectDirectMessageRecipientsScreen(_props) {
6
12
  const styles = useStyles();
13
+ const [searchTerm, setSearchTerm] = useState('');
14
+ const [debouncedTerm, setDebouncedTerm] = useState('');
15
+ const [selectedPeople, setSelectedPeople] = useState(new Map());
16
+ useEffect(() => {
17
+ const timer = setTimeout(() => setDebouncedTerm(searchTerm), 200);
18
+ return () => clearTimeout(timer);
19
+ }, [searchTerm]);
20
+ const selectPerson = (person) => {
21
+ if (selectedPeople.size >= MAX_RECIPIENTS)
22
+ return;
23
+ setSelectedPeople(prev => new Map(prev).set(person.id, person));
24
+ setSearchTerm('');
25
+ setDebouncedTerm('');
26
+ };
27
+ const removePerson = (id) => {
28
+ setSelectedPeople(prev => {
29
+ const next = new Map(prev);
30
+ next.delete(id);
31
+ return next;
32
+ });
33
+ };
7
34
  return (<View style={styles.container}>
8
- <Heading variant="h2">New direct message</Heading>
9
- <Text variant="secondary">Coming soon</Text>
35
+ <ToField selectedPeople={selectedPeople} searchTerm={searchTerm} onSearchChange={setSearchTerm} onRemove={removePerson} onClear={() => setSelectedPeople(new Map())}/>
36
+ <ComponentErrorBoundary key={debouncedTerm} fallback={<Banner appearance="error" description="Couldn't load people right now. Try again." style={styles.errorBanner}/>}>
37
+ <CandidateResults term={debouncedTerm} selectedPeople={selectedPeople} onSelect={selectPerson}/>
38
+ </ComponentErrorBoundary>
10
39
  </View>);
11
40
  }
41
+ function ToField({ selectedPeople, searchTerm, onSearchChange, onRemove, onClear, }) {
42
+ const styles = useStyles();
43
+ const { colors } = useTheme();
44
+ const hasSelections = selectedPeople.size > 0;
45
+ return (<View style={styles.toField}>
46
+ <View style={styles.toFieldHeader}>
47
+ <Text style={styles.toLabel}>To</Text>
48
+ {hasSelections && (<Pressable onPress={onClear} hitSlop={8}>
49
+ <Text style={styles.clearButton}>Clear</Text>
50
+ </Pressable>)}
51
+ </View>
52
+ <View style={styles.toInputArea}>
53
+ {[...selectedPeople.values()].map(person => (<PersonChip key={person.id} person={person} onRemove={onRemove}/>))}
54
+ <TextInput autoFocus placeholder="Search" placeholderTextColor={colors.textColorDefaultPlaceholder} value={searchTerm} onChangeText={onSearchChange} style={styles.toTextInput} returnKeyType="search"/>
55
+ </View>
56
+ </View>);
57
+ }
58
+ function PersonChip({ person, onRemove, }) {
59
+ const styles = useStyles();
60
+ return (<View style={styles.chip}>
61
+ <Avatar sourceUri={person.avatar} size="sm" showFallback/>
62
+ <Text style={styles.chipName}>{person.name}</Text>
63
+ <Pressable onPress={() => onRemove(person.id)} hitSlop={8} accessibilityLabel={`Remove ${person.name}`}>
64
+ <Icon name="general.x" size={12} style={styles.chipRemoveIcon}/>
65
+ </Pressable>
66
+ </View>);
67
+ }
68
+ function CandidateResults({ term, selectedPeople, onSelect, }) {
69
+ const styles = useStyles();
70
+ const { data, isFetching } = useDirectMessageCandidates(term);
71
+ const { groups, teams } = useDirectMessageAffiliations();
72
+ if (term.length < 1) {
73
+ if (selectedPeople.size > 0)
74
+ return null;
75
+ return (<View style={styles.emptyState}>
76
+ <Icon name="churchCenter.people" size={40} style={styles.emptyStateIcon}/>
77
+ <Text style={styles.emptyStateTitle}>Search for people to direct message</Text>
78
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
79
+ Start a conversation with people in your groups and teams.
80
+ </Text>
81
+ </View>);
82
+ }
83
+ const isAtLimit = selectedPeople.size >= MAX_RECIPIENTS;
84
+ if (isAtLimit) {
85
+ return (<View style={styles.emptyState}>
86
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
87
+ You can add up to {MAX_RECIPIENTS} people to a direct message.
88
+ </Text>
89
+ </View>);
90
+ }
91
+ const unselectedCandidates = (data?.data ?? []).filter(c => !selectedPeople.has(c.id));
92
+ const hasTruncated = (data?.meta.totalCount ?? 0) > (data?.data.length ?? 0);
93
+ if (isFetching && unselectedCandidates.length === 0) {
94
+ return (<View style={styles.emptyState}>
95
+ <Spinner />
96
+ </View>);
97
+ }
98
+ if (!isFetching && unselectedCandidates.length === 0) {
99
+ return (<View style={styles.emptyState}>
100
+ <Icon name="general.search" size={32} style={styles.emptyStateIcon}/>
101
+ <Text style={styles.emptyStateTitle}>No matches for "{term}"</Text>
102
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
103
+ Try a different name, or check that you share a group or team with them.
104
+ </Text>
105
+ </View>);
106
+ }
107
+ return (<CandidateList candidates={unselectedCandidates} hasTruncated={hasTruncated} groups={groups} teams={teams} onSelect={onSelect}/>);
108
+ }
109
+ function candidateBadges(candidate) {
110
+ const badges = [];
111
+ if (candidate.directMessagesDisabled)
112
+ badges.push({ title: 'DMs off' });
113
+ if (candidate.under_18)
114
+ badges.push({ title: 'Under 18' });
115
+ return badges;
116
+ }
117
+ function candidateAffiliationNames(candidate, groups, teams) {
118
+ return [
119
+ ...candidate.groups.flatMap(id => {
120
+ const name = groups.get(id);
121
+ return name ? [name] : [];
122
+ }),
123
+ ...candidate.teams.flatMap(id => {
124
+ const name = teams.get(id);
125
+ return name ? [name] : [];
126
+ }),
127
+ ];
128
+ }
129
+ function CandidateList({ candidates, hasTruncated, groups, teams, onSelect, }) {
130
+ const styles = useStyles();
131
+ const available = candidates.filter(c => !c.directMessagesDisabled);
132
+ const dmDisabled = candidates.filter(c => c.directMessagesDisabled);
133
+ return (<FlatList data={available} keyExtractor={item => item.id} renderItem={({ item }) => (<CandidateItem candidate={item} badges={candidateBadges(item)} affiliationNames={candidateAffiliationNames(item, groups, teams)} onSelect={() => onSelect(item)}/>)} ListHeaderComponent={<View style={styles.resultsHeader}>
134
+ <Text style={styles.resultsHeading}>Results</Text>
135
+ </View>} ListFooterComponent={<>
136
+ {dmDisabled.length > 0 && (<>
137
+ <View style={styles.unavailableHeader}>
138
+ <Text variant="secondary" style={styles.unavailableLabel}>
139
+ Unavailable for direct messages
140
+ </Text>
141
+ </View>
142
+ {dmDisabled.map(candidate => (<CandidateItem key={candidate.id} candidate={candidate} badges={candidateBadges(candidate)} affiliationNames={candidateAffiliationNames(candidate, groups, teams)} disabled/>))}
143
+ </>)}
144
+ {hasTruncated && (<View style={styles.truncatedFooter}>
145
+ <Text variant="secondary" style={styles.truncatedText}>
146
+ Keep typing to narrow your results.
147
+ </Text>
148
+ </View>)}
149
+ </>}/>);
150
+ }
151
+ function CandidateItem({ candidate, badges, affiliationNames, onSelect, disabled = false, }) {
152
+ const styles = useStyles();
153
+ return (<PlatformPressable onPress={disabled ? undefined : onSelect} accessibilityRole="button" accessibilityLabel={`Select ${candidate.name}`} accessibilityState={{ disabled }}>
154
+ <View style={[styles.row, disabled && styles.candidateDisabled]}>
155
+ <Avatar sourceUri={candidate.avatar} size="lg" showFallback/>
156
+ <View style={styles.rowInfo}>
157
+ <Text style={styles.rowName}>{candidate.name}</Text>
158
+ {affiliationNames.length > 0 && (<Text style={styles.rowAffiliations} numberOfLines={2}>
159
+ {affiliationNames.join(', ')}
160
+ </Text>)}
161
+ {badges.length > 0 && (<View style={styles.badgeRow}>
162
+ {badges.map(({ title }) => (<Badge key={title} label={title}/>))}
163
+ </View>)}
164
+ </View>
165
+ </View>
166
+ </PlatformPressable>);
167
+ }
12
168
  const useStyles = () => {
13
169
  const { bottom } = useSafeAreaInsets();
170
+ const { colors } = useTheme();
14
171
  return StyleSheet.create({
15
172
  container: {
16
173
  flex: 1,
17
- padding: 16,
18
- paddingBottom: 16 + bottom,
174
+ paddingBottom: bottom,
175
+ },
176
+ toField: {
177
+ paddingHorizontal: 16,
178
+ paddingTop: 16,
179
+ paddingBottom: 8,
180
+ },
181
+ toFieldHeader: {
182
+ flexDirection: 'row',
183
+ justifyContent: 'space-between',
19
184
  alignItems: 'center',
20
- justifyContent: 'center',
185
+ marginBottom: 8,
186
+ },
187
+ toLabel: {
188
+ fontWeight: '600',
189
+ color: colors.textColorDefaultSecondary,
190
+ },
191
+ clearButton: {
192
+ color: colors.interaction,
193
+ },
194
+ toInputArea: {
195
+ flexDirection: 'row',
196
+ flexWrap: 'wrap',
197
+ alignItems: 'center',
198
+ gap: 6,
199
+ borderWidth: 1,
200
+ borderColor: colors.borderColorDefaultBase,
201
+ borderRadius: 8,
202
+ paddingHorizontal: 10,
203
+ paddingVertical: 8,
204
+ },
205
+ toTextInput: {
206
+ flex: 1,
207
+ minWidth: 80,
208
+ color: colors.textColorDefaultPrimary,
209
+ fontSize: 16,
210
+ paddingVertical: 2,
211
+ },
212
+ chip: {
213
+ flexDirection: 'row',
214
+ alignItems: 'center',
215
+ gap: 4,
216
+ borderWidth: 1,
217
+ borderColor: colors.borderColorDefaultBase,
218
+ borderRadius: 100,
219
+ paddingLeft: 4,
220
+ paddingRight: 6,
221
+ paddingVertical: 3,
222
+ },
223
+ chipName: {
224
+ fontSize: 14,
225
+ },
226
+ chipRemoveIcon: {
227
+ color: colors.iconColorDefaultSecondary,
228
+ },
229
+ errorBanner: {
230
+ margin: 16,
231
+ },
232
+ emptyState: {
233
+ alignItems: 'center',
234
+ paddingTop: 32,
235
+ paddingHorizontal: 32,
21
236
  gap: 8,
22
237
  },
238
+ emptyStateIcon: {
239
+ color: colors.iconColorDefaultSecondary,
240
+ marginBottom: 4,
241
+ },
242
+ emptyStateTitle: {
243
+ fontWeight: '600',
244
+ textAlign: 'center',
245
+ },
246
+ emptyStateSubtitle: {
247
+ textAlign: 'center',
248
+ fontSize: 14,
249
+ },
250
+ row: {
251
+ flexDirection: 'row',
252
+ alignItems: 'flex-start',
253
+ gap: 12,
254
+ paddingHorizontal: 16,
255
+ paddingVertical: 12,
256
+ borderBottomWidth: 1,
257
+ borderColor: colors.borderColorDefaultDim,
258
+ },
259
+ rowInfo: {
260
+ flex: 1,
261
+ gap: 2,
262
+ },
263
+ rowName: {
264
+ fontSize: 15,
265
+ fontWeight: '500',
266
+ },
267
+ rowAffiliations: {
268
+ fontSize: 12,
269
+ color: colors.textColorDefaultSecondary,
270
+ },
271
+ badgeRow: {
272
+ flexDirection: 'row',
273
+ flexWrap: 'wrap',
274
+ gap: 4,
275
+ marginTop: 4,
276
+ },
277
+ candidateDisabled: {
278
+ opacity: 0.5,
279
+ },
280
+ resultsHeader: {
281
+ paddingHorizontal: 16,
282
+ paddingTop: 16,
283
+ paddingBottom: 8,
284
+ },
285
+ resultsHeading: {
286
+ fontWeight: '600',
287
+ },
288
+ unavailableHeader: {
289
+ paddingHorizontal: 16,
290
+ paddingTop: 16,
291
+ paddingBottom: 8,
292
+ },
293
+ unavailableLabel: {
294
+ fontSize: 13,
295
+ },
296
+ truncatedFooter: {
297
+ paddingHorizontal: 16,
298
+ paddingTop: 12,
299
+ paddingBottom: 8,
300
+ },
301
+ truncatedText: {
302
+ fontSize: 13,
303
+ textAlign: 'center',
304
+ },
23
305
  });
24
306
  };
25
307
  //# sourceMappingURL=conversation_select_direct_message_recipients_screen.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"conversation_select_direct_message_recipients_screen.js","sourceRoot":"","sources":["../../../src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAGhD,MAAM,UAAU,+CAA+C,CAC7D,MAA+C;IAE/C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAC5B;MAAA,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,OAAO,CACjD;MAAA,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE,IAAI,CAC7C;IAAA,EAAE,IAAI,CAAC,CACR,CAAA;AACH,CAAC;AAED,MAAM,SAAS,GAAG,GAAG,EAAE;IACrB,MAAM,EAAE,MAAM,EAAE,GAAG,iBAAiB,EAAE,CAAA;IAEtC,OAAO,UAAU,CAAC,MAAM,CAAC;QACvB,SAAS,EAAE;YACT,IAAI,EAAE,CAAC;YACP,OAAO,EAAE,EAAE;YACX,aAAa,EAAE,EAAE,GAAG,MAAM;YAC1B,UAAU,EAAE,QAAQ;YACpB,cAAc,EAAE,QAAQ;YACxB,GAAG,EAAE,CAAC;SACP;KACF,CAAC,CAAA;AACJ,CAAC,CAAA","sourcesContent":["import React from 'react'\nimport { StyleSheet, View } from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport { Heading, Text } from '../../components'\nimport { ConversationSelectRecipientsScreenProps } from './types/screen_props'\n\nexport function ConversationSelectDirectMessageRecipientsScreen(\n _props: ConversationSelectRecipientsScreenProps\n) {\n const styles = useStyles()\n\n return (\n <View style={styles.container}>\n <Heading variant=\"h2\">New direct message</Heading>\n <Text variant=\"secondary\">Coming soon</Text>\n </View>\n )\n}\n\nconst useStyles = () => {\n const { bottom } = useSafeAreaInsets()\n\n return StyleSheet.create({\n container: {\n flex: 1,\n padding: 16,\n paddingBottom: 16 + bottom,\n alignItems: 'center',\n justifyContent: 'center',\n gap: 8,\n },\n })\n}\n"]}
1
+ {"version":3,"file":"conversation_select_direct_message_recipients_screen.js","sourceRoot":"","sources":["../../../src/screens/conversation_select_recipients/conversation_select_direct_message_recipients_screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { PlatformPressable } from '@react-navigation/elements'\nimport { useEffect, useState } from 'react'\nimport { FlatList, Pressable, StyleSheet, TextInput, View } from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport { Avatar, Badge, Banner, Icon, Spinner, Text } from '../../components'\nimport { ComponentErrorBoundary } from '../../components/page/component_error_boundary'\nimport { useTheme } from '../../hooks'\nimport { useDirectMessageAffiliations } from '../../hooks/direct_messages/use_direct_message_affiliations'\nimport { useDirectMessageCandidates } from '../../hooks/direct_messages/use_direct_message_candidates'\nimport { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'\nimport { ConversationSelectRecipientsScreenProps } from './types/screen_props'\n\nconst MAX_RECIPIENTS = 9\n\nexport function ConversationSelectDirectMessageRecipientsScreen(\n _props: ConversationSelectRecipientsScreenProps\n) {\n const styles = useStyles()\n const [searchTerm, setSearchTerm] = useState('')\n const [debouncedTerm, setDebouncedTerm] = useState('')\n const [selectedPeople, setSelectedPeople] = useState<Map<string, DirectMessageCandidateResource>>(\n new Map()\n )\n\n useEffect(() => {\n const timer = setTimeout(() => setDebouncedTerm(searchTerm), 200)\n return () => clearTimeout(timer)\n }, [searchTerm])\n\n const selectPerson = (person: DirectMessageCandidateResource) => {\n if (selectedPeople.size >= MAX_RECIPIENTS) return\n setSelectedPeople(prev => new Map(prev).set(person.id, person))\n setSearchTerm('')\n setDebouncedTerm('')\n }\n\n const removePerson = (id: string) => {\n setSelectedPeople(prev => {\n const next = new Map(prev)\n next.delete(id)\n return next\n })\n }\n\n return (\n <View style={styles.container}>\n <ToField\n selectedPeople={selectedPeople}\n searchTerm={searchTerm}\n onSearchChange={setSearchTerm}\n onRemove={removePerson}\n onClear={() => setSelectedPeople(new Map())}\n />\n <ComponentErrorBoundary\n key={debouncedTerm}\n fallback={\n <Banner\n appearance=\"error\"\n description=\"Couldn't load people right now. Try again.\"\n style={styles.errorBanner}\n />\n }\n >\n <CandidateResults\n term={debouncedTerm}\n selectedPeople={selectedPeople}\n onSelect={selectPerson}\n />\n </ComponentErrorBoundary>\n </View>\n )\n}\n\nfunction ToField({\n selectedPeople,\n searchTerm,\n onSearchChange,\n onRemove,\n onClear,\n}: {\n selectedPeople: Map<string, DirectMessageCandidateResource>\n searchTerm: string\n onSearchChange: (term: string) => void\n onRemove: (id: string) => void\n onClear: () => void\n}) {\n const styles = useStyles()\n const { colors } = useTheme()\n const hasSelections = selectedPeople.size > 0\n\n return (\n <View style={styles.toField}>\n <View style={styles.toFieldHeader}>\n <Text style={styles.toLabel}>To</Text>\n {hasSelections && (\n <Pressable onPress={onClear} hitSlop={8}>\n <Text style={styles.clearButton}>Clear</Text>\n </Pressable>\n )}\n </View>\n <View style={styles.toInputArea}>\n {[...selectedPeople.values()].map(person => (\n <PersonChip key={person.id} person={person} onRemove={onRemove} />\n ))}\n <TextInput\n autoFocus\n placeholder=\"Search\"\n placeholderTextColor={colors.textColorDefaultPlaceholder}\n value={searchTerm}\n onChangeText={onSearchChange}\n style={styles.toTextInput}\n returnKeyType=\"search\"\n />\n </View>\n </View>\n )\n}\n\nfunction PersonChip({\n person,\n onRemove,\n}: {\n person: DirectMessageCandidateResource\n onRemove: (id: string) => void\n}) {\n const styles = useStyles()\n\n return (\n <View style={styles.chip}>\n <Avatar sourceUri={person.avatar} size=\"sm\" showFallback />\n <Text style={styles.chipName}>{person.name}</Text>\n <Pressable\n onPress={() => onRemove(person.id)}\n hitSlop={8}\n accessibilityLabel={`Remove ${person.name}`}\n >\n <Icon name=\"general.x\" size={12} style={styles.chipRemoveIcon} />\n </Pressable>\n </View>\n )\n}\n\nfunction CandidateResults({\n term,\n selectedPeople,\n onSelect,\n}: {\n term: string\n selectedPeople: Map<string, DirectMessageCandidateResource>\n onSelect: (candidate: DirectMessageCandidateResource) => void\n}) {\n const styles = useStyles()\n const { data, isFetching } = useDirectMessageCandidates(term)\n const { groups, teams } = useDirectMessageAffiliations()\n\n if (term.length < 1) {\n if (selectedPeople.size > 0) return null\n\n return (\n <View style={styles.emptyState}>\n <Icon name=\"churchCenter.people\" size={40} style={styles.emptyStateIcon} />\n <Text style={styles.emptyStateTitle}>Search for people to direct message</Text>\n <Text variant=\"secondary\" style={styles.emptyStateSubtitle}>\n Start a conversation with people in your groups and teams.\n </Text>\n </View>\n )\n }\n\n const isAtLimit = selectedPeople.size >= MAX_RECIPIENTS\n\n if (isAtLimit) {\n return (\n <View style={styles.emptyState}>\n <Text variant=\"secondary\" style={styles.emptyStateSubtitle}>\n You can add up to {MAX_RECIPIENTS} people to a direct message.\n </Text>\n </View>\n )\n }\n\n const unselectedCandidates = (data?.data ?? []).filter(c => !selectedPeople.has(c.id))\n const hasTruncated = (data?.meta.totalCount ?? 0) > (data?.data.length ?? 0)\n\n if (isFetching && unselectedCandidates.length === 0) {\n return (\n <View style={styles.emptyState}>\n <Spinner />\n </View>\n )\n }\n\n if (!isFetching && unselectedCandidates.length === 0) {\n return (\n <View style={styles.emptyState}>\n <Icon name=\"general.search\" size={32} style={styles.emptyStateIcon} />\n <Text style={styles.emptyStateTitle}>No matches for \"{term}\"</Text>\n <Text variant=\"secondary\" style={styles.emptyStateSubtitle}>\n Try a different name, or check that you share a group or team with them.\n </Text>\n </View>\n )\n }\n\n return (\n <CandidateList\n candidates={unselectedCandidates}\n hasTruncated={hasTruncated}\n groups={groups}\n teams={teams}\n onSelect={onSelect}\n />\n )\n}\n\nfunction candidateBadges(candidate: DirectMessageCandidateResource) {\n const badges = []\n if (candidate.directMessagesDisabled) badges.push({ title: 'DMs off' })\n if (candidate.under_18) badges.push({ title: 'Under 18' })\n return badges\n}\n\nfunction candidateAffiliationNames(\n candidate: DirectMessageCandidateResource,\n groups: Map<number, string>,\n teams: Map<number, string>\n) {\n return [\n ...candidate.groups.flatMap(id => {\n const name = groups.get(id)\n return name ? [name] : []\n }),\n ...candidate.teams.flatMap(id => {\n const name = teams.get(id)\n return name ? [name] : []\n }),\n ]\n}\n\nfunction CandidateList({\n candidates,\n hasTruncated,\n groups,\n teams,\n onSelect,\n}: {\n candidates: DirectMessageCandidateResource[]\n hasTruncated: boolean\n groups: Map<number, string>\n teams: Map<number, string>\n onSelect: (candidate: DirectMessageCandidateResource) => void\n}) {\n const styles = useStyles()\n const available = candidates.filter(c => !c.directMessagesDisabled)\n const dmDisabled = candidates.filter(c => c.directMessagesDisabled)\n\n return (\n <FlatList<DirectMessageCandidateResource>\n data={available}\n keyExtractor={item => item.id}\n renderItem={({ item }) => (\n <CandidateItem\n candidate={item}\n badges={candidateBadges(item)}\n affiliationNames={candidateAffiliationNames(item, groups, teams)}\n onSelect={() => onSelect(item)}\n />\n )}\n ListHeaderComponent={\n <View style={styles.resultsHeader}>\n <Text style={styles.resultsHeading}>Results</Text>\n </View>\n }\n ListFooterComponent={\n <>\n {dmDisabled.length > 0 && (\n <>\n <View style={styles.unavailableHeader}>\n <Text variant=\"secondary\" style={styles.unavailableLabel}>\n Unavailable for direct messages\n </Text>\n </View>\n {dmDisabled.map(candidate => (\n <CandidateItem\n key={candidate.id}\n candidate={candidate}\n badges={candidateBadges(candidate)}\n affiliationNames={candidateAffiliationNames(candidate, groups, teams)}\n disabled\n />\n ))}\n </>\n )}\n {hasTruncated && (\n <View style={styles.truncatedFooter}>\n <Text variant=\"secondary\" style={styles.truncatedText}>\n Keep typing to narrow your results.\n </Text>\n </View>\n )}\n </>\n }\n />\n )\n}\n\nfunction CandidateItem({\n candidate,\n badges,\n affiliationNames,\n onSelect,\n disabled = false,\n}: {\n candidate: DirectMessageCandidateResource\n badges: { title: string }[]\n affiliationNames: string[]\n onSelect?: () => void\n disabled?: boolean\n}) {\n const styles = useStyles()\n\n return (\n <PlatformPressable\n onPress={disabled ? undefined : onSelect}\n accessibilityRole=\"button\"\n accessibilityLabel={`Select ${candidate.name}`}\n accessibilityState={{ disabled }}\n >\n <View style={[styles.row, disabled && styles.candidateDisabled]}>\n <Avatar sourceUri={candidate.avatar} size=\"lg\" showFallback />\n <View style={styles.rowInfo}>\n <Text style={styles.rowName}>{candidate.name}</Text>\n {affiliationNames.length > 0 && (\n <Text style={styles.rowAffiliations} numberOfLines={2}>\n {affiliationNames.join(', ')}\n </Text>\n )}\n {badges.length > 0 && (\n <View style={styles.badgeRow}>\n {badges.map(({ title }) => (\n <Badge key={title} label={title} />\n ))}\n </View>\n )}\n </View>\n </View>\n </PlatformPressable>\n )\n}\n\nconst useStyles = () => {\n const { bottom } = useSafeAreaInsets()\n const { colors } = useTheme()\n\n return StyleSheet.create({\n container: {\n flex: 1,\n paddingBottom: bottom,\n },\n toField: {\n paddingHorizontal: 16,\n paddingTop: 16,\n paddingBottom: 8,\n },\n toFieldHeader: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'center',\n marginBottom: 8,\n },\n toLabel: {\n fontWeight: '600',\n color: colors.textColorDefaultSecondary,\n },\n clearButton: {\n color: colors.interaction,\n },\n toInputArea: {\n flexDirection: 'row',\n flexWrap: 'wrap',\n alignItems: 'center',\n gap: 6,\n borderWidth: 1,\n borderColor: colors.borderColorDefaultBase,\n borderRadius: 8,\n paddingHorizontal: 10,\n paddingVertical: 8,\n },\n toTextInput: {\n flex: 1,\n minWidth: 80,\n color: colors.textColorDefaultPrimary,\n fontSize: 16,\n paddingVertical: 2,\n },\n chip: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: 4,\n borderWidth: 1,\n borderColor: colors.borderColorDefaultBase,\n borderRadius: 100,\n paddingLeft: 4,\n paddingRight: 6,\n paddingVertical: 3,\n },\n chipName: {\n fontSize: 14,\n },\n chipRemoveIcon: {\n color: colors.iconColorDefaultSecondary,\n },\n errorBanner: {\n margin: 16,\n },\n emptyState: {\n alignItems: 'center',\n paddingTop: 32,\n paddingHorizontal: 32,\n gap: 8,\n },\n emptyStateIcon: {\n color: colors.iconColorDefaultSecondary,\n marginBottom: 4,\n },\n emptyStateTitle: {\n fontWeight: '600',\n textAlign: 'center',\n },\n emptyStateSubtitle: {\n textAlign: 'center',\n fontSize: 14,\n },\n row: {\n flexDirection: 'row',\n alignItems: 'flex-start',\n gap: 12,\n paddingHorizontal: 16,\n paddingVertical: 12,\n borderBottomWidth: 1,\n borderColor: colors.borderColorDefaultDim,\n },\n rowInfo: {\n flex: 1,\n gap: 2,\n },\n rowName: {\n fontSize: 15,\n fontWeight: '500',\n },\n rowAffiliations: {\n fontSize: 12,\n color: colors.textColorDefaultSecondary,\n },\n badgeRow: {\n flexDirection: 'row',\n flexWrap: 'wrap',\n gap: 4,\n marginTop: 4,\n },\n\n candidateDisabled: {\n opacity: 0.5,\n },\n resultsHeader: {\n paddingHorizontal: 16,\n paddingTop: 16,\n paddingBottom: 8,\n },\n resultsHeading: {\n fontWeight: '600',\n },\n unavailableHeader: {\n paddingHorizontal: 16,\n paddingTop: 16,\n paddingBottom: 8,\n },\n unavailableLabel: {\n fontSize: 13,\n },\n truncatedFooter: {\n paddingHorizontal: 16,\n paddingTop: 12,\n paddingBottom: 8,\n },\n truncatedText: {\n fontSize: 13,\n textAlign: 'center',\n },\n })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.39.0-rc.1",
3
+ "version": "3.39.0-rc.3",
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": "03a375a045ff2ae572627318c41bfc6cdcf1bc74"
75
+ "gitHead": "96e7118e4de1c7754f60028933d48779d384b32f"
76
76
  }
@@ -26,5 +26,6 @@ export function useDirectMessageCandidates(term: string) {
26
26
  }) as Promise<ApiCollection<DirectMessageCandidateResource>>,
27
27
  enabled: term.length >= 1,
28
28
  staleTime: 1000 * 60,
29
+ throwOnError: true,
29
30
  })
30
31
  }
@@ -1,33 +1,491 @@
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, Badge, Banner, Icon, Spinner, Text } from '../../components'
6
+ import { ComponentErrorBoundary } from '../../components/page/component_error_boundary'
7
+ import { useTheme } from '../../hooks'
8
+ import { useDirectMessageAffiliations } from '../../hooks/direct_messages/use_direct_message_affiliations'
9
+ import { useDirectMessageCandidates } from '../../hooks/direct_messages/use_direct_message_candidates'
10
+ import { DirectMessageCandidateResource } from '../../types/resources/direct_message_candidate'
5
11
  import { ConversationSelectRecipientsScreenProps } from './types/screen_props'
6
12
 
13
+ const MAX_RECIPIENTS = 9
14
+
7
15
  export function ConversationSelectDirectMessageRecipientsScreen(
8
16
  _props: ConversationSelectRecipientsScreenProps
9
17
  ) {
10
18
  const styles = useStyles()
19
+ const [searchTerm, setSearchTerm] = useState('')
20
+ const [debouncedTerm, setDebouncedTerm] = useState('')
21
+ const [selectedPeople, setSelectedPeople] = useState<Map<string, DirectMessageCandidateResource>>(
22
+ new Map()
23
+ )
24
+
25
+ useEffect(() => {
26
+ const timer = setTimeout(() => setDebouncedTerm(searchTerm), 200)
27
+ return () => clearTimeout(timer)
28
+ }, [searchTerm])
29
+
30
+ const selectPerson = (person: DirectMessageCandidateResource) => {
31
+ if (selectedPeople.size >= MAX_RECIPIENTS) return
32
+ setSelectedPeople(prev => new Map(prev).set(person.id, person))
33
+ setSearchTerm('')
34
+ setDebouncedTerm('')
35
+ }
36
+
37
+ const removePerson = (id: string) => {
38
+ setSelectedPeople(prev => {
39
+ const next = new Map(prev)
40
+ next.delete(id)
41
+ return next
42
+ })
43
+ }
11
44
 
12
45
  return (
13
46
  <View style={styles.container}>
14
- <Heading variant="h2">New direct message</Heading>
15
- <Text variant="secondary">Coming soon</Text>
47
+ <ToField
48
+ selectedPeople={selectedPeople}
49
+ searchTerm={searchTerm}
50
+ onSearchChange={setSearchTerm}
51
+ onRemove={removePerson}
52
+ onClear={() => setSelectedPeople(new Map())}
53
+ />
54
+ <ComponentErrorBoundary
55
+ key={debouncedTerm}
56
+ fallback={
57
+ <Banner
58
+ appearance="error"
59
+ description="Couldn't load people right now. Try again."
60
+ style={styles.errorBanner}
61
+ />
62
+ }
63
+ >
64
+ <CandidateResults
65
+ term={debouncedTerm}
66
+ selectedPeople={selectedPeople}
67
+ onSelect={selectPerson}
68
+ />
69
+ </ComponentErrorBoundary>
16
70
  </View>
17
71
  )
18
72
  }
19
73
 
74
+ function ToField({
75
+ selectedPeople,
76
+ searchTerm,
77
+ onSearchChange,
78
+ onRemove,
79
+ onClear,
80
+ }: {
81
+ selectedPeople: Map<string, DirectMessageCandidateResource>
82
+ searchTerm: string
83
+ onSearchChange: (term: string) => void
84
+ onRemove: (id: string) => void
85
+ onClear: () => void
86
+ }) {
87
+ const styles = useStyles()
88
+ const { colors } = useTheme()
89
+ const hasSelections = selectedPeople.size > 0
90
+
91
+ return (
92
+ <View style={styles.toField}>
93
+ <View style={styles.toFieldHeader}>
94
+ <Text style={styles.toLabel}>To</Text>
95
+ {hasSelections && (
96
+ <Pressable onPress={onClear} hitSlop={8}>
97
+ <Text style={styles.clearButton}>Clear</Text>
98
+ </Pressable>
99
+ )}
100
+ </View>
101
+ <View style={styles.toInputArea}>
102
+ {[...selectedPeople.values()].map(person => (
103
+ <PersonChip key={person.id} person={person} onRemove={onRemove} />
104
+ ))}
105
+ <TextInput
106
+ autoFocus
107
+ placeholder="Search"
108
+ placeholderTextColor={colors.textColorDefaultPlaceholder}
109
+ value={searchTerm}
110
+ onChangeText={onSearchChange}
111
+ style={styles.toTextInput}
112
+ returnKeyType="search"
113
+ />
114
+ </View>
115
+ </View>
116
+ )
117
+ }
118
+
119
+ function PersonChip({
120
+ person,
121
+ onRemove,
122
+ }: {
123
+ person: DirectMessageCandidateResource
124
+ onRemove: (id: string) => void
125
+ }) {
126
+ const styles = useStyles()
127
+
128
+ return (
129
+ <View style={styles.chip}>
130
+ <Avatar sourceUri={person.avatar} size="sm" showFallback />
131
+ <Text style={styles.chipName}>{person.name}</Text>
132
+ <Pressable
133
+ onPress={() => onRemove(person.id)}
134
+ hitSlop={8}
135
+ accessibilityLabel={`Remove ${person.name}`}
136
+ >
137
+ <Icon name="general.x" size={12} style={styles.chipRemoveIcon} />
138
+ </Pressable>
139
+ </View>
140
+ )
141
+ }
142
+
143
+ function CandidateResults({
144
+ term,
145
+ selectedPeople,
146
+ onSelect,
147
+ }: {
148
+ term: string
149
+ selectedPeople: Map<string, DirectMessageCandidateResource>
150
+ onSelect: (candidate: DirectMessageCandidateResource) => void
151
+ }) {
152
+ const styles = useStyles()
153
+ const { data, isFetching } = useDirectMessageCandidates(term)
154
+ const { groups, teams } = useDirectMessageAffiliations()
155
+
156
+ if (term.length < 1) {
157
+ if (selectedPeople.size > 0) return null
158
+
159
+ return (
160
+ <View style={styles.emptyState}>
161
+ <Icon name="churchCenter.people" size={40} style={styles.emptyStateIcon} />
162
+ <Text style={styles.emptyStateTitle}>Search for people to direct message</Text>
163
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
164
+ Start a conversation with people in your groups and teams.
165
+ </Text>
166
+ </View>
167
+ )
168
+ }
169
+
170
+ const isAtLimit = selectedPeople.size >= MAX_RECIPIENTS
171
+
172
+ if (isAtLimit) {
173
+ return (
174
+ <View style={styles.emptyState}>
175
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
176
+ You can add up to {MAX_RECIPIENTS} people to a direct message.
177
+ </Text>
178
+ </View>
179
+ )
180
+ }
181
+
182
+ const unselectedCandidates = (data?.data ?? []).filter(c => !selectedPeople.has(c.id))
183
+ const hasTruncated = (data?.meta.totalCount ?? 0) > (data?.data.length ?? 0)
184
+
185
+ if (isFetching && unselectedCandidates.length === 0) {
186
+ return (
187
+ <View style={styles.emptyState}>
188
+ <Spinner />
189
+ </View>
190
+ )
191
+ }
192
+
193
+ if (!isFetching && unselectedCandidates.length === 0) {
194
+ return (
195
+ <View style={styles.emptyState}>
196
+ <Icon name="general.search" size={32} style={styles.emptyStateIcon} />
197
+ <Text style={styles.emptyStateTitle}>No matches for "{term}"</Text>
198
+ <Text variant="secondary" style={styles.emptyStateSubtitle}>
199
+ Try a different name, or check that you share a group or team with them.
200
+ </Text>
201
+ </View>
202
+ )
203
+ }
204
+
205
+ return (
206
+ <CandidateList
207
+ candidates={unselectedCandidates}
208
+ hasTruncated={hasTruncated}
209
+ groups={groups}
210
+ teams={teams}
211
+ onSelect={onSelect}
212
+ />
213
+ )
214
+ }
215
+
216
+ function candidateBadges(candidate: DirectMessageCandidateResource) {
217
+ const badges = []
218
+ if (candidate.directMessagesDisabled) badges.push({ title: 'DMs off' })
219
+ if (candidate.under_18) badges.push({ title: 'Under 18' })
220
+ return badges
221
+ }
222
+
223
+ function candidateAffiliationNames(
224
+ candidate: DirectMessageCandidateResource,
225
+ groups: Map<number, string>,
226
+ teams: Map<number, string>
227
+ ) {
228
+ return [
229
+ ...candidate.groups.flatMap(id => {
230
+ const name = groups.get(id)
231
+ return name ? [name] : []
232
+ }),
233
+ ...candidate.teams.flatMap(id => {
234
+ const name = teams.get(id)
235
+ return name ? [name] : []
236
+ }),
237
+ ]
238
+ }
239
+
240
+ function CandidateList({
241
+ candidates,
242
+ hasTruncated,
243
+ groups,
244
+ teams,
245
+ onSelect,
246
+ }: {
247
+ candidates: DirectMessageCandidateResource[]
248
+ hasTruncated: boolean
249
+ groups: Map<number, string>
250
+ teams: Map<number, string>
251
+ onSelect: (candidate: DirectMessageCandidateResource) => void
252
+ }) {
253
+ const styles = useStyles()
254
+ const available = candidates.filter(c => !c.directMessagesDisabled)
255
+ const dmDisabled = candidates.filter(c => c.directMessagesDisabled)
256
+
257
+ return (
258
+ <FlatList<DirectMessageCandidateResource>
259
+ data={available}
260
+ keyExtractor={item => item.id}
261
+ renderItem={({ item }) => (
262
+ <CandidateItem
263
+ candidate={item}
264
+ badges={candidateBadges(item)}
265
+ affiliationNames={candidateAffiliationNames(item, groups, teams)}
266
+ onSelect={() => onSelect(item)}
267
+ />
268
+ )}
269
+ ListHeaderComponent={
270
+ <View style={styles.resultsHeader}>
271
+ <Text style={styles.resultsHeading}>Results</Text>
272
+ </View>
273
+ }
274
+ ListFooterComponent={
275
+ <>
276
+ {dmDisabled.length > 0 && (
277
+ <>
278
+ <View style={styles.unavailableHeader}>
279
+ <Text variant="secondary" style={styles.unavailableLabel}>
280
+ Unavailable for direct messages
281
+ </Text>
282
+ </View>
283
+ {dmDisabled.map(candidate => (
284
+ <CandidateItem
285
+ key={candidate.id}
286
+ candidate={candidate}
287
+ badges={candidateBadges(candidate)}
288
+ affiliationNames={candidateAffiliationNames(candidate, groups, teams)}
289
+ disabled
290
+ />
291
+ ))}
292
+ </>
293
+ )}
294
+ {hasTruncated && (
295
+ <View style={styles.truncatedFooter}>
296
+ <Text variant="secondary" style={styles.truncatedText}>
297
+ Keep typing to narrow your results.
298
+ </Text>
299
+ </View>
300
+ )}
301
+ </>
302
+ }
303
+ />
304
+ )
305
+ }
306
+
307
+ function CandidateItem({
308
+ candidate,
309
+ badges,
310
+ affiliationNames,
311
+ onSelect,
312
+ disabled = false,
313
+ }: {
314
+ candidate: DirectMessageCandidateResource
315
+ badges: { title: string }[]
316
+ affiliationNames: string[]
317
+ onSelect?: () => void
318
+ disabled?: boolean
319
+ }) {
320
+ const styles = useStyles()
321
+
322
+ return (
323
+ <PlatformPressable
324
+ onPress={disabled ? undefined : onSelect}
325
+ accessibilityRole="button"
326
+ accessibilityLabel={`Select ${candidate.name}`}
327
+ accessibilityState={{ disabled }}
328
+ >
329
+ <View style={[styles.row, disabled && styles.candidateDisabled]}>
330
+ <Avatar sourceUri={candidate.avatar} size="lg" showFallback />
331
+ <View style={styles.rowInfo}>
332
+ <Text style={styles.rowName}>{candidate.name}</Text>
333
+ {affiliationNames.length > 0 && (
334
+ <Text style={styles.rowAffiliations} numberOfLines={2}>
335
+ {affiliationNames.join(', ')}
336
+ </Text>
337
+ )}
338
+ {badges.length > 0 && (
339
+ <View style={styles.badgeRow}>
340
+ {badges.map(({ title }) => (
341
+ <Badge key={title} label={title} />
342
+ ))}
343
+ </View>
344
+ )}
345
+ </View>
346
+ </View>
347
+ </PlatformPressable>
348
+ )
349
+ }
350
+
20
351
  const useStyles = () => {
21
352
  const { bottom } = useSafeAreaInsets()
353
+ const { colors } = useTheme()
22
354
 
23
355
  return StyleSheet.create({
24
356
  container: {
25
357
  flex: 1,
26
- padding: 16,
27
- paddingBottom: 16 + bottom,
358
+ paddingBottom: bottom,
359
+ },
360
+ toField: {
361
+ paddingHorizontal: 16,
362
+ paddingTop: 16,
363
+ paddingBottom: 8,
364
+ },
365
+ toFieldHeader: {
366
+ flexDirection: 'row',
367
+ justifyContent: 'space-between',
368
+ alignItems: 'center',
369
+ marginBottom: 8,
370
+ },
371
+ toLabel: {
372
+ fontWeight: '600',
373
+ color: colors.textColorDefaultSecondary,
374
+ },
375
+ clearButton: {
376
+ color: colors.interaction,
377
+ },
378
+ toInputArea: {
379
+ flexDirection: 'row',
380
+ flexWrap: 'wrap',
381
+ alignItems: 'center',
382
+ gap: 6,
383
+ borderWidth: 1,
384
+ borderColor: colors.borderColorDefaultBase,
385
+ borderRadius: 8,
386
+ paddingHorizontal: 10,
387
+ paddingVertical: 8,
388
+ },
389
+ toTextInput: {
390
+ flex: 1,
391
+ minWidth: 80,
392
+ color: colors.textColorDefaultPrimary,
393
+ fontSize: 16,
394
+ paddingVertical: 2,
395
+ },
396
+ chip: {
397
+ flexDirection: 'row',
398
+ alignItems: 'center',
399
+ gap: 4,
400
+ borderWidth: 1,
401
+ borderColor: colors.borderColorDefaultBase,
402
+ borderRadius: 100,
403
+ paddingLeft: 4,
404
+ paddingRight: 6,
405
+ paddingVertical: 3,
406
+ },
407
+ chipName: {
408
+ fontSize: 14,
409
+ },
410
+ chipRemoveIcon: {
411
+ color: colors.iconColorDefaultSecondary,
412
+ },
413
+ errorBanner: {
414
+ margin: 16,
415
+ },
416
+ emptyState: {
28
417
  alignItems: 'center',
29
- justifyContent: 'center',
418
+ paddingTop: 32,
419
+ paddingHorizontal: 32,
30
420
  gap: 8,
31
421
  },
422
+ emptyStateIcon: {
423
+ color: colors.iconColorDefaultSecondary,
424
+ marginBottom: 4,
425
+ },
426
+ emptyStateTitle: {
427
+ fontWeight: '600',
428
+ textAlign: 'center',
429
+ },
430
+ emptyStateSubtitle: {
431
+ textAlign: 'center',
432
+ fontSize: 14,
433
+ },
434
+ row: {
435
+ flexDirection: 'row',
436
+ alignItems: 'flex-start',
437
+ gap: 12,
438
+ paddingHorizontal: 16,
439
+ paddingVertical: 12,
440
+ borderBottomWidth: 1,
441
+ borderColor: colors.borderColorDefaultDim,
442
+ },
443
+ rowInfo: {
444
+ flex: 1,
445
+ gap: 2,
446
+ },
447
+ rowName: {
448
+ fontSize: 15,
449
+ fontWeight: '500',
450
+ },
451
+ rowAffiliations: {
452
+ fontSize: 12,
453
+ color: colors.textColorDefaultSecondary,
454
+ },
455
+ badgeRow: {
456
+ flexDirection: 'row',
457
+ flexWrap: 'wrap',
458
+ gap: 4,
459
+ marginTop: 4,
460
+ },
461
+
462
+ candidateDisabled: {
463
+ opacity: 0.5,
464
+ },
465
+ resultsHeader: {
466
+ paddingHorizontal: 16,
467
+ paddingTop: 16,
468
+ paddingBottom: 8,
469
+ },
470
+ resultsHeading: {
471
+ fontWeight: '600',
472
+ },
473
+ unavailableHeader: {
474
+ paddingHorizontal: 16,
475
+ paddingTop: 16,
476
+ paddingBottom: 8,
477
+ },
478
+ unavailableLabel: {
479
+ fontSize: 13,
480
+ },
481
+ truncatedFooter: {
482
+ paddingHorizontal: 16,
483
+ paddingTop: 12,
484
+ paddingBottom: 8,
485
+ },
486
+ truncatedText: {
487
+ fontSize: 13,
488
+ textAlign: 'center',
489
+ },
32
490
  })
33
491
  }