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

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 195 lines 4.7 kB view raw
1import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' 2import {Image} from 'expo-image' 3import {type AppBskyFeedDefs} from '@atproto/api' 4import {Trans} from '@lingui/react/macro' 5 6import {isGifEmbed} from '#/lib/strings/embed-player' 7import {useHighQualityImages} from '#/state/preferences/high-quality-images' 8import { 9 applyImageTransforms, 10 useImageCdnHost, 11} from '#/state/preferences/image-cdn-host' 12import {atoms as a, useTheme} from '#/alf' 13import {MediaInsetBorder} from '#/components/MediaInsetBorder' 14import {Text} from '#/components/Typography' 15import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 16import * as bsky from '#/types/bsky' 17 18/** 19 * Streamlined MediaPreview component which just handles images, gifs, and videos 20 */ 21export function Embed({ 22 embed, 23 style, 24}: { 25 embed: AppBskyFeedDefs.PostView['embed'] 26 style?: StyleProp<ViewStyle> 27}) { 28 const e = bsky.post.parseEmbed(embed) 29 30 if (!e) return null 31 32 if (e.type === 'images') { 33 return ( 34 <Outer style={style}> 35 {e.view.images.map(image => ( 36 <ImageItem 37 key={image.thumb} 38 thumbnail={image.thumb} 39 alt={image.alt} 40 /> 41 ))} 42 </Outer> 43 ) 44 } else if (e.type === 'link') { 45 if (!e.view.external.thumb) return null 46 if (!isGifEmbed(e.view.external.uri)) return null 47 return ( 48 <Outer style={style}> 49 <GifItem 50 thumbnail={e.view.external.thumb} 51 alt={e.view.external.title} 52 /> 53 </Outer> 54 ) 55 } else if (e.type === 'video') { 56 return ( 57 <Outer style={style}> 58 {e.view.presentation === 'gif' ? ( 59 <GifItem 60 thumbnail={e.view.thumbnail ? e.view.thumbnail : undefined} 61 alt={e.view.alt} 62 /> 63 ) : ( 64 <VideoItem 65 thumbnail={e.view.thumbnail ? e.view.thumbnail : undefined} 66 alt={e.view.alt} 67 /> 68 )} 69 </Outer> 70 ) 71 } else if ( 72 e.type === 'post_with_media' && 73 // ignore further "nested" RecordWithMedia 74 e.media.type !== 'post_with_media' && 75 // ignore any unknowns 76 e.media.view !== null 77 ) { 78 return <Embed embed={e.media.view} style={style} /> 79 } 80 81 return null 82} 83 84export function Outer({ 85 children, 86 style, 87}: { 88 children?: React.ReactNode 89 style?: StyleProp<ViewStyle> 90}) { 91 return <View style={[a.flex_row, a.gap_xs, style]}>{children}</View> 92} 93 94export function ImageItem({ 95 thumbnail, 96 alt, 97 children, 98}: { 99 thumbnail?: string 100 alt?: string 101 children?: React.ReactNode 102}) { 103 const t = useTheme() 104 const highQualityImages = useHighQualityImages() 105 const imageCdnHost = useImageCdnHost() 106 107 const transformedThumbnail = thumbnail 108 ? applyImageTransforms(thumbnail, { 109 imageCdnHost, 110 highQualityImages, 111 }) 112 : undefined 113 114 if (!transformedThumbnail) { 115 return ( 116 <View 117 style={[ 118 {backgroundColor: 'black'}, 119 a.flex_1, 120 a.aspect_square, 121 {maxWidth: 100}, 122 a.rounded_xs, 123 ]} 124 accessibilityLabel={alt} 125 accessibilityHint=""> 126 {children} 127 </View> 128 ) 129 } 130 131 return ( 132 <View style={[a.relative, a.flex_1, a.aspect_square, {maxWidth: 100}]}> 133 <Image 134 key={transformedThumbnail} 135 source={{uri: transformedThumbnail}} 136 alt={alt} 137 style={[a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25]} 138 contentFit="cover" 139 accessible={true} 140 accessibilityIgnoresInvertColors 141 /> 142 <MediaInsetBorder style={[a.rounded_xs]} /> 143 {children} 144 </View> 145 ) 146} 147 148export function GifItem({thumbnail, alt}: {thumbnail?: string; alt?: string}) { 149 return ( 150 <ImageItem thumbnail={thumbnail} alt={alt}> 151 <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> 152 <PlayButtonIcon size={24} /> 153 </View> 154 <View style={styles.altContainer}> 155 <Text style={styles.alt}> 156 <Trans>GIF</Trans> 157 </Text> 158 </View> 159 </ImageItem> 160 ) 161} 162 163export function VideoItem({ 164 thumbnail, 165 alt, 166}: { 167 thumbnail?: string 168 alt?: string 169}) { 170 return ( 171 <ImageItem thumbnail={thumbnail} alt={alt}> 172 <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> 173 <PlayButtonIcon size={24} /> 174 </View> 175 </ImageItem> 176 ) 177} 178 179const styles = StyleSheet.create({ 180 altContainer: { 181 backgroundColor: 'rgba(0, 0, 0, 0.75)', 182 borderRadius: 6, 183 paddingHorizontal: 6, 184 paddingVertical: 3, 185 position: 'absolute', 186 left: 5, 187 bottom: 5, 188 zIndex: 2, 189 }, 190 alt: { 191 color: 'white', 192 fontSize: 7, 193 fontWeight: '600', 194 }, 195})