73-post-embeds (#253)

* update api to 0.1.3

* add repost modal with reposting functionality

* add quote post UI

* allow creation and view of quote posts

* Validate the post record before rendering a quote post

* Use createdAt in quote posts for now

* add web modal support

* Tune the quote post rendering

* Make did and declarationCid optional in postmeta

* Make did and declarationCid optional in postmeta

* dont allow image or link preview if quote post

* Handle no-text quote posts

* Tune the repost modal

* Tweak composer post text

* Fix lint

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Ansh 2023-03-02 16:09:48 -08:00 committed by GitHub
parent f539659ac8
commit 75174a6c37
18 changed files with 392 additions and 69 deletions

View file

@ -26,6 +26,7 @@ import {
} from 'lib/icons'
import {s, colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {useStores} from 'state/index'
interface PostCtrlsOpts {
itemUri: string
@ -33,6 +34,13 @@ interface PostCtrlsOpts {
itemHref: string
itemTitle: string
isAuthor: boolean
author: {
handle: string
displayName: string
avatar: string
}
text: string
indexedAt: string
big?: boolean
style?: StyleProp<ViewStyle>
replyCount?: number
@ -86,6 +94,7 @@ function ctrlAnimStyle(interp: Animated.Value) {
*/
export function PostCtrls(opts: PostCtrlsOpts) {
const store = useStores()
const theme = useTheme()
const defaultCtrlColor = React.useMemo(
() => ({
@ -98,7 +107,8 @@ export function PostCtrls(opts: PostCtrlsOpts) {
// DISABLED see #135
// const repostRef = React.useRef<TriggerableAnimatedRef | null>(null)
// const likeRef = React.useRef<TriggerableAnimatedRef | null>(null)
const onPressToggleRepostWrapper = () => {
const onRepost = () => {
store.shell.closeModal()
if (!opts.isReposted) {
ReactNativeHapticFeedback.trigger('impactMedium')
setRepostMod(1)
@ -122,6 +132,30 @@ export function PostCtrls(opts: PostCtrlsOpts) {
.then(() => setRepostMod(0))
}
}
const onQuote = () => {
store.shell.closeModal()
store.shell.openComposer({
quote: {
uri: opts.itemUri,
cid: opts.itemCid,
text: opts.text,
author: opts.author,
indexedAt: opts.indexedAt,
},
})
ReactNativeHapticFeedback.trigger('impactMedium')
}
const onPressToggleRepostWrapper = () => {
store.shell.openModal({
name: 'repost',
onRepost: onRepost,
onQuote: onQuote,
isReposted: opts.isReposted,
})
}
const onPressToggleUpvoteWrapper = () => {
if (!opts.isUpvoted) {
ReactNativeHapticFeedback.trigger('impactMedium')

View file

@ -0,0 +1,58 @@
import {StyleSheet} from 'react-native'
import React from 'react'
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/shell-ui'
const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => {
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],
)
return (
<Link
style={[styles.container, pal.border]}
href={itemHref}
title={itemTitle}>
<PostMeta
authorAvatar={quote.author.avatar}
authorHandle={quote.author.handle}
authorDisplayName={quote.author.displayName}
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>
</Link>
)
}
export default QuoteEmbed
const styles = StyleSheet.create({
container: {
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 12,
marginVertical: 8,
borderWidth: 1,
},
quotePost: {
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
})

View file

@ -6,7 +6,12 @@ import {
ViewStyle,
Image as RNImage,
} from 'react-native'
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
import {
AppBskyEmbedImages,
AppBskyEmbedExternal,
AppBskyEmbedRecord,
AppBskyFeedPost,
} from '@atproto/api'
import {Link} from '../Link'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@ -17,8 +22,10 @@ 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.Presented
| AppBskyEmbedImages.Presented
| AppBskyEmbedExternal.Presented
| {$type: string; [k: string]: unknown}
@ -32,6 +39,25 @@ export function PostEmbeds({
}) {
const pal = usePalette('default')
const store = useStores()
if (AppBskyEmbedRecord.isPresented(embed)) {
if (
AppBskyEmbedRecord.isPresentedRecord(embed.record) &&
AppBskyFeedPost.isRecord(embed.record.record) &&
AppBskyFeedPost.validateRecord(embed.record.record).success
) {
return (
<QuoteEmbed
quote={{
author: embed.record.author,
cid: embed.record.cid,
uri: embed.record.uri,
indexedAt: embed.record.record.createdAt, // TODO
text: embed.record.record.text,
}}
/>
)
}
}
if (AppBskyEmbedImages.isPresented(embed)) {
if (embed.images.length > 0) {
const uris = embed.images.map(img => img.fullsize)

View file

@ -4,15 +4,17 @@ import {Text} from './text/Text'
import {ago} from 'lib/strings/time'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {UserAvatar} from './UserAvatar'
import {observer} from 'mobx-react-lite'
import FollowButton from '../profile/FollowButton'
interface PostMetaOpts {
authorAvatar: string | undefined
authorHandle: string
authorDisplayName: string | undefined
timestamp: string
did: string
declarationCid: string
did?: string
declarationCid?: string
showFollowBtn?: boolean
}
@ -27,11 +29,18 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
// don't change this UI immediately, but rather upon future
// renders
const isFollowing = React.useMemo(
() => store.me.follows.isFollowing(opts.did),
() =>
typeof opts.did === 'string' && store.me.follows.isFollowing(opts.did),
[opts.did, store.me.follows],
)
if (opts.showFollowBtn && !isMe && !isFollowing) {
if (
opts.showFollowBtn &&
!isMe &&
!isFollowing &&
opts.did &&
opts.declarationCid
) {
// two-liner with follow button
return (
<View style={[styles.metaTwoLine]}>
@ -71,6 +80,16 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
// one-liner
return (
<View style={styles.meta}>
{typeof opts.authorAvatar !== 'undefined' && (
<View style={[styles.metaItem, styles.avatar]}>
<UserAvatar
avatar={opts.authorAvatar}
handle={opts.authorHandle}
displayName={opts.authorDisplayName}
size={16}
/>
</View>
)}
<View style={[styles.metaItem, styles.maxWidth]}>
<Text
type="lg-bold"
@ -107,6 +126,9 @@ const styles = StyleSheet.create({
metaItem: {
paddingRight: 5,
},
avatar: {
alignSelf: 'center',
},
maxWidth: {
maxWidth: '80%',
},