diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-03-31 13:17:26 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-31 13:17:26 -0500 |
commit | a3334a01a221877d3e06e02f960fda441f3460bd (patch) | |
tree | 64cdbb1232d1a3c00750c346b6e3ae529b51d1b0 /src/view/com/util/post-embeds | |
parent | 19f3a2fa92a61ddb785fc4e42d73792c1d0e772c (diff) | |
download | voidsky-a3334a01a221877d3e06e02f960fda441f3460bd.tar.zst |
Lex refactor (#362)
* Remove the hackcheck for upgrades * Rename the PostEmbeds folder to match the codebase style * Updates to latest lex refactor * Update to use new bsky agent * Update to use api package's richtext library * Switch to upsertProfile * Add TextEncoder/TextDecoder polyfill * Add Intl.Segmenter polyfill * Update composer to calculate lengths by grapheme * Fix detox * Fix login in e2e * Create account e2e passing * Implement an e2e mocking framework * Don't use private methods on mobx models as mobx can't track them * Add tooling for e2e-specific builds and add e2e media-picker mock * Add some tests and fix some bugs around profile editing * Add shell tests * Add home screen tests * Add thread screen tests * Add tests for other user profile screens * Add search screen tests * Implement profile imagery change tools and tests * Update to new embed behaviors * Add post tests * Fix to profile-screen test * Fix session resumption * Update web composer to new api * 1.11.0 * Fix pagination cursor parameters * Add quote posts to notifications * Fix embed layouts * Remove youtube inline player and improve tap handling on link cards * Reset minimal shell mode on all screen loads and feed swipes (close #299) * Update podfile.lock * Improve post notfound UI (close #366) * Bump atproto packages
Diffstat (limited to 'src/view/com/util/post-embeds')
-rw-r--r-- | src/view/com/util/post-embeds/ExternalLinkEmbed.tsx | 62 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/QuoteEmbed.tsx | 81 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/YoutubeEmbed.tsx | 55 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 198 |
4 files changed, 396 insertions, 0 deletions
diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx new file mode 100644 index 000000000..a4cbb3e29 --- /dev/null +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import {Text} from '../text/Text' +import {AutoSizedImage} from '../images/AutoSizedImage' +import {StyleSheet, View} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {AppBskyEmbedExternal} from '@atproto/api' + +export const ExternalLinkEmbed = ({ + link, + imageChild, +}: { + link: AppBskyEmbedExternal.ViewExternal + imageChild?: React.ReactNode +}) => { + const pal = usePalette('default') + return ( + <> + {link.thumb ? ( + <AutoSizedImage uri={link.thumb} style={styles.extImage}> + {imageChild} + </AutoSizedImage> + ) : undefined} + <View style={styles.extInner}> + <Text type="md-bold" numberOfLines={2} style={[pal.text]}> + {link.title || link.uri} + </Text> + <Text + type="sm" + numberOfLines={1} + style={[pal.textLight, styles.extUri]}> + {link.uri} + </Text> + {link.description ? ( + <Text + type="sm" + numberOfLines={2} + style={[pal.text, styles.extDescription]}> + {link.description} + </Text> + ) : undefined} + </View> + </> + ) +} + +const styles = StyleSheet.create({ + extInner: { + padding: 10, + }, + extImage: { + borderTopLeftRadius: 6, + borderTopRightRadius: 6, + width: '100%', + maxHeight: 200, + }, + extUri: { + marginTop: 2, + }, + extDescription: { + marginTop: 4, + }, +}) diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx new file mode 100644 index 000000000..9dc5739a0 --- /dev/null +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import {StyleProp, StyleSheet, ViewStyle} from 'react-native' +import {AppBskyEmbedImages, AppBskyEmbedRecordWithMedia} from '@atproto/api' +import {AtUri} from '../../../../third-party/uri' +import {PostMeta} from '../PostMeta' +import {Link} from '../Link' +import {Text} from '../text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {ComposerOptsQuote} from 'state/models/ui/shell' +import {PostEmbeds} from '.' + +export function QuoteEmbed({ + quote, + style, +}: { + quote: ComposerOptsQuote + style?: StyleProp<ViewStyle> +}) { + const pal = usePalette('default') + const itemUrip = new AtUri(quote.uri) + const itemHref = `/profile/${quote.author.handle}/post/${itemUrip.rkey}` + const itemTitle = `Post by ${quote.author.handle}` + const isEmpty = React.useMemo( + () => quote.text.trim().length === 0, + [quote.text], + ) + const imagesEmbed = React.useMemo( + () => + quote.embeds?.find( + embed => + AppBskyEmbedImages.isView(embed) || + AppBskyEmbedRecordWithMedia.isView(embed), + ), + [quote.embeds], + ) + return ( + <Link + style={[styles.container, pal.border, style]} + href={itemHref} + title={itemTitle}> + <PostMeta + authorAvatar={quote.author.avatar} + authorHandle={quote.author.handle} + authorDisplayName={quote.author.displayName} + postHref={itemHref} + timestamp={quote.indexedAt} + /> + <Text type="post-text" style={pal.text} numberOfLines={6}> + {isEmpty ? ( + <Text style={pal.link} lineHeight={1.5}> + View post + </Text> + ) : ( + quote.text + )} + </Text> + {AppBskyEmbedImages.isView(imagesEmbed) && ( + <PostEmbeds embed={imagesEmbed} /> + )} + {AppBskyEmbedRecordWithMedia.isView(imagesEmbed) && ( + <PostEmbeds embed={imagesEmbed.media} /> + )} + </Link> + ) +} + +export default QuoteEmbed + +const styles = StyleSheet.create({ + container: { + borderRadius: 8, + paddingVertical: 8, + paddingHorizontal: 12, + borderWidth: 1, + }, + quotePost: { + flex: 1, + paddingLeft: 13, + paddingRight: 8, + }, +}) diff --git a/src/view/com/util/post-embeds/YoutubeEmbed.tsx b/src/view/com/util/post-embeds/YoutubeEmbed.tsx new file mode 100644 index 000000000..2ca0750a3 --- /dev/null +++ b/src/view/com/util/post-embeds/YoutubeEmbed.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {ExternalLinkEmbed} from './ExternalLinkEmbed' +import {AppBskyEmbedExternal} from '@atproto/api' +import {Link} from '../Link' + +export const YoutubeEmbed = ({ + link, + style, +}: { + link: AppBskyEmbedExternal.ViewExternal + style?: StyleProp<ViewStyle> +}) => { + const pal = usePalette('default') + + const imageChild = ( + <View style={styles.playButton}> + <FontAwesomeIcon icon="play" size={24} color="white" /> + </View> + ) + + return ( + <Link + style={[styles.extOuter, pal.view, pal.border, style]} + href={link.uri} + noFeedback> + <ExternalLinkEmbed link={link} imageChild={imageChild} /> + </Link> + ) +} + +const styles = StyleSheet.create({ + extOuter: { + borderWidth: 1, + borderRadius: 8, + }, + playButton: { + position: 'absolute', + alignSelf: 'center', + alignItems: 'center', + top: '44%', + justifyContent: 'center', + backgroundColor: 'black', + padding: 10, + borderRadius: 50, + opacity: 0.8, + }, + webView: { + alignItems: 'center', + alignContent: 'center', + justifyContent: 'center', + }, +}) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx new file mode 100644 index 000000000..726bea6e7 --- /dev/null +++ b/src/view/com/util/post-embeds/index.tsx @@ -0,0 +1,198 @@ +import React from 'react' +import { + StyleSheet, + StyleProp, + View, + ViewStyle, + Image as RNImage, +} from 'react-native' +import { + AppBskyEmbedImages, + AppBskyEmbedExternal, + AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, + AppBskyFeedPost, +} from '@atproto/api' +import {Link} from '../Link' +import {AutoSizedImage} from '../images/AutoSizedImage' +import {ImageLayoutGrid} from '../images/ImageLayoutGrid' +import {ImagesLightbox} from 'state/models/ui/shell' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {saveImageModal} from 'lib/media/manip' +import {YoutubeEmbed} from './YoutubeEmbed' +import {ExternalLinkEmbed} from './ExternalLinkEmbed' +import {getYoutubeVideoId} from 'lib/strings/url-helpers' +import QuoteEmbed from './QuoteEmbed' + +type Embed = + | AppBskyEmbedRecord.View + | AppBskyEmbedImages.View + | AppBskyEmbedExternal.View + | AppBskyEmbedRecordWithMedia.View + | {$type: string; [k: string]: unknown} + +export function PostEmbeds({ + embed, + style, +}: { + embed?: Embed + style?: StyleProp<ViewStyle> +}) { + const pal = usePalette('default') + const store = useStores() + + if ( + AppBskyEmbedRecordWithMedia.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record.record) && + AppBskyFeedPost.isRecord(embed.record.record.value) && + AppBskyFeedPost.validateRecord(embed.record.record.value).success + ) { + return ( + <View style={[styles.stackContainer, style]}> + <PostEmbeds embed={embed.media} /> + <QuoteEmbed + quote={{ + author: embed.record.record.author, + cid: embed.record.record.cid, + uri: embed.record.record.uri, + indexedAt: embed.record.record.indexedAt, + text: embed.record.record.value.text, + embeds: embed.record.record.embeds, + }} + /> + </View> + ) + } + + if (AppBskyEmbedRecord.isView(embed)) { + if ( + AppBskyEmbedRecord.isViewRecord(embed.record) && + AppBskyFeedPost.isRecord(embed.record.value) && + AppBskyFeedPost.validateRecord(embed.record.value).success + ) { + return ( + <QuoteEmbed + quote={{ + author: embed.record.author, + cid: embed.record.cid, + uri: embed.record.uri, + indexedAt: embed.record.indexedAt, + text: embed.record.value.text, + embeds: embed.record.embeds, + }} + style={style} + /> + ) + } + } + + if (AppBskyEmbedImages.isView(embed)) { + if (embed.images.length > 0) { + const uris = embed.images.map(img => img.fullsize) + const openLightbox = (index: number) => { + store.shell.openLightbox(new ImagesLightbox(uris, index)) + } + const onLongPress = (index: number) => { + saveImageModal({uri: uris[index]}) + } + const onPressIn = (index: number) => { + const firstImageToShow = uris[index] + RNImage.prefetch(firstImageToShow) + uris.forEach(uri => { + if (firstImageToShow !== uri) { + // First image already prefeched above + RNImage.prefetch(uri) + } + }) + } + + if (embed.images.length === 4) { + return ( + <View style={[styles.imagesContainer, style]}> + <ImageLayoutGrid + type="four" + uris={embed.images.map(img => img.thumb)} + onPress={openLightbox} + onLongPress={onLongPress} + onPressIn={onPressIn} + /> + </View> + ) + } else if (embed.images.length === 3) { + return ( + <View style={[styles.imagesContainer, style]}> + <ImageLayoutGrid + type="three" + uris={embed.images.map(img => img.thumb)} + onPress={openLightbox} + onLongPress={onLongPress} + onPressIn={onPressIn} + /> + </View> + ) + } else if (embed.images.length === 2) { + return ( + <View style={[styles.imagesContainer, style]}> + <ImageLayoutGrid + type="two" + uris={embed.images.map(img => img.thumb)} + onPress={openLightbox} + onLongPress={onLongPress} + onPressIn={onPressIn} + /> + </View> + ) + } else { + return ( + <View style={[styles.imagesContainer, style]}> + <AutoSizedImage + uri={embed.images[0].thumb} + onPress={() => openLightbox(0)} + onLongPress={() => onLongPress(0)} + onPressIn={() => onPressIn(0)} + style={styles.singleImage} + /> + </View> + ) + } + } + } + + if (AppBskyEmbedExternal.isView(embed)) { + const link = embed.external + const youtubeVideoId = getYoutubeVideoId(link.uri) + + if (youtubeVideoId) { + return <YoutubeEmbed link={link} style={style} /> + } + + return ( + <Link + style={[styles.extOuter, pal.view, pal.border, style]} + href={link.uri} + noFeedback> + <ExternalLinkEmbed link={link} /> + </Link> + ) + } + return <View /> +} + +const styles = StyleSheet.create({ + stackContainer: { + gap: 6, + }, + imagesContainer: { + marginTop: 4, + }, + singleImage: { + borderRadius: 8, + maxHeight: 500, + }, + extOuter: { + borderWidth: 1, + borderRadius: 8, + marginTop: 4, + }, +}) |