Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 228 lines 6.2 kB view raw
1import {useCallback, useEffect, useMemo, useState} from 'react' 2import {LayoutAnimation, View} from 'react-native' 3import { 4 AppBskyFeedPost, 5 AppBskyRichtextFacet, 6 AtUri, 7 moderatePost, 8 RichText as RichTextAPI, 9} from '@atproto/api' 10import {msg} from '@lingui/macro' 11import {useLingui} from '@lingui/react' 12import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native' 13 14import {makeProfileLink} from '#/lib/routes/links' 15import { 16 type CommonNavigatorParams, 17 type NavigationProp, 18} from '#/lib/routes/types' 19import { 20 convertBskyAppUrlIfNeeded, 21 isBskyPostUrl, 22 makeRecordUri, 23} from '#/lib/strings/url-helpers' 24import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 25import {useModerationOpts} from '#/state/preferences/moderation-opts' 26import {usePostQuery} from '#/state/queries/post' 27import {PostMeta} from '#/view/com/util/PostMeta' 28import {atoms as a, useTheme} from '#/alf' 29import {Button, ButtonIcon} from '#/components/Button' 30import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 31import {Loader} from '#/components/Loader' 32import * as MediaPreview from '#/components/MediaPreview' 33import {ContentHider} from '#/components/moderation/ContentHider' 34import {PostAlerts} from '#/components/moderation/PostAlerts' 35import {RichText} from '#/components/RichText' 36import {Text} from '#/components/Typography' 37import * as bsky from '#/types/bsky' 38 39export function useMessageEmbed() { 40 const route = 41 useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>() 42 const navigation = useNavigation<NavigationProp>() 43 const embedFromParams = route.params.embed 44 45 const [embedUri, setEmbed] = useState(embedFromParams) 46 47 if (embedFromParams && embedUri !== embedFromParams) { 48 setEmbed(embedFromParams) 49 } 50 51 return { 52 embedUri, 53 setEmbed: useCallback( 54 (embedUrl: string | undefined) => { 55 if (!embedUrl) { 56 navigation.setParams({embed: ''}) 57 setEmbed(undefined) 58 return 59 } 60 61 if (embedFromParams) return 62 63 const url = convertBskyAppUrlIfNeeded(embedUrl) 64 const [_0, user, _1, rkey] = url.split('/').filter(Boolean) 65 const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey) 66 67 setEmbed(uri) 68 }, 69 [embedFromParams, navigation], 70 ), 71 } 72} 73 74export function useExtractEmbedFromFacets( 75 message: string, 76 setEmbed: (embedUrl: string | undefined) => void, 77) { 78 const rt = new RichTextAPI({text: message}) 79 rt.detectFacetsWithoutResolution() 80 81 let uriFromFacet: string | undefined 82 83 for (const facet of rt.facets ?? []) { 84 for (const feature of facet.features) { 85 if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) { 86 uriFromFacet = feature.uri 87 break 88 } 89 } 90 } 91 92 useEffect(() => { 93 if (uriFromFacet) { 94 setEmbed(uriFromFacet) 95 } 96 }, [uriFromFacet, setEmbed]) 97} 98 99export function MessageInputEmbed({ 100 embedUri, 101 setEmbed, 102}: { 103 embedUri: string | undefined 104 setEmbed: (embedUrl: string | undefined) => void 105}) { 106 const t = useTheme() 107 const {_} = useLingui() 108 109 const enableSquareButtons = useEnableSquareButtons() 110 111 const {data: post, status} = usePostQuery(embedUri) 112 113 const moderationOpts = useModerationOpts() 114 const moderation = useMemo( 115 () => 116 moderationOpts && post ? moderatePost(post, moderationOpts) : undefined, 117 [moderationOpts, post], 118 ) 119 120 const {rt, record} = useMemo(() => { 121 if ( 122 post && 123 bsky.dangerousIsType<AppBskyFeedPost.Record>( 124 post.record, 125 AppBskyFeedPost.isRecord, 126 ) 127 ) { 128 return { 129 rt: new RichTextAPI({ 130 text: post.record.text, 131 facets: post.record.facets, 132 }), 133 record: post.record, 134 } 135 } 136 137 return {rt: undefined, record: undefined} 138 }, [post]) 139 140 if (!embedUri) { 141 return null 142 } 143 144 let content = null 145 switch (status) { 146 case 'pending': 147 content = ( 148 <View 149 style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}> 150 <Loader /> 151 </View> 152 ) 153 break 154 case 'error': 155 content = ( 156 <View 157 style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}> 158 <Text style={a.text_center}>Could not fetch post</Text> 159 </View> 160 ) 161 break 162 case 'success': 163 const itemUrip = new AtUri(post.uri) 164 const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey) 165 166 if (!post || !moderation || !rt || !record) { 167 return null 168 } 169 170 content = ( 171 <View 172 style={[ 173 a.flex_1, 174 t.atoms.bg, 175 t.atoms.border_contrast_low, 176 a.rounded_md, 177 a.border, 178 a.p_sm, 179 a.mb_sm, 180 ]} 181 pointerEvents="none"> 182 <PostMeta 183 showAvatar 184 author={post.author} 185 moderation={moderation} 186 timestamp={post.indexedAt} 187 postHref={itemHref} 188 style={a.flex_0} 189 /> 190 <ContentHider modui={moderation.ui('contentView')}> 191 <PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} /> 192 {rt.text && ( 193 <View style={a.mt_xs}> 194 <RichText 195 enableTags 196 testID="postText" 197 value={rt} 198 style={[a.text_sm, t.atoms.text_contrast_high]} 199 authorHandle={post.author.handle} 200 numberOfLines={3} 201 /> 202 </View> 203 )} 204 <MediaPreview.Embed embed={post.embed} style={a.mt_sm} /> 205 </ContentHider> 206 </View> 207 ) 208 break 209 } 210 211 return ( 212 <View style={[a.flex_row, a.gap_sm]}> 213 {content} 214 <Button 215 label={_(msg`Remove embed`)} 216 onPress={() => { 217 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 218 setEmbed(undefined) 219 }} 220 size="tiny" 221 variant="solid" 222 color="secondary" 223 shape={enableSquareButtons ? 'square' : 'round'}> 224 <ButtonIcon icon={X} /> 225 </Button> 226 </View> 227 ) 228}