GIF previews in notifications (#4447)

* gifs in notifications

* remove try/catch

* Limit try/catch scope

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
zio/stable
Samuel Newman 2024-06-19 01:09:06 +01:00 committed by GitHub
parent 7ddbc392c3
commit 3dc34be929
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 143 additions and 59 deletions

View File

@ -1,7 +1,8 @@
import {Dimensions, Platform} from 'react-native' import {Dimensions} from 'react-native'
import {isSafari} from 'lib/browser' import {isSafari} from 'lib/browser'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
const {height: SCREEN_HEIGHT} = Dimensions.get('window') const {height: SCREEN_HEIGHT} = Dimensions.get('window')
const IFRAME_HOST = isWeb const IFRAME_HOST = isWeb
@ -342,40 +343,17 @@ export function parseEmbedPlayerFromUrl(
} }
} }
if (urlp.hostname === 'media.tenor.com') { const tenorGif = parseTenorGif(urlp)
let [_, id, filename] = urlp.pathname.split('/') if (tenorGif.success) {
const {playerUri, dimensions} = tenorGif
const h = urlp.searchParams.get('hh') return {
const w = urlp.searchParams.get('ww') type: 'tenor_gif',
let dimensions source: 'tenor',
if (h && w) { isGif: true,
dimensions = { hideDetails: true,
height: Number(h), playerUri,
width: Number(w), dimensions,
}
}
if (id && filename && dimensions && id.includes('AAAAC')) {
if (Platform.OS === 'web') {
if (isSafari) {
id = id.replace('AAAAC', 'AAAP1')
filename = filename.replace('.gif', '.mp4')
} else {
id = id.replace('AAAAC', 'AAAP3')
filename = filename.replace('.gif', '.webm')
}
} else {
id = id.replace('AAAAC', 'AAAAM')
}
return {
type: 'tenor_gif',
source: 'tenor',
isGif: true,
hideDetails: true,
playerUri: `https://t.gifs.bsky.app/${id}/${filename}`,
dimensions,
}
} }
} }
@ -516,3 +494,55 @@ export function getGiphyMetaUri(url: URL) {
} }
} }
} }
export function parseTenorGif(urlp: URL):
| {success: false}
| {
success: true
playerUri: string
dimensions: {height: number; width: number}
} {
if (urlp.hostname !== 'media.tenor.com') {
return {success: false}
}
let [_, id, filename] = urlp.pathname.split('/')
if (!id || !filename) {
return {success: false}
}
if (!id.includes('AAAAC')) {
return {success: false}
}
const h = urlp.searchParams.get('hh')
const w = urlp.searchParams.get('ww')
if (!h || !w) {
return {success: false}
}
const dimensions = {
height: Number(h),
width: Number(w),
}
if (isWeb) {
if (isSafari) {
id = id.replace('AAAAC', 'AAAP1')
filename = filename.replace('.gif', '.mp4')
} else {
id = id.replace('AAAAC', 'AAAP3')
filename = filename.replace('.gif', '.webm')
}
} else {
id = id.replace('AAAAC', 'AAAAM')
}
return {
success: true,
playerUri: `https://t.gifs.bsky.app/${id}/${filename}`,
dimensions,
}
}

View File

@ -8,6 +8,7 @@ import {
} from 'react-native' } from 'react-native'
import { import {
AppBskyActorDefs, AppBskyActorDefs,
AppBskyEmbedExternal,
AppBskyEmbedImages, AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia, AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs, AppBskyFeedDefs,
@ -51,6 +52,7 @@ import {TimeElapsed} from '../util/TimeElapsed'
import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth import hairlineWidth = StyleSheet.hairlineWidth
import {parseTenorGif} from '#/lib/strings/embed-player'
const MAX_AUTHORS = 5 const MAX_AUTHORS = 5
@ -465,17 +467,48 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
const pal = usePalette('default') const pal = usePalette('default')
if (post && AppBskyFeedPost.isRecord(post?.record)) { if (post && AppBskyFeedPost.isRecord(post?.record)) {
const text = post.record.text const text = post.record.text
const images = AppBskyEmbedImages.isView(post.embed) let images
? post.embed.images let isGif = false
: AppBskyEmbedRecordWithMedia.isView(post.embed) &&
AppBskyEmbedImages.isView(post.embed.media) if (AppBskyEmbedImages.isView(post.embed)) {
? post.embed.media.images images = post.embed.images
: undefined } else if (
AppBskyEmbedRecordWithMedia.isView(post.embed) &&
AppBskyEmbedImages.isView(post.embed.media)
) {
images = post.embed.media.images
} else if (
AppBskyEmbedExternal.isView(post.embed) &&
post.embed.external.thumb
) {
let url: URL | undefined
try {
url = new URL(post.embed.external.uri)
} catch {}
if (url) {
const {success} = parseTenorGif(url)
if (success) {
isGif = true
images = [
{
thumb: post.embed.external.thumb,
alt: post.embed.external.title,
fullsize: post.embed.external.thumb,
},
]
}
}
}
return ( return (
<> <>
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>} {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
{images && images.length > 0 && ( {images && images.length > 0 && (
<ImageHorzList images={images} style={styles.additionalPostImages} /> <ImageHorzList
images={images}
style={styles.additionalPostImages}
gif={isGif}
/>
)} )}
</> </>
) )

View File

@ -2,39 +2,60 @@ import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {AppBskyEmbedImages} from '@atproto/api' import {AppBskyEmbedImages} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
interface Props { interface Props {
images: AppBskyEmbedImages.ViewImage[] images: AppBskyEmbedImages.ViewImage[]
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
gif?: boolean
} }
export function ImageHorzList({images, style}: Props) { export function ImageHorzList({images, style, gif}: Props) {
return ( return (
<View style={[styles.flexRow, style]}> <View style={[a.flex_row, a.gap_xs, style]}>
{images.map(({thumb, alt}) => ( {images.map(({thumb, alt}) => (
<Image <View
key={thumb} key={thumb}
source={{uri: thumb}} style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}>
style={styles.image} <Image
accessible={true} key={thumb}
accessibilityIgnoresInvertColors source={{uri: thumb}}
accessibilityHint={alt} style={[a.flex_1, a.rounded_xs]}
accessibilityLabel="" accessible={true}
/> accessibilityIgnoresInvertColors
accessibilityHint={alt}
accessibilityLabel=""
/>
{gif && (
<View style={styles.altContainer}>
<Text style={styles.alt}>
<Trans>GIF</Trans>
</Text>
</View>
)}
</View>
))} ))}
</View> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
flexRow: { altContainer: {
flexDirection: 'row', backgroundColor: 'rgba(0, 0, 0, 0.75)',
gap: 5, borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
position: 'absolute',
right: 5,
bottom: 5,
zIndex: 2,
}, },
image: { alt: {
maxWidth: 100, color: 'white',
aspectRatio: 1, fontSize: 7,
flex: 1, fontWeight: 'bold',
borderRadius: 4,
}, },
}) })