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:
parent
f539659ac8
commit
75174a6c37
18 changed files with 392 additions and 69 deletions
|
@ -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')
|
||||
|
|
58
src/view/com/util/PostEmbeds/QuoteEmbed.tsx
Normal file
58
src/view/com/util/PostEmbeds/QuoteEmbed.tsx
Normal 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,
|
||||
},
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -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%',
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue