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 210 lines 7.1 kB view raw
1import {memo, useCallback} from 'react' 2import {type StyleProp, View, type ViewStyle} from 'react-native' 3import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' 4import {msg} from '@lingui/core/macro' 5import {useLingui} from '@lingui/react' 6import {useQueryClient} from '@tanstack/react-query' 7 8import {makeProfileLink} from '#/lib/routes/links' 9import {forceLTR} from '#/lib/strings/bidi' 10import {NON_BREAKING_SPACE} from '#/lib/strings/constants' 11import {sanitizeDisplayName} from '#/lib/strings/display-names' 12import {sanitizeHandle} from '#/lib/strings/handles' 13import {sanitizePronouns} from '#/lib/strings/pronouns' 14import {niceDate} from '#/lib/strings/time' 15import {useProfileShadow} from '#/state/cache/profile-shadow' 16import {unstableCacheProfileView} from '#/state/queries/profile' 17import {atoms as a, platform, useTheme, web} from '#/alf' 18import {WebOnlyInlineLinkText} from '#/components/Link' 19import {ProfileBadges} from '#/components/ProfileBadges' 20import {ProfileHoverCard} from '#/components/ProfileHoverCard' 21import {Text} from '#/components/Typography' 22import {IS_ANDROID} from '#/env' 23import {useActorStatus} from '#/features/liveNow' 24import {TimeElapsed} from './TimeElapsed' 25import {PreviewableUserAvatar} from './UserAvatar' 26 27interface PostMetaOpts { 28 author: AppBskyActorDefs.ProfileViewBasic 29 moderation: ModerationDecision | undefined 30 postHref: string 31 timestamp: string 32 linkDisabled?: boolean 33 showAvatar?: boolean 34 showPronouns?: boolean 35 avatarSize?: number 36 onOpenAuthor?: () => void 37 style?: StyleProp<ViewStyle> 38} 39 40let PostMeta = (opts: PostMetaOpts): React.ReactNode => { 41 const t = useTheme() 42 const {i18n, _} = useLingui() 43 44 const author = useProfileShadow(opts.author) 45 const displayName = author.displayName || author.handle 46 const handle = author.handle 47 // remove dumb typing when you update the atproto api package!! 48 const pronouns = (author as {pronouns?: string})?.pronouns 49 const profileLink = makeProfileLink(author) 50 const queryClient = useQueryClient() 51 const onOpenAuthor = opts.onOpenAuthor 52 const onBeforePressAuthor = useCallback(() => { 53 unstableCacheProfileView(queryClient, author) 54 onOpenAuthor?.() 55 }, [queryClient, author, onOpenAuthor]) 56 const onBeforePressPost = useCallback(() => { 57 unstableCacheProfileView(queryClient, author) 58 }, [queryClient, author]) 59 60 const timestampLabel = niceDate(i18n, opts.timestamp) 61 const {isActive: live} = useActorStatus(author) 62 63 const MaybeLinkText = opts.linkDisabled ? Text : WebOnlyInlineLinkText 64 65 return ( 66 <View 67 style={[ 68 IS_ANDROID ? a.flex_1 : a.flex_shrink, 69 a.flex_row, 70 a.align_center, 71 a.pb_xs, 72 a.gap_xs, 73 a.z_20, 74 opts.style, 75 ]}> 76 {opts.showAvatar && ( 77 <View style={[a.self_center, a.mr_2xs]}> 78 <PreviewableUserAvatar 79 size={opts.avatarSize || 16} 80 profile={author} 81 moderation={opts.moderation?.ui('avatar')} 82 type={author.associated?.labeler ? 'labeler' : 'user'} 83 live={live} 84 hideLiveBadge 85 disableNavigation={opts.linkDisabled} 86 /> 87 </View> 88 )} 89 <View style={[a.flex_row, a.align_end, a.flex_shrink]}> 90 <ProfileHoverCard did={author.did}> 91 <View style={[a.flex_row, a.align_end, a.flex_shrink]}> 92 <MaybeLinkText 93 emoji 94 numberOfLines={1} 95 to={profileLink} 96 label={_(msg`View profile`)} 97 disableMismatchWarning 98 onPress={opts.linkDisabled ? undefined : onBeforePressAuthor} 99 style={[ 100 a.text_md, 101 a.font_semi_bold, 102 t.atoms.text, 103 a.leading_tight, 104 a.flex_shrink, 105 ]}> 106 {forceLTR( 107 sanitizeDisplayName( 108 displayName, 109 opts.moderation?.ui('displayName'), 110 ), 111 )} 112 </MaybeLinkText> 113 <ProfileBadges 114 profile={author} 115 size="sm" 116 pdsInteractive={false} 117 style={[ 118 a.pl_2xs, 119 a.self_center, 120 { 121 marginTop: platform({web: 1, ios: 0, android: -1}), 122 }, 123 ]} 124 /> 125 <MaybeLinkText 126 emoji 127 numberOfLines={1} 128 to={profileLink} 129 label={_(msg`View profile`)} 130 disableMismatchWarning 131 disableUnderline 132 onPress={opts.linkDisabled ? undefined : onBeforePressAuthor} 133 style={[ 134 a.text_md, 135 t.atoms.text_contrast_medium, 136 {lineHeight: 1.17}, 137 {flexBasis: '30%'}, 138 a.flex_grow, 139 a.flex_shrink_0, 140 web({maxWidth: 'max-content'}), 141 ]}> 142 {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} 143 </MaybeLinkText> 144 {opts.showPronouns && pronouns && ( 145 <WebOnlyInlineLinkText 146 emoji 147 numberOfLines={1} 148 to={profileLink} 149 label={_(msg`View Profile`)} 150 disableMismatchWarning 151 disableUnderline 152 onPress={onBeforePressAuthor} 153 style={[ 154 t.atoms.text_contrast_low, 155 a.pl_2xs, 156 a.text_md, 157 {lineHeight: 1.17}, 158 {flexShrink: 5}, 159 ]}> 160 {NON_BREAKING_SPACE + sanitizePronouns(pronouns)} 161 </WebOnlyInlineLinkText> 162 )} 163 </View> 164 </ProfileHoverCard> 165 166 <TimeElapsed timestamp={opts.timestamp}> 167 {({timeElapsed}) => ( 168 <MaybeLinkText 169 to={opts.postHref} 170 label={timestampLabel} 171 title={timestampLabel} 172 disableMismatchWarning 173 disableUnderline 174 onPress={opts.linkDisabled ? undefined : onBeforePressPost} 175 style={[ 176 a.pl_xs, 177 a.text_md, 178 a.leading_tight, 179 IS_ANDROID && a.flex_grow, 180 a.text_right, 181 t.atoms.text_contrast_medium, 182 web({ 183 whiteSpace: 'nowrap', 184 }), 185 ]}> 186 {!opts.showPronouns && ( 187 <> 188 {!IS_ANDROID && ( 189 <Text 190 style={[ 191 a.text_md, 192 a.leading_tight, 193 t.atoms.text_contrast_medium, 194 ]} 195 accessible={false}> 196 &middot;{' '} 197 </Text> 198 )} 199 {timeElapsed} 200 </> 201 )} 202 </MaybeLinkText> 203 )} 204 </TimeElapsed> 205 </View> 206 </View> 207 ) 208} 209PostMeta = memo(PostMeta) 210export {PostMeta}