tf-checkout-react-native 2.0.0-beta.0 → 2.0.0-beta.11

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 (71) hide show
  1. package/README.md +115 -24
  2. package/dist/index.d.mts +8195 -282
  3. package/dist/index.d.ts +8195 -282
  4. package/dist/index.js +32346 -7001
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +32586 -7128
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +23 -11
  9. package/scripts/build-seat-map.mjs +31 -0
  10. package/scripts/seat-map-entry.jsx +22 -0
  11. package/scripts/verify-discussion-support.cjs +131 -0
  12. package/src/CardInput.tsx +64 -3
  13. package/src/EmbeddedCheckout.tsx +140 -38
  14. package/src/auth/AuthModal.tsx +186 -9
  15. package/src/auth/EmailCodeForm.tsx +91 -0
  16. package/src/auth/RegisterForm.tsx +91 -5
  17. package/src/auth/TwoFactorForm.tsx +74 -0
  18. package/src/auth/index.ts +4 -0
  19. package/src/configure.ts +19 -14
  20. package/src/confirmer.ts +112 -20
  21. package/src/generated/seatMapWidget.ts +3 -0
  22. package/src/index.tsx +1 -1
  23. package/src/sections/Accommodation.tsx +57 -0
  24. package/src/sections/AccountDeletion.tsx +347 -0
  25. package/src/sections/AccountEmailEnrichment.tsx +122 -0
  26. package/src/sections/AccountInformation.tsx +139 -0
  27. package/src/sections/AccountPaymentPlan.tsx +105 -0
  28. package/src/sections/AccountRewards.tsx +108 -0
  29. package/src/sections/AddOns.tsx +99 -148
  30. package/src/sections/BillingInfo.tsx +80 -78
  31. package/src/sections/Bundles.tsx +467 -0
  32. package/src/sections/EventDiscussionView.tsx +655 -0
  33. package/src/sections/NotificationPreferences.tsx +244 -0
  34. package/src/sections/OrderSummary.tsx +29 -2
  35. package/src/sections/Passes.tsx +177 -0
  36. package/src/sections/Payment.tsx +170 -53
  37. package/src/sections/PhoneAuthentication.tsx +371 -0
  38. package/src/sections/PostReaction.tsx +122 -0
  39. package/src/sections/ProfileSettings.tsx +446 -0
  40. package/src/sections/Rewards.tsx +320 -0
  41. package/src/sections/SeatMap.tsx +225 -0
  42. package/src/sections/SeatsIoMap.tsx +110 -0
  43. package/src/sections/Streaming.tsx +278 -0
  44. package/src/sections/TicketChange.tsx +379 -0
  45. package/src/sections/TicketGift.tsx +206 -0
  46. package/src/sections/TicketRefund.tsx +104 -0
  47. package/src/sections/Tickets.tsx +289 -208
  48. package/src/sections/Trips.tsx +87 -0
  49. package/src/sections/WaiverSigning.tsx +429 -0
  50. package/src/sections/billing/PhoneIdentity.tsx +169 -0
  51. package/src/sections/billing/TicketHoldersCard.tsx +4 -0
  52. package/src/sections/discussion/CommentImages.tsx +366 -0
  53. package/src/sections/discussion/SwipeReply.tsx +88 -0
  54. package/src/sections/index.ts +46 -0
  55. package/src/sections/rewards/RewardsLadder.tsx +167 -0
  56. package/src/sections/rewards/RewardsTree.tsx +117 -0
  57. package/src/storage.ts +8 -9
  58. package/src/ui/CheckoutProvider.tsx +51 -15
  59. package/src/ui/CustomFields.tsx +48 -12
  60. package/src/ui/Field.tsx +21 -3
  61. package/src/ui/Frozen.tsx +22 -0
  62. package/src/ui/InstallmentPicker.tsx +15 -18
  63. package/src/ui/PaymentMethods.tsx +6 -3
  64. package/src/ui/ProviderPages.tsx +214 -0
  65. package/src/ui/QuantitySelect.tsx +28 -3
  66. package/src/ui/Select.tsx +4 -1
  67. package/src/ui/SignaturePad.tsx +218 -0
  68. package/src/ui/index.ts +3 -0
  69. package/src/ui/primitives.tsx +13 -2
  70. package/src/ui/useAccountT.ts +16 -0
  71. package/src/useCheckout.ts +3 -1
