diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 30ced149..44e42fae 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -1,7 +1,8 @@ -import {Dimensions, Platform} from 'react-native' +import {Dimensions} from 'react-native' import {isSafari} from 'lib/browser' import {isWeb} from 'platform/detection' + const {height: SCREEN_HEIGHT} = Dimensions.get('window') const IFRAME_HOST = isWeb @@ -342,40 +343,17 @@ export function parseEmbedPlayerFromUrl( } } - if (urlp.hostname === 'media.tenor.com') { - let [_, id, filename] = urlp.pathname.split('/') + const tenorGif = parseTenorGif(urlp) + if (tenorGif.success) { + const {playerUri, dimensions} = tenorGif - const h = urlp.searchParams.get('hh') - const w = urlp.searchParams.get('ww') - let dimensions - if (h && w) { - dimensions = { - height: Number(h), - width: Number(w), - } - } - - 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, - } + return { + type: 'tenor_gif', + source: 'tenor', + isGif: true, + hideDetails: true, + playerUri, + 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, + } +} diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index d6c38ea6..9cd7a291 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -8,6 +8,7 @@ import { } from 'react-native' import { AppBskyActorDefs, + AppBskyEmbedExternal, AppBskyEmbedImages, AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, @@ -51,6 +52,7 @@ import {TimeElapsed} from '../util/TimeElapsed' import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar' import hairlineWidth = StyleSheet.hairlineWidth +import {parseTenorGif} from '#/lib/strings/embed-player' const MAX_AUTHORS = 5 @@ -465,17 +467,48 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { const pal = usePalette('default') if (post && AppBskyFeedPost.isRecord(post?.record)) { const text = post.record.text - const images = AppBskyEmbedImages.isView(post.embed) - ? post.embed.images - : AppBskyEmbedRecordWithMedia.isView(post.embed) && - AppBskyEmbedImages.isView(post.embed.media) - ? post.embed.media.images - : undefined + let images + let isGif = false + + if (AppBskyEmbedImages.isView(post.embed)) { + images = post.embed.images + } 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 ( <> {text?.length > 0 && {text}} {images && images.length > 0 && ( - + )} ) diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 12eef14f..bade2a44 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -2,39 +2,60 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {Image} from 'expo-image' import {AppBskyEmbedImages} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {atoms as a} from '#/alf' +import {Text} from '#/components/Typography' interface Props { images: AppBskyEmbedImages.ViewImage[] style?: StyleProp + gif?: boolean } -export function ImageHorzList({images, style}: Props) { +export function ImageHorzList({images, style, gif}: Props) { return ( - + {images.map(({thumb, alt}) => ( - + style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}> + + {gif && ( + + + GIF + + + )} + ))} ) } const styles = StyleSheet.create({ - flexRow: { - flexDirection: 'row', - gap: 5, + altContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 6, + paddingVertical: 3, + position: 'absolute', + right: 5, + bottom: 5, + zIndex: 2, }, - image: { - maxWidth: 100, - aspectRatio: 1, - flex: 1, - borderRadius: 4, + alt: { + color: 'white', + fontSize: 7, + fontWeight: 'bold', }, })