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
parent
7ddbc392c3
commit
3dc34be929
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue