Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 225 lines 6.8 kB view raw
1import {useMemo} from 'react' 2import {View} from 'react-native' 3import { 4 ChatBskyConvoDefs, 5 moderateProfile, 6 type ModerationOpts, 7} from '@atproto/api' 8import {useLingui} from '@lingui/react/macro' 9import {useNavigation} from '@react-navigation/native' 10 11import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 12import {makeProfileLink} from '#/lib/routes/links' 13import {type NavigationProp} from '#/lib/routes/types' 14import {useProfileShadow} from '#/state/cache/profile-shadow' 15import {useModerationOpts} from '#/state/preferences/moderation-opts' 16import {useSession} from '#/state/session' 17import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 18import {atoms as a, useTheme} from '#/alf' 19import {AvatarBubbles} from '#/components/AvatarBubbles' 20import {Button, ButtonIcon} from '#/components/Button' 21import {ConvoMenu} from '#/components/dms/ConvoMenu' 22import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2' 23import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid' 24import * as Layout from '#/components/Layout' 25import {Link} from '#/components/Link' 26import {ProfileBadges} from '#/components/ProfileBadges' 27import {Text} from '#/components/Typography' 28import {IS_LIQUID_GLASS, IS_WEB} from '#/env' 29import {type ConvoWithDetails} from './util' 30 31const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE 32 33export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) { 34 const t = useTheme() 35 const moderationOpts = useModerationOpts() 36 37 return ( 38 <Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}> 39 <View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}> 40 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 41 <Layout.Header.BackButton /> 42 </View> 43 {convo && moderationOpts ? ( 44 convo.kind === 'direct' ? ( 45 <ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} /> 46 ) : ( 47 <GroupHeaderReady convo={convo} /> 48 ) 49 ) : ( 50 <> 51 <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}> 52 <View 53 style={[ 54 {width: PFP_SIZE, height: PFP_SIZE}, 55 a.rounded_full, 56 t.atoms.bg_contrast_25, 57 ]} 58 /> 59 <View style={a.gap_xs}> 60 <View 61 style={[ 62 {width: 150, height: 16}, 63 a.rounded_xs, 64 t.atoms.bg_contrast_25, 65 a.mt_xs, 66 ]} 67 /> 68 </View> 69 </View> 70 71 <Layout.Header.Slot /> 72 </> 73 )} 74 </View> 75 </Layout.Header.Outer> 76 ) 77} 78 79function ProfileHeaderReady({ 80 convo, 81 moderationOpts, 82}: { 83 convo: Extract<ConvoWithDetails, {kind: 'direct'}> 84 moderationOpts: ModerationOpts 85}) { 86 const {t: l} = useLingui() 87 const {currentAccount} = useSession() 88 const profile = useProfileShadow(convo.primaryMember) 89 90 const moderation = moderateProfile(profile, moderationOpts) 91 92 const blockInfo = useMemo(() => { 93 const modui = moderation.ui('profileView') 94 const blocks = modui.alerts.filter(alert => alert.type === 'blocking') 95 const listBlocks = blocks.filter(alert => alert.source.type === 'list') 96 const userBlock = blocks.find(alert => alert.source.type === 'user') 97 return { 98 listBlocks, 99 userBlock, 100 } 101 }, [moderation]) 102 103 const isDeletedAccount = profile?.handle === 'missing.invalid' 104 const displayName = isDeletedAccount 105 ? l`Deleted Account` 106 : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) 107 108 const latestReportableMessage = 109 ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) && 110 convo.view.lastMessage.sender?.did !== currentAccount?.did 111 ? convo.view.lastMessage 112 : undefined 113 114 return ( 115 <Wrapper 116 heading={ 117 <Link 118 label={l`View ${displayName}鈥檚 profile`} 119 style={[a.flex_row, a.gap_md, a.flex_1, a.pr_md]} 120 to={makeProfileLink(profile)}> 121 <PreviewableUserAvatar 122 size={PFP_SIZE} 123 profile={profile} 124 moderation={moderation.ui('avatar')} 125 disableHoverCard={moderation.blocked} 126 /> 127 <View style={[a.flex_row, a.align_center, a.flex_1]}> 128 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}> 129 {displayName} 130 </Text> 131 <ProfileBadges profile={profile} size="md" style={[a.pl_xs]} /> 132 </View> 133 </Link> 134 } 135 muted={convo.view.muted} 136 settings={ 137 <ConvoMenu 138 convo={convo.view} 139 profile={profile} 140 currentScreen="conversation" 141 blockInfo={blockInfo} 142 latestReportableMessage={latestReportableMessage} 143 /> 144 } 145 /> 146 ) 147} 148 149function GroupHeaderReady({ 150 convo, 151}: { 152 convo: Extract<ConvoWithDetails, {kind: 'group'}> 153}) { 154 const {t: l} = useLingui() 155 156 const navigation = useNavigation<NavigationProp>() 157 158 const handleNavigateToSettings = () => { 159 navigation.navigate('MessagesConversationSettings', { 160 conversation: convo.view.id, 161 }) 162 } 163 164 return ( 165 <Wrapper 166 heading={ 167 <> 168 <AvatarBubbles size="small" profiles={convo.members} /> 169 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}> 170 {convo.details.name} 171 </Text> 172 </> 173 } 174 muted={convo.view.muted} 175 settings={ 176 <Button 177 label={l`Open group chat settings`} 178 size="small" 179 color="secondary" 180 shape="round" 181 variant="ghost" 182 style={[a.bg_transparent]} 183 onPress={handleNavigateToSettings}> 184 <ButtonIcon icon={DotsHorizontalIcon} size="md" /> 185 </Button> 186 } 187 /> 188 ) 189} 190 191function Wrapper({ 192 heading, 193 muted, 194 settings, 195}: { 196 heading: React.ReactNode 197 muted: boolean 198 settings: React.ReactNode 199}) { 200 return ( 201 <View style={[a.flex_1]}> 202 <View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}> 203 <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}> 204 {heading} 205 <MuteStatus muted={muted} /> 206 </View> 207 208 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 209 <Layout.Header.Slot>{settings}</Layout.Header.Slot> 210 </View> 211 </View> 212 </View> 213 ) 214} 215 216function MuteStatus({muted}: {muted: boolean}) { 217 const t = useTheme() 218 219 return muted ? ( 220 <> 221 <Text style={[a.text_md, t.atoms.text_contrast_medium]}> &middot; </Text> 222 <BellOffIcon size="sm" style={t.atoms.text_contrast_medium} /> 223 </> 224 ) : undefined 225}