Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Link cards (#5677)

* New link card styles

* Cleanup of consituent parts, add hover state

* Fix gif alt text view

* Fix alt text view more

* Remove dupe

* Update remove button

* Remove added margin on gif

authored by

Eric Bailey and committed by
GitHub
4c3c10d7 2d884634

+153 -231
+15 -23
src/view/com/composer/ExternalEmbedRemoveBtn.tsx
··· 1 1 import React from 'react' 2 - import {TouchableOpacity} from 'react-native' 3 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 2 + import {View} from 'react-native' 4 3 import {msg} from '@lingui/macro' 5 4 import {useLingui} from '@lingui/react' 6 5 7 - import {s} from 'lib/styles' 6 + import {atoms as a} from '#/alf' 7 + import {Button, ButtonIcon} from '#/components/Button' 8 + import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 8 9 9 10 export function ExternalEmbedRemoveBtn({onRemove}: {onRemove: () => void}) { 10 11 const {_} = useLingui() 11 12 12 13 return ( 13 - <TouchableOpacity 14 - style={{ 15 - position: 'absolute', 16 - top: 10, 17 - right: 10, 18 - height: 36, 19 - width: 36, 20 - backgroundColor: 'rgba(0, 0, 0, 0.75)', 21 - borderRadius: 18, 22 - alignItems: 'center', 23 - justifyContent: 'center', 24 - zIndex: 1, 25 - }} 26 - onPress={onRemove} 27 - accessibilityRole="button" 28 - accessibilityLabel={_(msg`Remove attachment`)} 29 - accessibilityHint={_(msg`Removes the attachment`)} 30 - onAccessibilityEscape={onRemove}> 31 - <FontAwesomeIcon size={18} icon="xmark" style={s.white} /> 32 - </TouchableOpacity> 14 + <View style={[a.absolute, a.pt_sm, a.pr_sm, {top: 0, right: 0}]}> 15 + <Button 16 + label={_(msg`Remove attachment`)} 17 + onPress={onRemove} 18 + size="small" 19 + variant="solid" 20 + color="secondary" 21 + shape="round"> 22 + <ButtonIcon icon={X} size="sm" /> 23 + </Button> 24 + </View> 33 25 ) 34 26 }
+2 -2
src/view/com/composer/GifAltText.tsx
··· 13 13 import {useResolveGifQuery} from '#/state/queries/resolve-link' 14 14 import {Gif} from '#/state/queries/tenor' 15 15 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' 16 - import {atoms as a, native, useTheme} from '#/alf' 16 + import {atoms as a, useTheme} from '#/alf' 17 17 import {Button, ButtonText} from '#/components/Button' 18 18 import * as Dialog from '#/components/Dialog' 19 19 import {DialogControlProps} from '#/components/Dialog' ··· 213 213 isPreferredAltText={true} 214 214 params={params} 215 215 hideAlt 216 - style={[native({maxHeight: 225})]} 216 + style={[{height: 225}]} 217 217 /> 218 218 </View> 219 219 </View>
+1 -39
src/view/com/util/post-embeds/ExternalGifEmbed.tsx
··· 4 4 GestureResponderEvent, 5 5 LayoutChangeEvent, 6 6 Pressable, 7 - StyleSheet, 8 7 } from 'react-native' 9 8 import {Image, ImageLoadEventData} from 'expo-image' 10 9 import {AppBskyEmbedExternal} from '@atproto/api' ··· 18 17 import {useDialogControl} from '#/components/Dialog' 19 18 import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 20 19 import {Fill} from '#/components/Fill' 21 - import {MediaInsetBorder} from '#/components/MediaInsetBorder' 22 20 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 23 21 24 22 export function ExternalGifEmbed({ ··· 116 114 <Pressable 117 115 style={[ 118 116 {height: imageDims.height}, 119 - styles.gifContainer, 120 - a.rounded_md, 117 + a.w_full, 121 118 a.overflow_hidden, 122 119 { 123 120 borderBottomLeftRadius: 0, ··· 166 163 )} 167 164 </Fill> 168 165 )} 169 - <MediaInsetBorder 170 - opaque 171 - style={[ 172 - { 173 - borderBottomLeftRadius: 0, 174 - borderBottomRightRadius: 0, 175 - }, 176 - ]} 177 - /> 178 166 </Pressable> 179 167 </> 180 168 ) 181 169 } 182 - 183 - const styles = StyleSheet.create({ 184 - topRadius: { 185 - borderTopLeftRadius: 6, 186 - borderTopRightRadius: 6, 187 - }, 188 - layer: { 189 - position: 'absolute', 190 - top: 0, 191 - left: 0, 192 - right: 0, 193 - bottom: 0, 194 - }, 195 - overlayContainer: { 196 - flex: 1, 197 - justifyContent: 'center', 198 - alignItems: 'center', 199 - }, 200 - overlayLayer: { 201 - zIndex: 2, 202 - }, 203 - gifContainer: { 204 - width: '100%', 205 - overflow: 'hidden', 206 - }, 207 - })
+112 -107
src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
··· 6 6 import {useLingui} from '@lingui/react' 7 7 8 8 import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' 9 - import {usePalette} from '#/lib/hooks/usePalette' 10 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 9 import {shareUrl} from '#/lib/sharing' 12 10 import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' 13 11 import { ··· 17 15 import {toNiceDomain} from '#/lib/strings/url-helpers' 18 16 import {isNative} from '#/platform/detection' 19 17 import {useExternalEmbedsPrefs} from '#/state/preferences' 20 - import {Link} from '#/view/com/util/Link' 21 18 import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' 22 19 import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' 23 20 import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' 24 21 import {atoms as a, useTheme} from '#/alf' 25 - import {MediaInsetBorder} from '#/components/MediaInsetBorder' 26 - import {Text} from '../text/Text' 22 + import {Divider} from '#/components/Divider' 23 + import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 24 + import {Link} from '#/components/Link' 25 + import {Text} from '#/components/Typography' 27 26 28 27 export const ExternalLinkEmbed = ({ 29 28 link, ··· 37 36 hideAlt?: boolean 38 37 }) => { 39 38 const {_} = useLingui() 40 - const pal = usePalette('default') 41 39 const t = useTheme() 42 - const {isMobile} = useWebMediaQueries() 43 40 const externalEmbedPrefs = useExternalEmbedsPrefs() 44 - 41 + const niceUrl = toNiceDomain(link.uri) 45 42 const starterPackParsed = parseStarterPackUri(link.uri) 46 43 const imageUri = starterPackParsed 47 44 ? getStarterPackOgCard(starterPackParsed.name, starterPackParsed.rkey) 48 45 : link.thumb 49 - 50 46 const embedPlayerParams = React.useMemo(() => { 51 47 const params = parseEmbedPlayerFromUrl(link.uri) 52 48 ··· 54 50 return params 55 51 } 56 52 }, [link.uri, externalEmbedPrefs]) 53 + const hasMedia = Boolean(imageUri || embedPlayerParams) 54 + 55 + const onShareExternal = useCallback(() => { 56 + if (link.uri && isNative) { 57 + shareUrl(link.uri) 58 + } 59 + }, [link.uri]) 57 60 58 61 if (embedPlayerParams?.source === 'tenor') { 59 62 const parsedAlt = parseAltFromGIFDescription(link.description) 60 63 return ( 61 - <GifEmbed 62 - params={embedPlayerParams} 63 - thumb={link.thumb} 64 - altText={parsedAlt.alt} 65 - isPreferredAltText={parsedAlt.isPreferred} 66 - hideAlt={hideAlt} 67 - /> 64 + <View style={style}> 65 + <GifEmbed 66 + params={embedPlayerParams} 67 + thumb={link.thumb} 68 + altText={parsedAlt.alt} 69 + isPreferredAltText={parsedAlt.isPreferred} 70 + hideAlt={hideAlt} 71 + /> 72 + </View> 68 73 ) 69 74 } 70 75 71 76 return ( 72 - <View style={[a.flex_col, a.rounded_md, a.w_full]}> 73 - <LinkWrapper link={link} onOpen={onOpen} style={style}> 74 - {imageUri && !embedPlayerParams ? ( 75 - <View> 77 + <Link 78 + label={link.title || _(msg`Open link to ${niceUrl}`)} 79 + to={link.uri} 80 + onPress={onOpen} 81 + onLongPress={onShareExternal}> 82 + {({hovered}) => ( 83 + <View 84 + style={[ 85 + a.transition_color, 86 + a.flex_col, 87 + a.rounded_md, 88 + a.overflow_hidden, 89 + a.w_full, 90 + a.border, 91 + style, 92 + hovered 93 + ? t.atoms.border_contrast_high 94 + : t.atoms.border_contrast_low, 95 + ]}> 96 + {imageUri && !embedPlayerParams ? ( 76 97 <Image 77 98 style={{ 78 99 aspectRatio: 1.91, 79 - borderTopRightRadius: a.rounded_md.borderRadius, 80 - borderTopLeftRadius: a.rounded_md.borderRadius, 81 100 }} 82 101 source={{uri: imageUri}} 83 102 accessibilityIgnoresInvertColors 84 - accessibilityLabel={starterPackParsed ? link.title : undefined} 85 - accessibilityHint={ 86 - starterPackParsed ? _(msg`Navigate to starter pack`) : undefined 87 - } 88 103 /> 89 - <MediaInsetBorder 90 - opaque 91 - style={[ 92 - { 93 - borderBottomLeftRadius: 0, 94 - borderBottomRightRadius: 0, 95 - }, 96 - ]} 97 - /> 98 - </View> 99 - ) : undefined} 100 - {embedPlayerParams?.isGif ? ( 101 - <ExternalGifEmbed link={link} params={embedPlayerParams} /> 102 - ) : embedPlayerParams ? ( 103 - <ExternalPlayer link={link} params={embedPlayerParams} /> 104 - ) : undefined} 105 - <View 106 - style={[ 107 - a.border_b, 108 - a.border_l, 109 - a.border_r, 110 - a.flex_1, 111 - a.py_sm, 112 - t.atoms.border_contrast_low, 113 - { 114 - borderBottomRightRadius: a.rounded_md.borderRadius, 115 - borderBottomLeftRadius: a.rounded_md.borderRadius, 116 - paddingHorizontal: isMobile ? 10 : 14, 117 - }, 118 - !imageUri && !embedPlayerParams && [a.border, a.rounded_md], 119 - ]}> 120 - <Text 121 - type="sm" 122 - numberOfLines={1} 123 - style={[pal.textLight, {marginVertical: 2}]}> 124 - {toNiceDomain(link.uri)} 125 - </Text> 104 + ) : undefined} 126 105 127 - {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( 128 - <Text emoji type="lg-bold" numberOfLines={3} style={[pal.text]}> 129 - {link.title || link.uri} 130 - </Text> 131 - )} 132 - {link.description ? ( 133 - <Text 134 - emoji 135 - type="md" 136 - numberOfLines={link.thumb ? 2 : 4} 137 - style={[pal.text, a.mt_xs]}> 138 - {link.description} 139 - </Text> 106 + {embedPlayerParams?.isGif ? ( 107 + <ExternalGifEmbed link={link} params={embedPlayerParams} /> 108 + ) : embedPlayerParams ? ( 109 + <ExternalPlayer link={link} params={embedPlayerParams} /> 140 110 ) : undefined} 111 + 112 + <View 113 + style={[ 114 + a.flex_1, 115 + a.pt_sm, 116 + {gap: 3}, 117 + hasMedia && a.border_t, 118 + hovered 119 + ? t.atoms.border_contrast_high 120 + : t.atoms.border_contrast_low, 121 + ]}> 122 + <View style={[{gap: 3}, a.pb_xs, a.px_md]}> 123 + {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( 124 + <Text 125 + emoji 126 + numberOfLines={3} 127 + style={[a.text_md, a.font_bold, a.leading_snug]}> 128 + {link.title || link.uri} 129 + </Text> 130 + )} 131 + {link.description ? ( 132 + <Text 133 + emoji 134 + numberOfLines={link.thumb ? 2 : 4} 135 + style={[a.text_sm, a.leading_snug]}> 136 + {link.description} 137 + </Text> 138 + ) : undefined} 139 + </View> 140 + <View style={[a.px_md]}> 141 + <Divider /> 142 + <View 143 + style={[ 144 + a.flex_row, 145 + a.align_center, 146 + a.gap_2xs, 147 + a.pb_sm, 148 + { 149 + paddingTop: 6, // off menu 150 + }, 151 + ]}> 152 + <Globe 153 + size="xs" 154 + style={[ 155 + a.transition_color, 156 + hovered 157 + ? t.atoms.text_contrast_medium 158 + : t.atoms.text_contrast_low, 159 + ]} 160 + /> 161 + <Text 162 + numberOfLines={1} 163 + style={[ 164 + a.transition_color, 165 + a.text_xs, 166 + a.leading_tight, 167 + hovered 168 + ? t.atoms.text_contrast_high 169 + : t.atoms.text_contrast_medium, 170 + ]}> 171 + {toNiceDomain(link.uri)} 172 + </Text> 173 + </View> 174 + </View> 175 + </View> 141 176 </View> 142 - </LinkWrapper> 143 - </View> 144 - ) 145 - } 146 - 147 - function LinkWrapper({ 148 - link, 149 - onOpen, 150 - style, 151 - children, 152 - }: { 153 - link: AppBskyEmbedExternal.ViewExternal 154 - onOpen?: () => void 155 - style?: StyleProp<ViewStyle> 156 - children: React.ReactNode 157 - }) { 158 - const onShareExternal = useCallback(() => { 159 - if (link.uri && isNative) { 160 - shareUrl(link.uri) 161 - } 162 - }, [link.uri]) 163 - 164 - return ( 165 - <Link 166 - asAnchor 167 - anchorNoUnderline 168 - href={link.uri} 169 - style={[a.flex_1, a.rounded_sm, style]} 170 - onBeforePress={onOpen} 171 - onLongPress={onShareExternal}> 172 - {children} 177 + )} 173 178 </Link> 174 179 ) 175 180 }
+3 -52
src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
··· 29 29 import {useDialogControl} from '#/components/Dialog' 30 30 import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 31 31 import {Fill} from '#/components/Fill' 32 - import {MediaInsetBorder} from '#/components/MediaInsetBorder' 33 32 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 34 33 import {EventStopper} from '../EventStopper' 35 34 ··· 59 58 accessibilityLabel={_(msg`Play Video`)} 60 59 accessibilityHint={_(msg`Play Video`)} 61 60 onPress={onPress} 62 - style={[styles.overlayContainer, styles.topRadius]}> 61 + style={[styles.overlayContainer]}> 63 62 {!isPlayerActive ? ( 64 63 <PlayButtonIcon /> 65 64 ) : ( ··· 107 106 onLoad={onLoad} 108 107 style={styles.webview} 109 108 setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) 110 - /> 111 - 112 - <MediaInsetBorder 113 - opaque 114 - style={[ 115 - { 116 - borderBottomLeftRadius: 0, 117 - borderBottomRightRadius: 0, 118 - }, 119 - ]} 120 109 /> 121 110 </EventStopper> 122 111 ) ··· 227 216 <Animated.View 228 217 ref={viewRef} 229 218 collapsable={false} 230 - style={[ 231 - aspect, 232 - a.rounded_md, 233 - a.overflow_hidden, 234 - { 235 - borderBottomLeftRadius: 0, 236 - borderBottomRightRadius: 0, 237 - }, 238 - ]}> 219 + style={[aspect, a.overflow_hidden]}> 239 220 {link.thumb && (!isPlayerActive || isLoading) ? ( 240 221 <> 241 222 <Image 242 - style={[a.flex_1, styles.topRadius]} 223 + style={[a.flex_1]} 243 224 source={{uri: link.thumb}} 244 225 accessibilityIgnoresInvertColors 245 226 /> 246 227 <Fill 247 228 style={[ 248 - a.rounded_md, 249 229 t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, 250 230 { 251 - borderBottomLeftRadius: 0, 252 - borderBottomRightRadius: 0, 253 231 opacity: 0.3, 254 232 }, 255 233 ]} 256 234 /> 257 - <MediaInsetBorder 258 - opaque 259 - style={[ 260 - { 261 - borderBottomLeftRadius: 0, 262 - borderBottomRightRadius: 0, 263 - }, 264 - ]} 265 - /> 266 235 </> 267 236 ) : ( 268 237 <Fill 269 238 style={[ 270 - a.rounded_md, 271 239 { 272 240 backgroundColor: 273 241 t.name === 'light' ? t.palette.contrast_975 : 'black', 274 - borderBottomLeftRadius: 0, 275 - borderBottomRightRadius: 0, 276 242 opacity: 0.3, 277 243 }, 278 244 ]} 279 245 /> 280 246 )} 281 - <MediaInsetBorder 282 - opaque 283 - style={[ 284 - { 285 - borderBottomLeftRadius: 0, 286 - borderBottomRightRadius: 0, 287 - }, 288 - ]} 289 - /> 290 247 <PlaceholderOverlay 291 248 isLoading={isLoading} 292 249 isPlayerActive={isPlayerActive} ··· 303 260 } 304 261 305 262 const styles = StyleSheet.create({ 306 - topRadius: { 307 - borderTopLeftRadius: a.rounded_md.borderRadius, 308 - borderTopRightRadius: a.rounded_md.borderRadius, 309 - }, 310 263 overlayContainer: { 311 264 flex: 1, 312 265 justifyContent: 'center', ··· 319 272 zIndex: 3, 320 273 }, 321 274 webview: { 322 - borderTopRightRadius: a.rounded_md.borderRadius, 323 - borderTopLeftRadius: a.rounded_md.borderRadius, 324 275 backgroundColor: 'transparent', 325 276 }, 326 277 gifContainer: {
+20 -8
src/view/com/util/post-embeds/GifEmbed.tsx
··· 18 18 import {atoms as a, useTheme} from '#/alf' 19 19 import {Fill} from '#/components/Fill' 20 20 import {Loader} from '#/components/Loader' 21 - import {MediaInsetBorder} from '#/components/MediaInsetBorder' 22 21 import * as Prompt from '#/components/Prompt' 23 22 import {Text} from '#/components/Typography' 24 23 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' ··· 51 50 a.inset_0, 52 51 a.w_full, 53 52 a.h_full, 54 - a.rounded_md, 55 53 { 56 54 zIndex: 2, 57 55 backgroundColor: !isLoaded ··· 114 112 }, []) 115 113 116 114 return ( 117 - <View style={[a.rounded_md, a.overflow_hidden, a.mt_sm, style]}> 115 + <View 116 + style={[ 117 + a.rounded_md, 118 + a.overflow_hidden, 119 + a.border, 120 + t.atoms.border_contrast_low, 121 + {aspectRatio: params.dimensions!.width / params.dimensions!.height}, 122 + style, 123 + ]}> 118 124 <View 119 125 style={[ 120 - a.rounded_md, 121 - a.overflow_hidden, 122 - {aspectRatio: params.dimensions!.width / params.dimensions!.height}, 126 + a.absolute, 127 + /* 128 + * Aspect ratio was being clipped weirdly on web -esb 129 + */ 130 + { 131 + top: -2, 132 + bottom: -2, 133 + left: -2, 134 + right: -2, 135 + }, 123 136 ]}> 124 137 <PlaybackControls 125 138 onPress={onPress} ··· 129 142 <GifView 130 143 source={params.playerUri} 131 144 placeholderSource={thumb} 132 - style={[a.flex_1, a.rounded_md]} 145 + style={[a.flex_1]} 133 146 autoplay={!autoplayDisabled} 134 147 onPlayerStateChange={onPlayerStateChange} 135 148 ref={playerRef} ··· 146 159 ]} 147 160 /> 148 161 )} 149 - <MediaInsetBorder /> 150 162 {!hideAlt && isPreferredAltText && <AltText text={altText} />} 151 163 </View> 152 164 </View>