forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
1import {
2 useCallback,
3 useLayoutEffect,
4 useMemo,
5 useReducer,
6 useRef,
7 useState,
8} from 'react'
9import {LayoutAnimation, type TextInput, View} from 'react-native'
10import {moderateProfile, type ModerationOpts} from '@atproto/api'
11import {Trans, useLingui} from '@lingui/react/macro'
12
13import {sanitizeDisplayName} from '#/lib/strings/display-names'
14import {sanitizeHandle} from '#/lib/strings/handles'
15import {useModerationOpts} from '#/state/preferences/moderation-opts'
16import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
17import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
18import {useSession} from '#/state/session'
19import {type ListMethods} from '#/view/com/util/List'
20import {android, atoms as a, native, useTheme, web} from '#/alf'
21import {Button, ButtonIcon, ButtonText} from '#/components/Button'
22import * as Dialog from '#/components/Dialog'
23import {canBeMessaged} from '#/components/dms/util'
24import * as TextField from '#/components/forms/TextField'
25import * as Toggle from '#/components/forms/Toggle'
26import {
27 ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon,
28 ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon,
29} from '#/components/icons/Arrow'
30import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
31import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
32import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
33import * as ProfileCard from '#/components/ProfileCard'
34import {Text} from '#/components/Typography'
35import {IS_NATIVE, IS_WEB} from '#/env'
36import type * as bsky from '#/types/bsky'
37import {ChatProfileTabs} from './ChatProfileTabs'
38import {EmptyMemberList} from './components/EmptyMemberList'
39import {GroupChatProfileCard} from './components/GroupChatProfileCard'
40import {ProfileCardSkeleton} from './components/ProfileCardSkeleton'
41import {UserLabel} from './components/UserLabel'
42import {UserSearchInput} from './components/UserSearchInput'
43
44type NewGroupChatItem = {
45 type: 'newGroupChat'
46 key: string
47}
48
49type LabelItem = {
50 type: 'label'
51 key: string
52 message: string
53}
54
55type ProfileItem = {
56 type: 'profile'
57 key: string
58 profile: bsky.profile.AnyProfileView
59}
60
61type EmptyItem = {
62 type: 'empty'
63 key: string
64 message: string
65}
66
67type PlaceholderItem = {
68 type: 'placeholder'
69 key: string
70}
71
72type ErrorItem = {
73 type: 'error'
74 key: string
75}
76
77type Item =
78 | NewGroupChatItem
79 | LabelItem
80 | ProfileItem
81 | EmptyItem
82 | PlaceholderItem
83 | ErrorItem
84
85enum ChatState {
86 NEW_CHAT,
87 NEW_GROUP_CHAT,
88 GROUP_NAME,
89}
90
91export type State = {
92 chatState: ChatState
93 screenTitle: string
94 groupChatDids: string[]
95 groupChatProfiles: bsky.profile.AnyProfileView[]
96 groupName: string
97}
98
99export type Action =
100 | {
101 type: 'startNewGroupChat'
102 screenTitle: string
103 }
104 | {
105 type: 'setDids'
106 groupChatDids: string[]
107 groupChatProfiles: bsky.profile.AnyProfileView[]
108 }
109 | {
110 type: 'removeDids'
111 groupChatDids: string[]
112 groupChatProfiles: bsky.profile.AnyProfileView[]
113 }
114 | {
115 type: 'startNameGroup'
116 screenTitle: string
117 }
118 | {
119 type: 'nameGroup'
120 groupName: string
121 }
122 | {
123 type: 'goBackFromNewGroupChat'
124 screenTitle: string
125 }
126 | {
127 type: 'goBackFromGroupName'
128 screenTitle: string
129 }
130
131function reducer(state: State, action: Action): State {
132 switch (action.type) {
133 case 'startNewGroupChat': {
134 return {
135 ...state,
136 chatState: ChatState.NEW_GROUP_CHAT,
137 screenTitle: action.screenTitle,
138 groupChatDids: [],
139 groupChatProfiles: [],
140 groupName: '',
141 }
142 }
143 case 'setDids': {
144 return {
145 ...state,
146 groupChatDids: action.groupChatDids,
147 groupChatProfiles: action.groupChatProfiles,
148 }
149 }
150 case 'removeDids': {
151 return {
152 ...state,
153 groupChatDids: action.groupChatDids,
154 groupChatProfiles: action.groupChatProfiles,
155 }
156 }
157 case 'startNameGroup': {
158 return {
159 ...state,
160 chatState: ChatState.GROUP_NAME,
161 screenTitle: action.screenTitle,
162 }
163 }
164 case 'nameGroup': {
165 return {
166 ...state,
167 groupName: action.groupName,
168 }
169 }
170 case 'goBackFromNewGroupChat': {
171 return {
172 ...state,
173 chatState: ChatState.NEW_CHAT,
174 screenTitle: action.screenTitle,
175 groupChatDids: [],
176 groupChatProfiles: [],
177 groupName: '',
178 }
179 }
180 case 'goBackFromGroupName': {
181 return {
182 ...state,
183 chatState: ChatState.NEW_GROUP_CHAT,
184 screenTitle: action.screenTitle,
185 groupName: '',
186 }
187 }
188 }
189}
190
191export function InitiateChatFlow({
192 title,
193 onSelectChat,
194 onSelectGroupChat,
195}: {
196 title: string
197 onSelectChat: (did: string) => void
198 onSelectGroupChat: (dids: string[], groupName: string) => void
199}) {
200 const t = useTheme()
201 const {t: l} = useLingui()
202 const moderationOpts = useModerationOpts()
203 const control = Dialog.useDialogContext()
204 const [headerHeight, setHeaderHeight] = useState(0)
205 const [footerHeight, setFooterHeight] = useState(0)
206 const listRef = useRef<ListMethods>(null)
207 const {currentAccount} = useSession()
208 const inputRef = useRef<TextInput>(null)
209
210 const [searchText, setSearchText] = useState('')
211
212 const {
213 data: results,
214 isError,
215 isFetching,
216 } = useActorAutocompleteQuery(searchText, true, 12)
217 const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
218
219 const [
220 {chatState, screenTitle, groupChatDids, groupChatProfiles, groupName},
221 dispatch,
222 ] = useReducer(reducer, {
223 chatState: ChatState.NEW_CHAT,
224 screenTitle: title,
225 groupChatDids: [],
226 groupChatProfiles: [],
227 groupName: '',
228 })
229
230 const newGroupChatTitle = l`New group chat`
231 const groupNameTitle = l`Group name`
232
233 const onRemoveDid = useCallback(
234 (did: string) => {
235 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
236 dispatch({
237 type: 'removeDids',
238 groupChatDids: groupChatDids.filter(d => d !== did),
239 groupChatProfiles: groupChatProfiles.filter(
240 profile => profile.did !== did,
241 ),
242 })
243 },
244 [groupChatDids, groupChatProfiles],
245 )
246
247 const items = useMemo(() => {
248 let _items: Item[] = []
249
250 if (isError) {
251 _items.push({
252 type: 'empty',
253 key: 'empty',
254 message: l`We’re having network issues, try again`,
255 })
256 } else if (chatState === ChatState.GROUP_NAME) {
257 _items = groupChatProfiles.map(profile => ({
258 type: 'profile',
259 key: profile.did,
260 profile,
261 }))
262 _items.unshift({
263 type: 'label',
264 key: 'members',
265 message: l`New group chat with:`,
266 })
267 } else if (searchText.length) {
268 if (results?.length) {
269 for (const profile of results) {
270 if (profile.did === currentAccount?.did) continue
271 _items.push({
272 type: 'profile',
273 key: profile.did,
274 profile,
275 })
276 }
277
278 _items = _items.sort(item => {
279 return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
280 })
281 }
282 } else {
283 const placeholders: Item[] = Array(10)
284 .fill(0)
285 .map((__, i) => ({
286 type: 'placeholder',
287 key: i + '',
288 }))
289
290 if (follows) {
291 for (const page of follows.pages) {
292 for (const profile of page.follows) {
293 _items.push({
294 type: 'profile',
295 key: profile.did,
296 profile,
297 })
298 }
299 }
300
301 _items = _items.sort(item => {
302 return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
303 })
304 } else {
305 _items.push(...placeholders)
306 }
307 }
308
309 if (
310 searchText === '' &&
311 (chatState === ChatState.NEW_CHAT ||
312 chatState === ChatState.NEW_GROUP_CHAT)
313 ) {
314 _items.unshift({
315 type: 'label',
316 key: 'suggested',
317 message: l`Suggested`,
318 })
319 }
320
321 if (chatState === ChatState.NEW_CHAT && searchText === '') {
322 _items.unshift({type: 'newGroupChat', key: 'newGroupChat'})
323 }
324
325 return _items
326 }, [
327 isError,
328 chatState,
329 searchText,
330 l,
331 groupChatProfiles,
332 results,
333 currentAccount?.did,
334 follows,
335 ])
336
337 if (searchText && !isFetching && !items.length && !isError) {
338 items.push({type: 'empty', key: 'empty', message: l`No results`})
339 }
340
341 const handlePressBack = useCallback(() => {
342 switch (chatState) {
343 case ChatState.NEW_CHAT:
344 control.close()
345 break
346 case ChatState.NEW_GROUP_CHAT:
347 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
348 dispatch({type: 'goBackFromNewGroupChat', screenTitle: title})
349 setSearchText('')
350 break
351 case ChatState.GROUP_NAME:
352 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
353 dispatch({type: 'goBackFromGroupName', screenTitle: newGroupChatTitle})
354 break
355 }
356 }, [chatState, control, newGroupChatTitle, title])
357
358 const handlePressNewGroupChat = useCallback(() => {
359 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
360 dispatch({type: 'startNewGroupChat', screenTitle: newGroupChatTitle})
361 }, [newGroupChatTitle])
362
363 const handlePressNext = useCallback(() => {
364 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
365 dispatch({type: 'startNameGroup', screenTitle: groupNameTitle})
366 setSearchText('')
367 }, [groupNameTitle])
368
369 const handlePressConfirm = useCallback(() => {
370 onSelectGroupChat(groupChatDids, groupName)
371 }, [groupChatDids, groupName, onSelectGroupChat])
372
373 const setGroupName = (newGroupName: string) => {
374 dispatch({type: 'nameGroup', groupName: newGroupName})
375 }
376
377 const renderItems = useCallback(
378 ({item}: {item: Item}) => {
379 switch (item.type) {
380 case 'newGroupChat': {
381 return (
382 <NewGroupChatButton
383 key={item.key}
384 onPress={handlePressNewGroupChat}
385 />
386 )
387 }
388 case 'label': {
389 return <UserLabel key={item.key} message={item.message} />
390 }
391 case 'profile': {
392 switch (chatState) {
393 case ChatState.NEW_CHAT:
394 return (
395 <DefaultProfileCard
396 key={item.key}
397 profile={item.profile}
398 moderationOpts={moderationOpts!}
399 onPress={onSelectChat}
400 />
401 )
402 case ChatState.NEW_GROUP_CHAT:
403 return (
404 <GroupChatProfileCard
405 key={item.key}
406 profile={item.profile}
407 moderationOpts={moderationOpts!}
408 />
409 )
410 case ChatState.GROUP_NAME:
411 return (
412 <GroupChatMemberProfileCard
413 key={item.key}
414 profile={item.profile}
415 moderationOpts={moderationOpts!}
416 />
417 )
418 }
419 }
420 case 'placeholder': {
421 return <ProfileCardSkeleton key={item.key} />
422 }
423 case 'empty': {
424 return <EmptyMemberList key={item.key} message={item.message} />
425 }
426 default:
427 return null
428 }
429 },
430 [chatState, handlePressNewGroupChat, moderationOpts, onSelectChat],
431 )
432
433 useLayoutEffect(() => {
434 if (IS_WEB) {
435 setImmediate(() => {
436 inputRef?.current?.focus()
437 })
438 }
439 }, [])
440
441 let buttonLabel = l`Continue to group name`
442 let buttonText = l`Next`
443 let handleButtonPress = handlePressNext
444 let showButton =
445 chatState === ChatState.NEW_GROUP_CHAT && groupChatProfiles.length > 0
446 let isButtonDisabled = !showButton
447 switch (chatState) {
448 case ChatState.GROUP_NAME:
449 buttonLabel = l`Create group chat`
450 buttonText = l`Create`
451 handleButtonPress = handlePressConfirm
452 showButton = true
453 isButtonDisabled = groupName === ''
454 break
455 }
456
457 const showChatProfileTabs =
458 chatState === ChatState.NEW_GROUP_CHAT && groupChatProfiles.length > 0
459
460 const listHeader = useMemo(
461 () => (
462 <View onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
463 <View
464 style={[
465 a.relative,
466 web(a.pt_lg),
467 native(a.pt_4xl),
468 android({
469 borderTopLeftRadius: a.rounded_md.borderRadius,
470 borderTopRightRadius: a.rounded_md.borderRadius,
471 }),
472 a.px_lg,
473 chatState !== ChatState.GROUP_NAME ? a.pb_xs : a.pb_lg,
474 chatState !== ChatState.GROUP_NAME && a.border_b,
475 t.atoms.border_contrast_low,
476 t.atoms.bg,
477 ]}>
478 <View
479 style={[
480 a.flex_row,
481 a.gap_sm,
482 a.relative,
483 a.align_center,
484 a.justify_between,
485 web(a.pb_lg),
486 ]}>
487 {IS_NATIVE ? (
488 <Button
489 label={l`Back`}
490 size="large"
491 shape="round"
492 variant="ghost"
493 color="secondary"
494 style={[native([a.absolute, a.z_20])]}
495 onPress={handlePressBack}>
496 <ButtonIcon icon={ArrowLeftIcon} size="lg" />
497 </Button>
498 ) : null}
499 <Text
500 style={[
501 a.flex_grow,
502 a.z_10,
503 a.text_lg,
504 a.font_bold,
505 a.leading_tight,
506 t.atoms.text_contrast_high,
507 a.text_center,
508 a.px_5xl,
509 ]}>
510 {screenTitle}
511 </Text>
512 {IS_WEB ? (
513 <Button
514 label={l`Close`}
515 size="small"
516 shape="round"
517 variant="ghost"
518 color="secondary"
519 style={[a.absolute, a.z_20, {right: -4}]}
520 onPress={() => control.close()}>
521 <ButtonIcon icon={XIcon} size="lg" />
522 </Button>
523 ) : showButton ? (
524 <Button
525 label={buttonLabel}
526 size="small"
527 color="primary"
528 style={[
529 native([
530 a.absolute,
531 a.z_20,
532 {
533 right: 8,
534 },
535 ]),
536 ]}
537 disabled={isButtonDisabled}
538 onPress={handleButtonPress}>
539 <ButtonText>{buttonText}</ButtonText>
540 </Button>
541 ) : null}
542 </View>
543 <View style={[web(a.pt_xs), native(a.pt_md)]}>
544 {chatState === ChatState.GROUP_NAME ? (
545 <View
546 style={[a.w_full, a.relative, web(a.pt_md), native(a.pt_xl)]}>
547 <TextField.Root>
548 <TextField.Input
549 label={l`Group name`}
550 value={groupName}
551 returnKeyType="next"
552 keyboardAppearance={t.scheme}
553 selectTextOnFocus={IS_NATIVE}
554 autoFocus={false}
555 accessibilityRole="text"
556 autoCorrect={false}
557 autoComplete="off"
558 autoCapitalize="none"
559 onChangeText={setGroupName}
560 onSubmitEditing={
561 isButtonDisabled ? undefined : handleButtonPress
562 }
563 />
564 </TextField.Root>
565 </View>
566 ) : (
567 <UserSearchInput
568 inputRef={inputRef}
569 value={searchText}
570 onChangeText={text => {
571 setSearchText(text)
572 listRef.current?.scrollToOffset({offset: 0, animated: false})
573 }}
574 onEscape={control.close}
575 />
576 )}
577 </View>
578 </View>
579 {showChatProfileTabs ? (
580 <View style={[a.pb_sm, a.pt_md, t.atoms.bg]}>
581 <ChatProfileTabs
582 testID="newGroupChatMembers"
583 profiles={groupChatProfiles}
584 onRemove={onRemoveDid}
585 />
586 </View>
587 ) : null}
588 </View>
589 ),
590 [
591 chatState,
592 t.atoms.border_contrast_low,
593 t.atoms.bg,
594 t.atoms.text_contrast_high,
595 t.scheme,
596 l,
597 handlePressBack,
598 screenTitle,
599 showButton,
600 buttonLabel,
601 isButtonDisabled,
602 handleButtonPress,
603 buttonText,
604 groupName,
605 searchText,
606 control,
607 showChatProfileTabs,
608 groupChatProfiles,
609 onRemoveDid,
610 ],
611 )
612
613 const setGroupChatMembers = (dids: string[]) => {
614 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
615
616 const added = dids.filter(d => !groupChatDids.includes(d))
617 const removed = groupChatDids.filter(d => !dids.includes(d))
618 const newDids = [
619 ...groupChatDids.filter(d => !removed.includes(d)),
620 ...added,
621 ]
622
623 const kept = groupChatProfiles.filter(p => dids.includes(p.did))
624 const keptDids = new Set(kept.map(p => p.did))
625 const addedProfiles = items
626 .filter(
627 (item): item is ProfileItem =>
628 item.type === 'profile' &&
629 dids.includes(item.profile.did) &&
630 !keptDids.has(item.profile.did),
631 )
632 .map(item => item.profile)
633 .sort((a, b) => dids.indexOf(a.did) - dids.indexOf(b.did))
634
635 dispatch({
636 type: 'setDids',
637 groupChatDids: newDids,
638 groupChatProfiles: [...kept, ...addedProfiles],
639 })
640 }
641
642 return (
643 <Toggle.Group
644 values={groupChatDids}
645 onChange={setGroupChatMembers}
646 type="checkbox"
647 label={
648 chatState === ChatState.NEW_GROUP_CHAT
649 ? l`Select group chat members`
650 : l`Start chat`
651 }
652 style={web([a.contents])}>
653 <Dialog.InnerFlatList
654 ref={listRef}
655 data={items}
656 renderItem={renderItems}
657 ListHeaderComponent={listHeader}
658 stickyHeaderIndices={[0]}
659 keyExtractor={(item: Item) => item.key}
660 style={[
661 web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
662 native({height: '100%'}),
663 ]}
664 webInnerContentContainerStyle={[a.py_0, {paddingBottom: footerHeight}]}
665 webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
666 scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}}
667 keyboardDismissMode="on-drag"
668 footer={
669 IS_WEB && chatState !== ChatState.NEW_CHAT ? (
670 <Dialog.FlatListFooter
671 onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}>
672 <View style={[a.flex_row, a.align_center, a.justify_between]}>
673 <Button
674 label={l`Back`}
675 size="small"
676 color="secondary"
677 onPress={handlePressBack}>
678 <ButtonIcon icon={ArrowLeftIcon} size="md" />
679 <ButtonText>
680 {' '}
681 <Trans>Back</Trans>
682 </ButtonText>
683 </Button>
684 <Button
685 label={buttonLabel}
686 size="small"
687 color="primary"
688 disabled={isButtonDisabled}
689 onPress={handleButtonPress}>
690 <ButtonText>{buttonText} </ButtonText>
691 {chatState !== ChatState.GROUP_NAME ? (
692 <ButtonIcon icon={ArrowRightIcon} size="md" />
693 ) : null}
694 </Button>
695 </View>
696 </Dialog.FlatListFooter>
697 ) : null
698 }
699 />
700 </Toggle.Group>
701 )
702}
703
704function NewGroupChatButton({onPress}: {onPress: () => void}) {
705 const t = useTheme()
706 const {t: l} = useLingui()
707
708 const handleOnPress = () => {
709 onPress()
710 }
711
712 return (
713 <Button label={l`New group chat`} onPress={handleOnPress}>
714 {({hovered, pressed, focused}) => (
715 <View
716 style={[
717 a.px_lg,
718 a.py_md,
719 a.flex_row,
720 a.flex_1,
721 a.justify_between,
722 a.align_center,
723 a.gap_sm,
724 pressed || focused || hovered ? t.atoms.bg_contrast_25 : t.atoms.bg,
725 ]}>
726 <View
727 style={[
728 a.rounded_full,
729 a.justify_center,
730 a.align_center,
731 {
732 backgroundColor: t.palette.contrast_50,
733 padding: 12,
734 },
735 ]}>
736 <PersonGroupIcon size="md" fill={t.palette.contrast_1000} />
737 </View>
738 <View style={[a.flex_grow]}>
739 <Text
740 style={[a.text_md, a.font_medium, a.leading_snug, t.atoms.text]}>
741 <Trans>New group chat</Trans>
742 </Text>
743 </View>
744 <ChevronRightIcon size="md" fill={t.palette.contrast_1000} />
745 </View>
746 )}
747 </Button>
748 )
749}
750
751function DefaultProfileCard({
752 profile,
753 moderationOpts,
754 onPress,
755}: {
756 profile: bsky.profile.AnyProfileView
757 moderationOpts: ModerationOpts
758 onPress: (did: string) => void
759}) {
760 const t = useTheme()
761 const {t: l} = useLingui()
762 const enabled = canBeMessaged(profile)
763 const moderation = moderateProfile(profile, moderationOpts)
764 const handle = sanitizeHandle(profile.handle, '@')
765 const displayName = sanitizeDisplayName(
766 profile.displayName || sanitizeHandle(profile.handle),
767 moderation.ui('displayName'),
768 )
769
770 const handleOnPress = useCallback(() => {
771 onPress(profile.did)
772 }, [onPress, profile.did])
773
774 return (
775 <Button
776 disabled={!enabled}
777 label={l`Start chat with ${displayName}`}
778 onPress={handleOnPress}>
779 {({hovered, pressed, focused}) => (
780 <View
781 style={[
782 a.flex_1,
783 a.py_sm,
784 a.px_lg,
785 !enabled
786 ? {opacity: 0.5}
787 : pressed || focused || hovered
788 ? t.atoms.bg_contrast_25
789 : t.atoms.bg,
790 ]}>
791 <ProfileCard.Header>
792 <ProfileCard.Avatar
793 profile={profile}
794 moderationOpts={moderationOpts}
795 size={44}
796 disabledPreview
797 />
798 <View style={[a.flex_1]}>
799 <ProfileCard.Name
800 profile={profile}
801 moderationOpts={moderationOpts}
802 />
803 {enabled ? (
804 <ProfileCard.Handle profile={profile} />
805 ) : (
806 <Text
807 style={[a.leading_snug, t.atoms.text_contrast_high]}
808 numberOfLines={2}>
809 <Trans>{handle} can’t be messaged</Trans>
810 </Text>
811 )}
812 </View>
813 </ProfileCard.Header>
814 </View>
815 )}
816 </Button>
817 )
818}
819
820function GroupChatMemberProfileCard({
821 profile,
822 moderationOpts,
823}: {
824 profile: bsky.profile.AnyProfileView
825 moderationOpts: ModerationOpts
826}) {
827 const t = useTheme()
828 const enabled = canBeMessaged(profile)
829 const handle = sanitizeHandle(profile.handle, '@')
830
831 return (
832 <View style={[a.flex_1, a.py_sm, a.px_lg, t.atoms.bg]}>
833 <ProfileCard.Header>
834 <ProfileCard.Avatar
835 profile={profile}
836 moderationOpts={moderationOpts}
837 size={44}
838 disabledPreview
839 />
840 <View style={[a.flex_1]}>
841 <ProfileCard.Name profile={profile} moderationOpts={moderationOpts} />
842 {enabled ? (
843 <ProfileCard.Handle profile={profile} />
844 ) : (
845 <Text
846 style={[a.leading_snug, t.atoms.text_contrast_high]}
847 numberOfLines={2}>
848 <Trans>{handle} can’t be messaged</Trans>
849 </Text>
850 )}
851 </View>
852 </ProfileCard.Header>
853 </View>
854 )
855}