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