Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Replace `ImageHorzList` 🤮 with `MediaPreview` ✨ (#5143)

authored by

Samuel Newman and committed by
GitHub
5f5c14d0 82ca0b16

+176 -118
+169
src/components/MediaPreview.tsx
··· 1 + import React from 'react' 2 + import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 + import {Image} from 'expo-image' 4 + import { 5 + AppBskyEmbedExternal, 6 + AppBskyEmbedImages, 7 + AppBskyEmbedRecordWithMedia, 8 + AppBskyEmbedVideo, 9 + } from '@atproto/api' 10 + import {Trans} from '@lingui/macro' 11 + 12 + import {parseTenorGif} from '#/lib/strings/embed-player' 13 + import {atoms as a} from '#/alf' 14 + import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' 15 + import {Text} from '#/components/Typography' 16 + 17 + /** 18 + * Streamlined MediaPreview component which just handles images, gifs, and videos 19 + */ 20 + export function Embed({ 21 + embed, 22 + style, 23 + }: { 24 + embed?: 25 + | AppBskyEmbedImages.View 26 + | AppBskyEmbedRecordWithMedia.View 27 + | AppBskyEmbedExternal.View 28 + | AppBskyEmbedVideo.View 29 + | {[k: string]: unknown} 30 + style?: StyleProp<ViewStyle> 31 + }) { 32 + let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed 33 + 34 + if (AppBskyEmbedImages.isView(media)) { 35 + return ( 36 + <Outer style={style}> 37 + {media.images.map(image => ( 38 + <ImageItem 39 + key={image.thumb} 40 + thumbnail={image.thumb} 41 + alt={image.alt} 42 + /> 43 + ))} 44 + </Outer> 45 + ) 46 + } else if (AppBskyEmbedExternal.isView(embed) && embed.external.thumb) { 47 + let url: URL | undefined 48 + try { 49 + url = new URL(embed.external.uri) 50 + } catch {} 51 + if (url) { 52 + const {success} = parseTenorGif(url) 53 + if (success) { 54 + return ( 55 + <Outer style={style}> 56 + <GifItem 57 + thumbnail={embed.external.thumb} 58 + alt={embed.external.title} 59 + /> 60 + </Outer> 61 + ) 62 + } 63 + } 64 + } else if (AppBskyEmbedVideo.isView(embed)) { 65 + return ( 66 + <Outer style={style}> 67 + <VideoItem thumbnail={embed.thumbnail} alt={embed.alt} /> 68 + </Outer> 69 + ) 70 + } 71 + 72 + return null 73 + } 74 + 75 + export function Outer({ 76 + children, 77 + style, 78 + }: { 79 + children?: React.ReactNode 80 + style?: StyleProp<ViewStyle> 81 + }) { 82 + return <View style={[a.flex_row, a.gap_xs, style]}>{children}</View> 83 + } 84 + 85 + export function ImageItem({ 86 + thumbnail, 87 + alt, 88 + children, 89 + }: { 90 + thumbnail: string 91 + alt?: string 92 + children?: React.ReactNode 93 + }) { 94 + return ( 95 + <View style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}> 96 + <Image 97 + key={thumbnail} 98 + source={{uri: thumbnail}} 99 + style={[a.flex_1, a.rounded_xs]} 100 + contentFit="cover" 101 + accessible={true} 102 + accessibilityIgnoresInvertColors 103 + accessibilityHint={alt} 104 + accessibilityLabel="" 105 + /> 106 + {children} 107 + </View> 108 + ) 109 + } 110 + 111 + export function GifItem({thumbnail, alt}: {thumbnail: string; alt?: string}) { 112 + return ( 113 + <ImageItem thumbnail={thumbnail} alt={alt}> 114 + <View style={styles.altContainer}> 115 + <Text style={styles.alt}> 116 + <Trans>GIF</Trans> 117 + </Text> 118 + </View> 119 + </ImageItem> 120 + ) 121 + } 122 + 123 + export function VideoItem({ 124 + thumbnail, 125 + alt, 126 + }: { 127 + thumbnail?: string 128 + alt?: string 129 + }) { 130 + if (!thumbnail) { 131 + return ( 132 + <View 133 + style={[ 134 + {backgroundColor: 'black'}, 135 + a.flex_1, 136 + {aspectRatio: 1, maxWidth: 100}, 137 + a.justify_center, 138 + a.align_center, 139 + ]}> 140 + <PlayIcon size="xl" fill="white" /> 141 + </View> 142 + ) 143 + } 144 + return ( 145 + <ImageItem thumbnail={thumbnail} alt={alt}> 146 + <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> 147 + <PlayIcon size="xl" fill="white" /> 148 + </View> 149 + </ImageItem> 150 + ) 151 + } 152 + 153 + const styles = StyleSheet.create({ 154 + altContainer: { 155 + backgroundColor: 'rgba(0, 0, 0, 0.75)', 156 + borderRadius: 6, 157 + paddingHorizontal: 6, 158 + paddingVertical: 3, 159 + position: 'absolute', 160 + right: 5, 161 + bottom: 5, 162 + zIndex: 2, 163 + }, 164 + alt: { 165 + color: 'white', 166 + fontSize: 7, 167 + fontWeight: 'bold', 168 + }, 169 + })
+2 -13
src/screens/Messages/Conversation/MessageInputEmbed.tsx
··· 1 1 import React, {useCallback, useEffect, useMemo, useState} from 'react' 2 2 import {LayoutAnimation, View} from 'react-native' 3 3 import { 4 - AppBskyEmbedImages, 5 - AppBskyEmbedRecordWithMedia, 6 4 AppBskyFeedPost, 7 5 AppBskyRichtextFacet, 8 6 AtUri, ··· 22 20 } from '#/lib/strings/url-helpers' 23 21 import {useModerationOpts} from '#/state/preferences/moderation-opts' 24 22 import {usePostQuery} from '#/state/queries/post' 25 - import {ImageHorzList} from '#/view/com/util/images/ImageHorzList' 26 23 import {PostMeta} from '#/view/com/util/PostMeta' 27 24 import {atoms as a, useTheme} from '#/alf' 28 25 import {Button, ButtonIcon} from '#/components/Button' 29 26 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 30 27 import {Loader} from '#/components/Loader' 28 + import * as MediaPreview from '#/components/MediaPreview' 31 29 import {ContentHider} from '#/components/moderation/ContentHider' 32 30 import {PostAlerts} from '#/components/moderation/PostAlerts' 33 31 import {RichText} from '#/components/RichText' ··· 160 158 return null 161 159 } 162 160 163 - const images = AppBskyEmbedImages.isView(post.embed) 164 - ? post.embed.images 165 - : AppBskyEmbedRecordWithMedia.isView(post.embed) && 166 - AppBskyEmbedImages.isView(post.embed.media) 167 - ? post.embed.media.images 168 - : undefined 169 - 170 161 content = ( 171 162 <View 172 163 style={[ ··· 202 193 /> 203 194 </View> 204 195 )} 205 - {images && images?.length > 0 && ( 206 - <ImageHorzList images={images} style={a.mt_xs} /> 207 - )} 196 + <MediaPreview.Embed embed={post.embed} style={a.mt_sm} /> 208 197 </ContentHider> 209 198 </View> 210 199 )
+5 -44
src/view/com/notifications/FeedItem.tsx
··· 8 8 } from 'react-native' 9 9 import { 10 10 AppBskyActorDefs, 11 - AppBskyEmbedExternal, 12 - AppBskyEmbedImages, 13 - AppBskyEmbedRecordWithMedia, 14 11 AppBskyFeedDefs, 15 12 AppBskyFeedPost, 16 13 AppBskyGraphFollow, ··· 25 22 import {useNavigation} from '@react-navigation/native' 26 23 import {useQueryClient} from '@tanstack/react-query' 27 24 28 - import {parseTenorGif} from '#/lib/strings/embed-player' 29 25 import {logger} from '#/logger' 30 26 import {FeedNotification} from '#/state/queries/notifications/feed' 31 27 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' ··· 52 48 import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' 53 49 import {StarterPack} from '#/components/icons/StarterPack' 54 50 import {Link as NewLink} from '#/components/Link' 51 + import * as MediaPreview from '#/components/MediaPreview' 55 52 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 56 53 import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 57 54 import {FeedSourceCard} from '../feeds/FeedSourceCard' 58 55 import {Post} from '../post/Post' 59 - import {ImageHorzList} from '../util/images/ImageHorzList' 60 56 import {Link, TextLink} from '../util/Link' 61 57 import {formatCount} from '../util/numeric/format' 62 58 import {Text} from '../util/text/Text' ··· 593 589 const pal = usePalette('default') 594 590 if (post && AppBskyFeedPost.isRecord(post?.record)) { 595 591 const text = post.record.text 596 - let images 597 - let isGif = false 598 - 599 - if (AppBskyEmbedImages.isView(post.embed)) { 600 - images = post.embed.images 601 - } else if ( 602 - AppBskyEmbedRecordWithMedia.isView(post.embed) && 603 - AppBskyEmbedImages.isView(post.embed.media) 604 - ) { 605 - images = post.embed.media.images 606 - } else if ( 607 - AppBskyEmbedExternal.isView(post.embed) && 608 - post.embed.external.thumb 609 - ) { 610 - let url: URL | undefined 611 - try { 612 - url = new URL(post.embed.external.uri) 613 - } catch {} 614 - if (url) { 615 - const {success} = parseTenorGif(url) 616 - if (success) { 617 - isGif = true 618 - images = [ 619 - { 620 - thumb: post.embed.external.thumb, 621 - alt: post.embed.external.title, 622 - fullsize: post.embed.external.thumb, 623 - }, 624 - ] 625 - } 626 - } 627 - } 628 592 629 593 return ( 630 594 <> 631 595 {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>} 632 - {images && images.length > 0 && ( 633 - <ImageHorzList 634 - images={images} 635 - style={styles.additionalPostImages} 636 - gif={isGif} 637 - /> 638 - )} 596 + <MediaPreview.Embed 597 + embed={post.embed} 598 + style={styles.additionalPostImages} 599 + /> 639 600 </> 640 601 ) 641 602 }
-61
src/view/com/util/images/ImageHorzList.tsx
··· 1 - import React from 'react' 2 - import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 - import {Image} from 'expo-image' 4 - import {AppBskyEmbedImages} from '@atproto/api' 5 - import {Trans} from '@lingui/macro' 6 - 7 - import {atoms as a} from '#/alf' 8 - import {Text} from '#/components/Typography' 9 - 10 - interface Props { 11 - images: AppBskyEmbedImages.ViewImage[] 12 - style?: StyleProp<ViewStyle> 13 - gif?: boolean 14 - } 15 - 16 - export function ImageHorzList({images, style, gif}: Props) { 17 - return ( 18 - <View style={[a.flex_row, a.gap_xs, style]}> 19 - {images.map(({thumb, alt}) => ( 20 - <View 21 - key={thumb} 22 - style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}> 23 - <Image 24 - key={thumb} 25 - source={{uri: thumb}} 26 - style={[a.flex_1, a.rounded_xs]} 27 - accessible={true} 28 - accessibilityIgnoresInvertColors 29 - accessibilityHint={alt} 30 - accessibilityLabel="" 31 - /> 32 - {gif && ( 33 - <View style={styles.altContainer}> 34 - <Text style={styles.alt}> 35 - <Trans>GIF</Trans> 36 - </Text> 37 - </View> 38 - )} 39 - </View> 40 - ))} 41 - </View> 42 - ) 43 - } 44 - 45 - const styles = StyleSheet.create({ 46 - altContainer: { 47 - backgroundColor: 'rgba(0, 0, 0, 0.75)', 48 - borderRadius: 6, 49 - paddingHorizontal: 6, 50 - paddingVertical: 3, 51 - position: 'absolute', 52 - right: 5, 53 - bottom: 5, 54 - zIndex: 2, 55 - }, 56 - alt: { 57 - color: 'white', 58 - fontSize: 7, 59 - fontWeight: 'bold', 60 - }, 61 - })