Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 235 lines 5.9 kB view raw
1import React from 'react' 2import { 3 Pressable, 4 type StyleProp, 5 StyleSheet, 6 TouchableOpacity, 7 View, 8 type ViewStyle, 9} from 'react-native' 10import {msg, Trans} from '@lingui/macro' 11import {useLingui} from '@lingui/react' 12 13import {HITSLOP_20} from '#/lib/constants' 14import {clamp} from '#/lib/numbers' 15import {type EmbedPlayerParams} from '#/lib/strings/embed-player' 16import {useAutoplayDisabled} from '#/state/preferences' 17import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 18import {atoms as a, useTheme} from '#/alf' 19import {Fill} from '#/components/Fill' 20import {Loader} from '#/components/Loader' 21import * as Prompt from '#/components/Prompt' 22import {Text} from '#/components/Typography' 23import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 24import {IS_WEB} from '#/env' 25import {GifView} from '../../../../../modules/expo-bluesky-gif-view' 26import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' 27 28function PlaybackControls({ 29 onPress, 30 isPlaying, 31 isLoaded, 32}: { 33 onPress: () => void 34 isPlaying: boolean 35 isLoaded: boolean 36}) { 37 const {_} = useLingui() 38 const t = useTheme() 39 40 return ( 41 <Pressable 42 accessibilityRole="button" 43 accessibilityHint={_(msg`Plays or pauses the GIF`)} 44 accessibilityLabel={isPlaying ? _(msg`Pause`) : _(msg`Play`)} 45 style={[ 46 a.absolute, 47 a.align_center, 48 a.justify_center, 49 !isLoaded && a.border, 50 t.atoms.border_contrast_medium, 51 a.inset_0, 52 a.w_full, 53 a.h_full, 54 { 55 zIndex: 2, 56 backgroundColor: !isLoaded 57 ? t.atoms.bg_contrast_25.backgroundColor 58 : undefined, 59 }, 60 ]} 61 onPress={onPress}> 62 {!isLoaded ? ( 63 <View> 64 <View style={[a.align_center, a.justify_center]}> 65 <Loader size="xl" /> 66 </View> 67 </View> 68 ) : !isPlaying ? ( 69 <PlayButtonIcon /> 70 ) : undefined} 71 </Pressable> 72 ) 73} 74 75export function GifEmbed({ 76 params, 77 thumb, 78 altText, 79 isPreferredAltText, 80 hideAlt, 81 style = {width: '100%'}, 82}: { 83 params: EmbedPlayerParams 84 thumb: string | undefined 85 altText: string 86 isPreferredAltText: boolean 87 hideAlt?: boolean 88 style?: StyleProp<ViewStyle> 89}) { 90 const t = useTheme() 91 const {_} = useLingui() 92 const autoplayDisabled = useAutoplayDisabled() 93 94 const playerRef = React.useRef<GifView>(null) 95 96 const [playerState, setPlayerState] = React.useState<{ 97 isPlaying: boolean 98 isLoaded: boolean 99 }>({ 100 isPlaying: !autoplayDisabled, 101 isLoaded: false, 102 }) 103 104 const onPlayerStateChange = React.useCallback( 105 (e: GifViewStateChangeEvent) => { 106 setPlayerState(e.nativeEvent) 107 }, 108 [], 109 ) 110 111 const onPress = React.useCallback(() => { 112 playerRef.current?.toggleAsync() 113 }, []) 114 115 let aspectRatio = 1 116 if (params.dimensions) { 117 aspectRatio = clamp( 118 params.dimensions.width / params.dimensions.height, 119 0.75, 120 4, 121 ) 122 } 123 124 return ( 125 <View 126 style={[ 127 a.rounded_md, 128 a.overflow_hidden, 129 a.border, 130 t.atoms.border_contrast_low, 131 {backgroundColor: t.palette.black}, 132 {aspectRatio}, 133 style, 134 ]}> 135 <View 136 style={[ 137 a.absolute, 138 /* 139 * Aspect ratio was being clipped weirdly on web -esb 140 */ 141 { 142 top: -2, 143 bottom: -2, 144 left: -2, 145 right: -2, 146 }, 147 ]}> 148 <PlaybackControls 149 onPress={onPress} 150 isPlaying={playerState.isPlaying} 151 isLoaded={playerState.isLoaded} 152 /> 153 <GifView 154 source={params.playerUri} 155 placeholderSource={thumb} 156 style={[a.flex_1]} 157 autoplay={!autoplayDisabled} 158 onPlayerStateChange={onPlayerStateChange} 159 ref={playerRef} 160 accessibilityHint={_(msg`Animated GIF`)} 161 accessibilityLabel={altText} 162 /> 163 {!playerState.isPlaying && ( 164 <Fill 165 style={[ 166 t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg, 167 { 168 opacity: 0.3, 169 }, 170 ]} 171 /> 172 )} 173 {!hideAlt && isPreferredAltText && <AltText text={altText} />} 174 </View> 175 </View> 176 ) 177} 178 179function AltText({text}: {text: string}) { 180 const control = Prompt.usePromptControl() 181 const largeAltBadge = useLargeAltBadgeEnabled() 182 183 const {_} = useLingui() 184 return ( 185 <> 186 <TouchableOpacity 187 testID="altTextButton" 188 accessibilityRole="button" 189 accessibilityLabel={_(msg`Show alt text`)} 190 accessibilityHint="" 191 hitSlop={HITSLOP_20} 192 onPress={control.open} 193 style={styles.altContainer}> 194 <Text 195 style={[styles.alt, largeAltBadge && a.text_xs]} 196 accessible={false}> 197 <Trans>ALT</Trans> 198 </Text> 199 </TouchableOpacity> 200 <Prompt.Outer control={control}> 201 <Prompt.TitleText> 202 <Trans>Alt Text</Trans> 203 </Prompt.TitleText> 204 <Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText> 205 <Prompt.Actions> 206 <Prompt.Action 207 onPress={() => control.close()} 208 cta={_(msg`Close`)} 209 color="secondary" 210 /> 211 </Prompt.Actions> 212 </Prompt.Outer> 213 </> 214 ) 215} 216 217const styles = StyleSheet.create({ 218 altContainer: { 219 backgroundColor: 'rgba(0, 0, 0, 0.75)', 220 borderRadius: 6, 221 paddingHorizontal: IS_WEB ? 8 : 6, 222 paddingVertical: IS_WEB ? 6 : 3, 223 position: 'absolute', 224 // Related to margin/gap hack. This keeps the alt label in the same position 225 // on all platforms 226 right: IS_WEB ? 8 : 5, 227 bottom: IS_WEB ? 8 : 5, 228 zIndex: 2, 229 }, 230 alt: { 231 color: 'white', 232 fontSize: IS_WEB ? 10 : 7, 233 fontWeight: '600', 234 }, 235})