@@ -0,0 +1,655 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ Alert,
5
+ FlatList,
6
+ Image,
7
+ KeyboardAvoidingView,
8
+ Linking,
9
+ Modal,
10
+ Platform,
11
+ Pressable,
12
+ StyleSheet,
13
+ Text,
14
+ TextInput,
15
+ View,
16
+ } from 'react-native'
17
+ import type {
18
+ EventComment,
19
+ EventCommentReportReason,
20
+ EventDiscussion,
21
+ } from 'tf-checkout-shared'
22
+ import { EVENT_COMMENT_IMAGE_MAX_COUNT } from 'tf-checkout-shared'
23
+ import { useTheme } from '../ui/CheckoutProvider'
24
+ import { CommentImages, CommentImageDrafts } from './discussion/CommentImages'
25
+ import { SwipeReply } from './discussion/SwipeReply'
26
+ import { useAccountT } from '../ui/useAccountT'
27
+
28
+ export interface EventDiscussionViewProps {
29
+ eventId: string
30
+ discussion: EventDiscussion
31
+ onLogin?: () => void
32
+ /** Platform picker only; common hook owns upload, validation and publication. */
33
+ pickImage?: () => Promise<string | null>
34
+ pickImages?: (remaining: number) => Promise<string[] | null>
35
+ previewKey?: string
36
+ locale?: string
37
+ renderReplyIcon?: (color: string) => React.ReactNode
38
+ /** Optional host artwork; the SDK owns the accessible close button and behavior. */
39
+ renderImageCloseIcon?: (color: string) => React.ReactNode
40
+ onModerationFilter?: (filter: 'pending' | 'reported' | undefined) => void
41
+ renderReactionIcon?: (
42
+ reaction: 'up' | 'down',
43
+ selected: boolean,
44
+ color: string
45
+ ) => React.ReactNode
46
+ }
47
+
48
+ /** Native presentation of the common discussion state. No account, moderation
49
+ * or mutation eligibility is inferred here: controls use the API's capabilities. */
50
+ const EventDiscussionBody = ({
51
+ eventId,
52
+ discussion: d,
53
+ onLogin,
54
+ pickImage,
55
+ pickImages,
56
+ previewKey,
57
+ locale,
58
+ renderReplyIcon,
59
+ renderImageCloseIcon,
60
+ onModerationFilter,
61
+ renderReactionIcon,
62
+ }: EventDiscussionViewProps) => {
63
+ const theme = useTheme()
64
+ const t = useAccountT(locale)
65
+ const list = useRef<FlatList<EventComment>>(null)
66
+ const firstContent = useRef(true)
67
+ const [actions, setActions] = useState<EventComment | null>(null)
68
+ const [reporting, setReporting] = useState<EventComment | null>(null)
69
+ const [moderating, setModerating] = useState<{
70
+ comment: EventComment
71
+ status: 'published' | 'hidden'
72
+ } | null>(null)
73
+ const [reason, setReason] = useState('')
74
+ const [blocksVisible, setBlocksVisible] = useState(false)
75
+ const [pickerError, setPickerError] = useState('')
76
+ const [imageOpen, setImageOpen] = useState(false)
77
+ const [pickerBusy, setPickerBusy] = useState(false)
78
+ const picking = useRef(false)
79
+ const text = { color: theme.text, fontFamily: theme.fontFamily }
80
+ const muted = { color: theme.muted, fontFamily: theme.fontFamily }
81
+ const closeActions = (work: (comment: EventComment) => void) => {
82
+ if (actions) work(actions)
83
+ setActions(null)
84
+ }
85
+ const chooseImage = async () => {
86
+ if (
87
+ (!pickImages && !pickImage) ||
88
+ d.isSubmitting ||
89
+ picking.current ||
90
+ d.imagesBase64.length >= EVENT_COMMENT_IMAGE_MAX_COUNT
91
+ )
92
+ return
93
+ picking.current = true
94
+ setPickerBusy(true)
95
+ setPickerError('')
96
+ try {
97
+ const values = pickImages
98
+ ? await pickImages(EVENT_COMMENT_IMAGE_MAX_COUNT - d.imagesBase64.length)
99
+ : [await pickImage?.()].filter((value): value is string => Boolean(value))
100
+ if (values?.length) d.appendImagesBase64(values)
101
+ } catch (error) {
102
+ setPickerError(
103
+ error instanceof Error
104
+ ? error.message
105
+ : 'Your image couldn’t be opened. Try again.'
106
+ )
107
+ } finally {
108
+ picking.current = false
109
+ setPickerBusy(false)
110
+ }
111
+ }
112
+ const send = async () => {
113
+ if (await d.submit()) list.current?.scrollToEnd({ animated: true })
114
+ }
115
+ const item = ({ item: comment }: { item: EventComment }) => {
116
+ const own = comment.canEdit || comment.canDelete
117
+ return (
118
+ <SwipeReply
119
+ enabled={d.canReplyTo(comment) && !d.isSubmitting && !imageOpen}
120
+ onReply={() => d.replyTo(comment)}
121
+ renderIcon={renderReplyIcon}
122
+ hint={t('Swipe left to reply')}
123
+ >
124
+ <View style={[s.row, own && s.ownRow]}>
125
+ {!own && (
126
+ <View style={[s.avatar, { backgroundColor: theme.surfaceMuted }]}>
127
+ {comment.author.avatarUrl ? (
128
+ <Image source={{ uri: comment.author.avatarUrl }} style={s.avatar} />
129
+ ) : (
130
+ <Text style={text}>{comment.author.name.slice(0, 1).toUpperCase()}</Text>
131
+ )}
132
+ </View>
133
+ )}
134
+ <View
135
+ style={[
136
+ s.bubble,
137
+ {
138
+ backgroundColor: theme.surface,
139
+ borderColor: own ? theme.accent : theme.border,
140
+ },
141
+ own && s.ownBubble,
142
+ ]}
143
+ >
144
+ <View style={s.authorRow}>
145
+ <Text style={[muted, s.author]}>
146
+ {comment.author.name}
147
+ {own ? t(' (you)') : ''}
148
+ </Text>
149
+ <Pressable
150
+ accessibilityRole="button"
151
+ accessibilityLabel={t('Actions for comment by {{name}}', {
152
+ name: comment.author.name,
153
+ })}
154
+ hitSlop={10}
155
+ onPress={() => setActions(comment)}
156
+ >
157
+ <Text style={muted}>•••</Text>
158
+ </Pressable>
159
+ </View>
160
+ {comment.replyTo && (
161
+ <Pressable
162
+ accessibilityRole="button"
163
+ onPress={() => d.replyTo(comment)}
164
+ style={[s.reply, { borderLeftColor: theme.accent }]}
165
+ >
166
+ <Text style={[muted, s.author]}>{comment.replyTo.authorName}</Text>
167
+ <Text style={muted} numberOfLines={2}>
168
+ {comment.replyTo.body}
169
+ </Text>
170
+ </Pressable>
171
+ )}
172
+ <CommentImages
173
+ key={`${comment.id}:${comment.revision}`}
174
+ eventId={eventId}
175
+ comment={comment}
176
+ previewKey={previewKey}
177
+ locale={locale}
178
+ onPreviewChange={setImageOpen}
179
+ renderCloseIcon={renderImageCloseIcon}
180
+ />
181
+ <Text selectable style={[text, s.body]}>
182
+ {comment.status === 'deleted' ? t('Comment removed') : comment.body}
183
+ </Text>
184
+ {comment.status === 'pending' && (
185
+ <Text accessibilityLiveRegion="polite" style={[muted, s.pending]}>
186
+ {t('Awaiting review')}
187
+ </Text>
188
+ )}
189
+ {comment.status === 'hidden' && (
190
+ <Text style={[muted, s.pending]}>{t('Hidden')}</Text>
191
+ )}
192
+ <View style={s.meta}>
193
+ <Text style={[muted, s.date]}>
194
+ {new Date(comment.createdAt).toLocaleString()}
195
+ {comment.editedAt ? t(' · edited') : ''}
196
+ </Text>
197
+ {(['up', 'down'] as const).map((reaction) => (
198
+ <Pressable
199
+ key={reaction}
200
+ accessibilityRole="button"
201
+ accessibilityLabel={
202
+ reaction === 'up' ? t('Like comment') : t('Dislike comment')
203
+ }
204
+ accessibilityState={{
205
+ selected: comment.reactions.mine === reaction,
206
+ disabled: d.isSubmitting,
207
+ }}
208
+ disabled={d.isSubmitting}
209
+ onPress={() =>
210
+ void d.react(
211
+ comment,
212
+ comment.reactions.mine === reaction ? null : reaction
213
+ )
214
+ }
215
+ style={s.reaction}
216
+ >
217
+ <View style={s.reactionInner}>
218
+ {renderReactionIcon ? (
219
+ renderReactionIcon(
220
+ reaction,
221
+ comment.reactions.mine === reaction,
222
+ comment.reactions.mine === reaction ? theme.accent : theme.muted
223
+ )
224
+ ) : (
225
+ <Text>{reaction === 'up' ? '👍' : '👎'}</Text>
226
+ )}
227
+ <Text
228
+ style={{
229
+ color:
230
+ comment.reactions.mine === reaction
231
+ ? theme.accent
232
+ : theme.muted,
233
+ }}
234
+ >
235
+ {comment.reactions[reaction] || ''}
236
+ </Text>
237
+ </View>
238
+ </Pressable>
239
+ ))}
240
+ </View>
241
+ {comment.replyCount > 0 && (
242
+ <Pressable
243
+ accessibilityRole="button"
244
+ disabled={!d.canReplyTo(comment)}
245
+ onPress={() => d.replyTo(comment)}
246
+ >
247
+ <Text style={[muted, s.pending]}>
248
+ {t(
249
+ comment.replyCount === 1
250
+ ? '{{count}} reply · Reply'
251
+ : '{{count}} replies · Reply',
252
+ { count: comment.replyCount }
253
+ )}
254
+ </Text>
255
+ </Pressable>
256
+ )}
257
+ </View>
258
+ </View>
259
+ </SwipeReply>
260
+ )
261
+ }
262
+ const actionButton = (
263
+ label: string,
264
+ onPress: () => void,
265
+ vars?: Record<string, string | number>
266
+ ) => (
267
+ <Pressable key={label} accessibilityRole="button" onPress={onPress} style={s.action}>
268
+ <Text style={text}>{t(label, vars)}</Text>
269
+ </Pressable>
270
+ )
271
+ const sheet = (visible: boolean, close: () => void, children: React.ReactNode) => (
272
+ <Modal transparent animationType="slide" visible={visible} onRequestClose={close}>
273
+ <View style={s.overlay}>
274
+ <Pressable
275
+ accessibilityLabel={t('Close menu')}
276
+ style={StyleSheet.absoluteFill}
277
+ onPress={close}
278
+ />
279
+ <View
280
+ style={[s.sheet, { backgroundColor: theme.surface, borderColor: theme.border }]}
281
+ >
282
+ {children}
283
+ {actionButton('Close', close)}
284
+ </View>
285
+ </View>
286
+ </Modal>
287
+ )
288
+ return (
289
+ <KeyboardAvoidingView
290
+ style={s.container}
291
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
292
+ >
293
+ {onModerationFilter && d.canModerate && (
294
+ <View style={s.filters}>
295
+ {actionButton('Comments', () => onModerationFilter(undefined))}
296
+ {actionButton('Pending', () => onModerationFilter('pending'))}
297
+ {actionButton('Reported', () => onModerationFilter('reported'))}
298
+ </View>
299
+ )}
300
+ {!!d.blockedAuthors.length && (
301
+ <Pressable
302
+ accessibilityRole="button"
303
+ onPress={() => setBlocksVisible(true)}
304
+ style={s.blocked}
305
+ >
306
+ <Text style={muted}>{t('Blocked people')}</Text>
307
+ </Pressable>
308
+ )}
309
+ {d.isLoading ? (
310
+ <ActivityIndicator style={s.loading} color={theme.accent} />
311
+ ) : (
312
+ <FlatList
313
+ ref={list}
314
+ data={d.comments}
315
+ keyExtractor={(comment) => comment.id}
316
+ renderItem={item}
317
+ contentContainerStyle={s.messages}
318
+ keyboardShouldPersistTaps="handled"
319
+ maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
320
+ onContentSizeChange={() => {
321
+ if (firstContent.current && d.comments.length) {
322
+ firstContent.current = false
323
+ list.current?.scrollToEnd({ animated: false })
324
+ }
325
+ }}
326
+ ListHeaderComponent={
327
+ d.hasMore ? actionButton('Earlier comments', d.loadMore) : null
328
+ }
329
+ ListEmptyComponent={
330
+ <Text style={[muted, s.empty]}>{t('Be the first to leave a comment.')}</Text>
331
+ }
332
+ refreshing={d.isLoading}
333
+ onRefresh={() => void d.refresh()}
334
+ />
335
+ )}
336
+ {(d.error || pickerError) && (
337
+ <View style={s.notice}>
338
+ <Text accessibilityRole="alert" style={{ color: theme.error }}>
339
+ {t(d.error || pickerError)}
340
+ </Text>
341
+ <Pressable accessibilityRole="button" onPress={() => void d.refresh()}>
342
+ <Text style={muted}>{t('Try again')}</Text>
343
+ </Pressable>
344
+ </View>
345
+ )}
346
+ {!!d.message && (
347
+ <Text accessibilityLiveRegion="polite" style={[muted, s.notice]}>
348
+ {t(d.message)}
349
+ </Text>
350
+ )}
351
+ {(d.replyingTo || d.editing) && (
352
+ <View style={[s.draftContext, { borderColor: theme.border }]}>
353
+ <Text style={muted} numberOfLines={1}>
354
+ {d.editing
355
+ ? t('Edit your comment')
356
+ : t('Reply to {{name}}', { name: d.replyingTo?.author.name ?? '' })}
357
+ </Text>
358
+ <Pressable accessibilityRole="button" onPress={d.cancelDraft}>
359
+ <Text style={muted}>{t('Cancel')}</Text>
360
+ </Pressable>
361
+ </View>
362
+ )}
363
+ <CommentImageDrafts
364
+ images={d.imagesBase64}
365
+ locale={locale}
366
+ disabled={d.isSubmitting}
367
+ onRemove={d.removeImage}
368
+ />
369
+ {d.canPost ? (
370
+ <View
371
+ style={[
372
+ s.composer,
373
+ { borderColor: theme.border, backgroundColor: theme.surface },
374
+ ]}
375
+ >
376
+ {(pickImages || pickImage) && !d.editing && (
377
+ <Pressable
378
+ accessibilityRole="button"
379
+ accessibilityLabel={t('Attach image')}
380
+ disabled={
381
+ d.isSubmitting ||
382
+ pickerBusy ||
383
+ d.imagesBase64.length >= EVENT_COMMENT_IMAGE_MAX_COUNT
384
+ }
385
+ onPress={() => void chooseImage()}
386
+ style={s.attach}
387
+ >
388
+ <Text style={[muted, s.plus]}>⊕</Text>
389
+ </Pressable>
390
+ )}
391
+ <TextInput
392
+ accessibilityLabel={t('Your comment')}
393
+ placeholder={t('Enter your comment…')}
394
+ placeholderTextColor={theme.muted}
395
+ multiline
396
+ maxLength={4000}
397
+ value={d.body}
398
+ onChangeText={d.setBody}
399
+ editable={!d.isSubmitting}
400
+ style={[text, s.input]}
401
+ />
402
+ <Pressable
403
+ accessibilityRole="button"
404
+ accessibilityLabel={d.editing ? t('Save comment') : t('Post comment')}
405
+ disabled={
406
+ d.isSubmitting ||
407
+ pickerBusy ||
408
+ (!d.body.trim() && !d.imagesBase64.length && !d.editing?.images.length)
409
+ }
410
+ onPress={() => void send()}
411
+ style={s.send}
412
+ >
413
+ {d.isSubmitting ? (
414
+ <ActivityIndicator color={theme.accent} />
415
+ ) : (
416
+ <Text style={{ color: theme.accent }}>
417
+ {d.editing ? t('Save') : t('Send')}
418
+ </Text>
419
+ )}
420
+ </Pressable>
421
+ </View>
422
+ ) : (
423
+ <Pressable
424
+ accessibilityRole="button"
425
+ onPress={onLogin}
426
+ style={[
427
+ s.composer,
428
+ { borderColor: theme.border, backgroundColor: theme.surface },
429
+ ]}
430
+ >
431
+ <Text style={text}>{t('Sign in to leave a comment')}</Text>
432
+ </Pressable>
433
+ )}
434
+ <Pressable
435
+ accessibilityRole="link"
436
+ style={s.support}
437
+ onPress={() => {
438
+ if (/^(https:\/\/|mailto:)/i.test(d.supportUrl))
439
+ void Linking.openURL(d.supportUrl).catch(() => undefined)
440
+ }}
441
+ >
442
+ <Text style={[muted, s.date]}>{t('Need help? Contact support')}</Text>
443
+ </Pressable>
444
+ {sheet(
445
+ Boolean(actions),
446
+ () => setActions(null),
447
+ actions && (
448
+ <>
449
+ {d.canReplyTo(actions) &&
450
+ actionButton('Reply', () => closeActions((comment) => d.replyTo(comment)))}
451
+ {actions.canEdit &&
452
+ actionButton('Edit', () => closeActions((comment) => d.edit(comment)))}
453
+ {actions.canDelete &&
454
+ actionButton('Delete', () =>
455
+ closeActions((comment) =>
456
+ Alert.alert(
457
+ t('Delete your comment?'),
458
+ t('Your comment will be removed from this conversation.'),
459
+ [
460
+ { text: t('Cancel'), style: 'cancel' },
461
+ {
462
+ text: t('Delete'),
463
+ style: 'destructive',
464
+ onPress: () => void d.remove(comment),
465
+ },
466
+ ]
467
+ )
468
+ )
469
+ )}
470
+ {d.canPost &&
471
+ actionButton('Report comment', () => closeActions(setReporting))}
472
+ {d.canPost &&
473
+ !actions.canEdit &&
474
+ actionButton('Block this person', () =>
475
+ closeActions((comment) =>
476
+ Alert.alert(
477
+ t('Block {{name}}?', { name: comment.author.name }),
478
+ t(
479
+ 'You won’t see their comments. You can unblock them from Blocked people.'
480
+ ),
481
+ [
482
+ { text: t('Cancel'), style: 'cancel' },
483
+ {
484
+ text: t('Block'),
485
+ onPress: () => void d.block(comment.author.id, true),
486
+ },
487
+ ]
488
+ )
489
+ )
490
+ )}
491
+ {actions.canModerate &&
492
+ actionButton(
493
+ actions.status === 'published' ? 'Hide comment' : 'Approve comment',
494
+ () =>
495
+ closeActions((comment) => {
496
+ setReason('')
497
+ setModerating({
498
+ comment,
499
+ status: comment.status === 'published' ? 'hidden' : 'published',
500
+ })
501
+ })
502
+ )}
503
+ </>
504
+ )
505
+ )}
506
+ {sheet(
507
+ Boolean(reporting),
508
+ () => setReporting(null),
509
+ <>
510
+ <Text style={text}>{t('Report comment')}</Text>
511
+ {(
512
+ [
513
+ ['spam', 'Spam'],
514
+ ['abuse', 'Harassment or abuse'],
515
+ ['unsafe', 'Unsafe content'],
516
+ ['other', 'Other'],
517
+ ] as Array<[EventCommentReportReason, string]>
518
+ ).map(([value, label]) =>
519
+ actionButton(label, () => {
520
+ if (reporting) void d.report(reporting, value)
521
+ setReporting(null)
522
+ })
523
+ )}
524
+ </>
525
+ )}
526
+ {sheet(
527
+ Boolean(moderating),
528
+ () => setModerating(null),
529
+ <>
530
+ <Text style={text}>
531
+ {moderating?.status === 'published'
532
+ ? t('Approve comment')
533
+ : t('Hide comment')}
534
+ </Text>
535
+ <TextInput
536
+ accessibilityLabel={t('Moderation reason')}
537
+ placeholder={t('Reason')}
538
+ placeholderTextColor={theme.muted}
539
+ value={reason}
540
+ onChangeText={setReason}
541
+ style={[text, s.reason]}
542
+ />
543
+ {actionButton('Confirm', () => {
544
+ if (moderating && reason.trim()) {
545
+ void d.moderate(moderating.comment, moderating.status, reason.trim())
546
+ setModerating(null)
547
+ }
548
+ })}
549
+ </>
550
+ )}
551
+ {sheet(
552
+ blocksVisible,
553
+ () => setBlocksVisible(false),
554
+ <>
555
+ <Text style={text}>{t('Blocked people')}</Text>
556
+ <FlatList
557
+ data={d.blockedAuthors}
558
+ keyExtractor={(person) => person.id}
559
+ renderItem={({ item: person }) =>
560
+ actionButton('Unblock {{name}}', () => void d.block(person.id, false), {
561
+ name: person.name,
562
+ })
563
+ }
564
+ />
565
+ </>
566
+ )}
567
+ </KeyboardAvoidingView>
568
+ )
569
+ }
570
+
571
+ /** Retire local dialogs and full-screen images along with their shared subject. */
572
+ export const EventDiscussionView = (props: EventDiscussionViewProps) => (
573
+ <EventDiscussionBody key={props.discussion.scopeKey} {...props} />
574
+ )
575
+
576
+ const s = StyleSheet.create({
577
+ container: { flex: 1 },
578
+ messages: { padding: 16, paddingBottom: 12 },
579
+ row: { flexDirection: 'row', alignItems: 'flex-end', marginBottom: 16, gap: 12 },
580
+ ownRow: { paddingRight: 44 },
581
+ avatar: {
582
+ width: 32,
583
+ height: 32,
584
+ borderRadius: 16,
585
+ alignItems: 'center',
586
+ justifyContent: 'center',
587
+ },
588
+ bubble: {
589
+ flex: 1,
590
+ borderWidth: 1,
591
+ borderRadius: 14,
592
+ borderBottomLeftRadius: 0,
593
+ padding: 15,
594
+ },
595
+ ownBubble: { borderBottomLeftRadius: 14, borderBottomRightRadius: 0 },
596
+ authorRow: {
597
+ flexDirection: 'row',
598
+ alignItems: 'center',
599
+ justifyContent: 'space-between',
600
+ marginBottom: 6,
601
+ gap: 8,
602
+ },
603
+ author: { fontSize: 12, flexShrink: 1 },
604
+ body: { fontSize: 14, lineHeight: 22 },
605
+ meta: { flexDirection: 'row', alignItems: 'center', marginTop: 10, gap: 10 },
606
+ date: { fontSize: 11, flexShrink: 1 },
607
+ reaction: { minHeight: 28, justifyContent: 'center' },
608
+ reactionInner: { flexDirection: 'row', alignItems: 'center', gap: 4 },
609
+ pending: { fontSize: 12, marginTop: 8 },
610
+ reply: { borderLeftWidth: 2, paddingLeft: 8, marginBottom: 8 },
611
+ composer: {
612
+ flexDirection: 'row',
613
+ alignItems: 'center',
614
+ borderWidth: 1,
615
+ borderRadius: 32,
616
+ minHeight: 64,
617
+ marginHorizontal: 16,
618
+ paddingHorizontal: 14,
619
+ paddingVertical: 10,
620
+ },
621
+ input: { flex: 1, maxHeight: 140, fontSize: 14, paddingVertical: 8 },
622
+ attach: { paddingRight: 10, paddingVertical: 6 },
623
+ plus: { fontSize: 24 },
624
+ send: { paddingLeft: 10, paddingVertical: 10 },
625
+ support: { padding: 8, alignItems: 'center' },
626
+ draftContext: {
627
+ marginHorizontal: 20,
628
+ padding: 10,
629
+ borderTopWidth: 1,
630
+ flexDirection: 'row',
631
+ justifyContent: 'space-between',
632
+ },
633
+ notice: { marginHorizontal: 20, marginVertical: 8 },
634
+ empty: { textAlign: 'center', paddingVertical: 32 },
635
+ loading: { margin: 32 },
636
+ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end' },
637
+ sheet: {
638
+ borderTopLeftRadius: 24,
639
+ borderTopRightRadius: 24,
640
+ borderWidth: 1,
641
+ padding: 20,
642
+ paddingBottom: 32,
643
+ maxHeight: '80%',
644
+ },
645
+ action: { paddingVertical: 14, paddingHorizontal: 8 },
646
+ filters: { flexDirection: 'row', justifyContent: 'space-around' },
647
+ blocked: { alignItems: 'flex-end', paddingHorizontal: 20 },
648
+ reason: {
649
+ borderWidth: 1,
650
+ borderColor: '#777',
651
+ borderRadius: 8,
652
+ padding: 12,
653
+ marginTop: 16,
654
+ },
655
+ })