Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 282 lines 8.2 kB view raw
1import {useMemo} from 'react' 2import {View} from 'react-native' 3import { 4 type AppBskyActorDefs, 5 type ModerationCause, 6 type ModerationDecision, 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 {logger} from '#/logger' 15import {type Shadow} from '#/state/cache/profile-shadow' 16import { 17 type ActiveConvoStates, 18 isConvoActive, 19 useConvo, 20} from '#/state/messages/convo' 21import {type ConvoItem} from '#/state/messages/convo/types' 22import {useSession} from '#/state/session' 23import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 24import {atoms as a, useTheme} from '#/alf' 25import {AvatarBubbles} from '#/components/AvatarBubbles' 26import {Button, ButtonIcon} from '#/components/Button' 27import {ConvoMenu} from '#/components/dms/ConvoMenu' 28import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2' 29import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid' 30import * as Layout from '#/components/Layout' 31import {Link} from '#/components/Link' 32import {ProfileBadges} from '#/components/ProfileBadges' 33import {Text} from '#/components/Typography' 34import {IS_LIQUID_GLASS, IS_WEB} from '#/env' 35 36const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE 37 38export function MessagesListHeader({ 39 profile, 40 moderation, 41}: { 42 profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed> 43 moderation?: ModerationDecision | null 44}) { 45 const t = useTheme() 46 47 const convoState = useConvo() 48 const isGroupChat = convoState?.isGroup?.() 49 50 const blockInfo = useMemo(() => { 51 if (!moderation) return 52 const modui = moderation.ui('profileView') 53 const blocks = modui.alerts.filter(alert => alert.type === 'blocking') 54 const listBlocks = blocks.filter(alert => alert.source.type === 'list') 55 const userBlock = blocks.find(alert => alert.source.type === 'user') 56 return { 57 listBlocks, 58 userBlock, 59 } 60 }, [moderation]) 61 62 return ( 63 <Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}> 64 <View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}> 65 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 66 <Layout.Header.BackButton /> 67 </View> 68 {isConvoActive(convoState) ? ( 69 moderation && blockInfo && profile && !isGroupChat ? ( 70 <ProfileHeaderReady 71 convoState={convoState} 72 profile={profile} 73 moderation={moderation} 74 blockInfo={blockInfo} 75 /> 76 ) : ( 77 <GroupHeaderReady 78 convoState={convoState} 79 profile={profile} 80 moderation={moderation} 81 /> 82 ) 83 ) : ( 84 <> 85 <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}> 86 <View 87 style={[ 88 {width: PFP_SIZE, height: PFP_SIZE}, 89 a.rounded_full, 90 t.atoms.bg_contrast_25, 91 ]} 92 /> 93 <View style={a.gap_xs}> 94 <View 95 style={[ 96 {width: 150, height: 16}, 97 a.rounded_xs, 98 t.atoms.bg_contrast_25, 99 a.mt_xs, 100 ]} 101 /> 102 </View> 103 </View> 104 105 <Layout.Header.Slot /> 106 </> 107 )} 108 </View> 109 </Layout.Header.Outer> 110 ) 111} 112 113function ProfileHeaderReady({ 114 convoState, 115 profile, 116 moderation, 117 blockInfo, 118}: { 119 convoState: ActiveConvoStates 120 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 121 moderation: ModerationDecision 122 blockInfo: { 123 listBlocks: ModerationCause[] 124 userBlock?: ModerationCause 125 } 126}) { 127 const {t: l} = useLingui() 128 const {currentAccount} = useSession() 129 130 const isDeletedAccount = profile?.handle === 'missing.invalid' 131 const displayName = isDeletedAccount 132 ? l`Deleted Account` 133 : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) 134 135 const latestMessageFromOther = convoState.items.findLast( 136 (item: ConvoItem) => 137 item.type === 'message' && 138 item.message.sender.did !== currentAccount?.did, 139 ) 140 141 const latestReportableMessage = 142 latestMessageFromOther?.type === 'message' 143 ? latestMessageFromOther.message 144 : undefined 145 146 return ( 147 <Wrapper 148 heading={ 149 <Link 150 label={l`View ${displayName}鈥檚 profile`} 151 style={[a.flex_row, a.gap_md, a.flex_1, a.pr_md]} 152 to={makeProfileLink(profile)}> 153 <PreviewableUserAvatar 154 size={PFP_SIZE} 155 profile={profile} 156 moderation={moderation.ui('avatar')} 157 disableHoverCard={moderation.blocked} 158 /> 159 <View style={[a.flex_row, a.align_center, a.flex_1]}> 160 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}> 161 {displayName} 162 </Text> 163 <ProfileBadges profile={profile} size="md" style={[a.pl_xs]} /> 164 </View> 165 </Link> 166 } 167 muted={convoState.convo?.muted} 168 settings={ 169 isConvoActive(convoState) ? ( 170 <ConvoMenu 171 convo={convoState.convo} 172 profile={profile} 173 currentScreen="conversation" 174 blockInfo={blockInfo} 175 latestReportableMessage={latestReportableMessage} 176 /> 177 ) : null 178 } 179 /> 180 ) 181} 182 183function GroupHeaderReady({ 184 convoState, 185 profile, 186 moderation, 187}: { 188 convoState: ActiveConvoStates 189 profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed> 190 moderation?: ModerationDecision | null 191}) { 192 const {t: l} = useLingui() 193 194 const navigation = useNavigation<NavigationProp>() 195 196 const groupInfo = convoState.getGroupInfo?.() 197 198 const isDeletedAccount = profile?.handle === 'missing.invalid' 199 const displayName = isDeletedAccount 200 ? l`Deleted Account` 201 : profile 202 ? createSanitizedDisplayName(profile, true, moderation?.ui('displayName')) 203 : undefined 204 const groupName = 205 groupInfo?.name ?? 206 (displayName ? l`${displayName}鈥檚 group chat` : l`Group chat`) 207 208 const handleNavigateToSettings = () => { 209 const convoId = convoState.convo?.id 210 if (convoId) { 211 navigation.navigate('MessagesConversationSettings', { 212 conversation: convoId, 213 }) 214 } else { 215 logger.error(`handleNavigateToSettings: missing convo ID`) 216 } 217 } 218 219 return ( 220 <Wrapper 221 heading={ 222 <> 223 <AvatarBubbles size="small" profiles={convoState.recipients ?? []} /> 224 <Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}> 225 {groupName} 226 </Text> 227 </> 228 } 229 muted={convoState.convo?.muted} 230 settings={ 231 isConvoActive(convoState) ? ( 232 <Button 233 label={l`Open group chat settings`} 234 size="small" 235 color="secondary" 236 shape="round" 237 variant="ghost" 238 style={[a.bg_transparent]} 239 onPress={handleNavigateToSettings}> 240 <ButtonIcon icon={DotsHorizontalIcon} size="md" /> 241 </Button> 242 ) : null 243 } 244 /> 245 ) 246} 247 248function Wrapper({ 249 heading, 250 muted, 251 settings, 252}: { 253 heading: React.ReactNode 254 muted: boolean 255 settings: React.ReactNode 256}) { 257 return ( 258 <View style={[a.flex_1]}> 259 <View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}> 260 <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}> 261 {heading} 262 <MuteStatus muted={muted} /> 263 </View> 264 265 <View style={[{minHeight: PFP_SIZE}, a.justify_center]}> 266 <Layout.Header.Slot>{settings}</Layout.Header.Slot> 267 </View> 268 </View> 269 </View> 270 ) 271} 272 273function MuteStatus({muted}: {muted: boolean}) { 274 const t = useTheme() 275 276 return muted ? ( 277 <> 278 <Text style={[a.text_md, t.atoms.text_contrast_medium]}> &middot; </Text> 279 <BellOffIcon size="sm" style={t.atoms.text_contrast_medium} /> 280 </> 281 ) : undefined 282}