diff options
author | Ansh <anshnanda10@gmail.com> | 2023-03-02 16:09:48 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-02 18:09:48 -0600 |
commit | 75174a6c37a01282b8bd1389fee3fb00488dcf0d (patch) | |
tree | 6ced8feb720a2ccd4c3978740f22ce2ec7e59ccc /src | |
parent | f539659ac8eb0857e888ea2a972f78305a42e201 (diff) | |
download | voidsky-75174a6c37a01282b8bd1389fee3fb00488dcf0d.tar.zst |
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>
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/api/index.ts | 80 | ||||
-rw-r--r-- | src/state/models/shell-ui.ts | 20 | ||||
-rw-r--r-- | src/view/com/composer/ComposePost.tsx | 64 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/modals/Repost.tsx | 90 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 14 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 7 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 7 | ||||
-rw-r--r-- | src/view/com/util/PostCtrls.tsx | 36 | ||||
-rw-r--r-- | src/view/com/util/PostEmbeds/QuoteEmbed.tsx | 58 | ||||
-rw-r--r-- | src/view/com/util/PostEmbeds/index.tsx | 28 | ||||
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 30 | ||||
-rw-r--r-- | src/view/index.ts | 6 | ||||
-rw-r--r-- | src/view/shell/mobile/Composer.tsx | 3 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 1 |
16 files changed, 387 insertions, 64 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index ae156928e..3b8af44e8 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -2,6 +2,7 @@ import { AppBskyEmbedImages, AppBskyEmbedExternal, ComAtprotoBlobUpload, + AppBskyEmbedRecord, } from '@atproto/api' import {AtUri} from '../../third-party/uri' import {RootStoreModel} from 'state/models/root-store' @@ -51,23 +52,32 @@ export async function uploadBlob( } } -export async function post( - store: RootStoreModel, - rawText: string, - replyTo?: string, - extLink?: ExternalEmbedDraft, - images?: string[], - knownHandles?: Set<string>, - onStateChange?: (state: string) => void, -) { - let embed: AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main | undefined +interface PostOpts { + rawText: string + replyTo?: string + quote?: { + uri: string + cid: string + } + extLink?: ExternalEmbedDraft + images?: string[] + knownHandles?: Set<string> + onStateChange?: (state: string) => void +} + +export async function post(store: RootStoreModel, opts: PostOpts) { + let embed: + | AppBskyEmbedImages.Main + | AppBskyEmbedExternal.Main + | AppBskyEmbedRecord.Main + | undefined let reply - const text = new RichText(rawText, undefined, { + const text = new RichText(opts.rawText, undefined, { cleanNewlines: true, }).text.trim() - onStateChange?.('Processing...') - const entities = extractEntities(text, knownHandles) + opts.onStateChange?.('Processing...') + const entities = extractEntities(text, opts.knownHandles) if (entities) { for (const ent of entities) { if (ent.type === 'mention') { @@ -77,14 +87,22 @@ export async function post( } } - if (images?.length) { + if (opts.quote) { + embed = { + $type: 'app.bsky.embed.record', + record: { + uri: opts.quote.uri, + cid: opts.quote.cid, + }, + } as AppBskyEmbedRecord.Main + } else if (opts.images?.length) { embed = { $type: 'app.bsky.embed.images', images: [], } as AppBskyEmbedImages.Main let i = 1 - for (const image of images) { - onStateChange?.(`Uploading image #${i++}...`) + for (const image of opts.images) { + opts.onStateChange?.(`Uploading image #${i++}...`) const res = await uploadBlob(store, image, 'image/jpeg') embed.images.push({ image: { @@ -94,30 +112,28 @@ export async function post( alt: '', // TODO supply alt text }) } - } - - if (!embed && extLink) { + } else if (opts.extLink) { let thumb - if (extLink.localThumb) { - onStateChange?.('Uploading link thumbnail...') + if (opts.extLink.localThumb) { + opts.onStateChange?.('Uploading link thumbnail...') let encoding - if (extLink.localThumb.path.endsWith('.png')) { + if (opts.extLink.localThumb.path.endsWith('.png')) { encoding = 'image/png' } else if ( - extLink.localThumb.path.endsWith('.jpeg') || - extLink.localThumb.path.endsWith('.jpg') + opts.extLink.localThumb.path.endsWith('.jpeg') || + opts.extLink.localThumb.path.endsWith('.jpg') ) { encoding = 'image/jpeg' } else { store.log.warn( 'Unexpected image format for thumbnail, skipping', - extLink.localThumb.path, + opts.extLink.localThumb.path, ) } if (encoding) { const thumbUploadRes = await uploadBlob( store, - extLink.localThumb.path, + opts.extLink.localThumb.path, encoding, ) thumb = { @@ -129,16 +145,16 @@ export async function post( embed = { $type: 'app.bsky.embed.external', external: { - uri: extLink.uri, - title: extLink.meta?.title || '', - description: extLink.meta?.description || '', + uri: opts.extLink.uri, + title: opts.extLink.meta?.title || '', + description: opts.extLink.meta?.description || '', thumb, }, } as AppBskyEmbedExternal.Main } - if (replyTo) { - const replyToUrip = new AtUri(replyTo) + if (opts.replyTo) { + const replyToUrip = new AtUri(opts.replyTo) const parentPost = await store.api.app.bsky.feed.post.get({ user: replyToUrip.host, rkey: replyToUrip.rkey, @@ -156,7 +172,7 @@ export async function post( } try { - onStateChange?.('Posting...') + opts.onStateChange?.('Posting...') return await store.api.app.bsky.feed.post.create( {did: store.me.did || ''}, { diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts index 640bed0b3..1b0e350a2 100644 --- a/src/state/models/shell-ui.ts +++ b/src/state/models/shell-ui.ts @@ -44,6 +44,13 @@ export interface DeleteAccountModal { name: 'delete-account' } +export interface RepostModal { + name: 'repost' + onRepost: () => void + onQuote: () => void + isReposted: boolean +} + export type Modal = | ConfirmModal | EditProfileModal @@ -52,6 +59,7 @@ export type Modal = | ReportAccountModal | CropImageModal | DeleteAccountModal + | RepostModal interface LightboxModel {} @@ -82,10 +90,22 @@ export interface ComposerOptsPostRef { avatar?: string } } +export interface ComposerOptsQuote { + uri: string + cid: string + text: string + indexedAt: string + author: { + handle: string + displayName?: string + avatar?: string + } +} export interface ComposerOpts { imagesOpen?: boolean replyTo?: ComposerOptsPostRef onPost?: () => void + quote?: ComposerOptsQuote } export class ShellUiModel { diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index e3befafff..ad6a8ec66 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -48,6 +48,7 @@ import { POST_IMG_MAX_SIZE, } from 'lib/constants' import {isWeb} from 'platform/detection' +import QuoteEmbed from '../util/PostEmbeds/QuoteEmbed' const MAX_TEXT_LENGTH = 256 const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} @@ -62,11 +63,13 @@ export const ComposePost = observer(function ComposePost({ imagesOpen, onPost, onClose, + quote, }: { replyTo?: ComposerOpts['replyTo'] imagesOpen?: ComposerOpts['imagesOpen'] onPost?: ComposerOpts['onPost'] onClose: () => void + quote?: ComposerOpts['quote'] }) { const {track} = useAnalytics() const pal = usePalette('default') @@ -280,15 +283,15 @@ export const ComposePost = observer(function ComposePost({ } setIsProcessing(true) try { - await apilib.post( - store, - text, - replyTo?.uri, - extLink, - selectedPhotos, - autocompleteView.knownHandles, - setProcessingState, - ) + await apilib.post(store, { + rawText: text, + replyTo: replyTo?.uri, + images: selectedPhotos, + quote: quote, + extLink: extLink, + onStateChange: setProcessingState, + knownHandles: autocompleteView.knownHandles, + }) track('Create Post', { imageCount: selectedPhotos.length, }) @@ -418,6 +421,7 @@ export const ComposePost = observer(function ComposePost({ </View> </View> ) : undefined} + <View style={[ pal.border, @@ -445,6 +449,13 @@ export const ComposePost = observer(function ComposePost({ {textDecorated} </TextInput> </View> + + {quote ? ( + <View style={s.mt5}> + <QuoteEmbed quote={quote} /> + </View> + ) : undefined} + <SelectedPhoto selectedPhotos={selectedPhotos} onSelectPhotos={onSelectPhotos} @@ -463,7 +474,8 @@ export const ComposePost = observer(function ComposePost({ /> ) : !extLink && selectedPhotos.length === 0 && - suggestedExtLinks.size > 0 ? ( + suggestedExtLinks.size > 0 && + !quote ? ( <View style={s.mb5}> {Array.from(suggestedExtLinks).map(url => ( <TouchableOpacity @@ -478,21 +490,23 @@ export const ComposePost = observer(function ComposePost({ </View> ) : null} <View style={[pal.border, styles.bottomBar]}> - <TouchableOpacity - testID="composerSelectPhotosButton" - onPress={onPressSelectPhotos} - style={[s.pl5]} - hitSlop={HITSLOP}> - <FontAwesomeIcon - icon={['far', 'image']} - style={ - (selectedPhotos.length < 4 - ? pal.link - : pal.textLight) as FontAwesomeIconStyle - } - size={24} - /> - </TouchableOpacity> + {quote ? undefined : ( + <TouchableOpacity + testID="composerSelectPhotosButton" + onPress={onPressSelectPhotos} + style={[s.pl5]} + hitSlop={HITSLOP}> + <FontAwesomeIcon + icon={['far', 'image']} + style={ + (selectedPhotos.length < 4 + ? pal.link + : pal.textLight) as FontAwesomeIconStyle + } + size={24} + /> + </TouchableOpacity> + )} <View style={s.flex1} /> <CharProgress count={text.length} /> </View> diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 2b4457c4b..f939442ba 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -9,6 +9,7 @@ import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' +import * as RepostModal from './Repost' import * as ReportAccountModal from './ReportAccount' import * as DeleteAccountModal from './DeleteAccount' import {usePalette} from 'lib/hooks/usePalette' @@ -61,6 +62,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'delete-account') { snapPoints = DeleteAccountModal.snapPoints element = <DeleteAccountModal.Component /> + } else if (activeModal?.name === 'repost') { + snapPoints = RepostModal.snapPoints + element = <RepostModal.Component {...activeModal} /> } else { element = <View /> } diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 38b526d29..b10b60be8 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -10,6 +10,7 @@ import * as EditProfileModal from './EditProfile' import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' import * as ReportAccountModal from './ReportAccount' +import * as RepostModal from './Repost' import * as CropImageModal from './crop-image/CropImage.web' export const ModalsContainer = observer(function ModalsContainer() { @@ -59,6 +60,8 @@ function Modal({modal}: {modal: ModalIface}) { element = <ReportAccountModal.Component {...modal} /> } else if (modal.name === 'crop-image') { element = <CropImageModal.Component {...modal} /> + } else if (modal.name === 'repost') { + element = <RepostModal.Component {...modal} /> } else { return null } diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx new file mode 100644 index 000000000..6ab15317b --- /dev/null +++ b/src/view/com/modals/Repost.tsx @@ -0,0 +1,90 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import LinearGradient from 'react-native-linear-gradient' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' +import {Text} from '../util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {RepostIcon} from 'lib/icons' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' + +export const snapPoints = [250] + +export function Component({ + onRepost, + onQuote, + isReposted, +}: { + onRepost: () => void + onQuote: () => void + isReposted: boolean +}) { + const store = useStores() + const pal = usePalette('default') + const onPress = async () => { + store.shell.closeModal() + } + + return ( + <View style={[s.flex1, pal.view, styles.container]}> + <View style={s.pb20}> + <TouchableOpacity style={[styles.actionBtn]} onPress={onRepost}> + <RepostIcon strokeWidth={2} size={24} /> + <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> + {!isReposted ? 'Repost' : 'Undo repost'} + </Text> + </TouchableOpacity> + <TouchableOpacity style={[styles.actionBtn]} onPress={onQuote}> + <FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} /> + <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> + Quote Post + </Text> + </TouchableOpacity> + </View> + <TouchableOpacity onPress={onPress}> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[styles.btn]}> + <Text style={[s.white, s.bold, s.f18]}>Cancel</Text> + </LinearGradient> + </TouchableOpacity> + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 30, + }, + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + marginBottom: 12, + }, + description: { + textAlign: 'center', + fontSize: 17, + paddingHorizontal: 22, + marginBottom: 10, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + borderRadius: 32, + padding: 14, + backgroundColor: colors.gray1, + }, + actionBtn: { + flexDirection: 'row', + alignItems: 'center', + }, + actionBtnLabel: { + paddingHorizontal: 14, + paddingVertical: 16, + }, +}) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 65bae0192..a95d91795 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -245,6 +245,13 @@ export const PostThreadItem = observer(function PostThreadItem({ itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} + author={{ + avatar: item.post.author.avatar!, + handle: item.post.author.handle, + displayName: item.post.author.displayName!, + }} + text={item.richText?.text || record.text} + indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} isReposted={!!item.post.viewer.repost} isUpvoted={!!item.post.viewer.upvote} @@ -329,6 +336,13 @@ export const PostThreadItem = observer(function PostThreadItem({ itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} + author={{ + avatar: item.post.author.avatar!, + handle: item.post.author.handle, + displayName: item.post.author.displayName!, + }} + text={item.richText?.text || record.text} + indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} replyCount={item.post.replyCount} repostCount={item.post.repostCount} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index c0ff95416..e8e6781f7 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -197,6 +197,13 @@ export const Post = observer(function Post({ itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} + author={{ + avatar: item.post.author.avatar!, + handle: item.post.author.handle, + displayName: item.post.author.displayName!, + }} + indexedAt={item.post.indexedAt} + text={item.richText?.text || record.text} isAuthor={item.post.author.did === store.me.did} replyCount={item.post.replyCount} repostCount={item.post.repostCount} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index c3e9f61fa..1847827c3 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -226,6 +226,13 @@ export const FeedItem = observer(function ({ itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} + author={{ + avatar: item.post.author.avatar!, + handle: item.post.author.handle, + displayName: item.post.author.displayName!, + }} + text={item.richText?.text || record.text} + indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} replyCount={item.post.replyCount} repostCount={item.post.repostCount} diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index e42c5e63b..cb4dfab26 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -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') diff --git a/src/view/com/util/PostEmbeds/QuoteEmbed.tsx b/src/view/com/util/PostEmbeds/QuoteEmbed.tsx new file mode 100644 index 000000000..76b71a53d --- /dev/null +++ b/src/view/com/util/PostEmbeds/QuoteEmbed.tsx @@ -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, + }, +}) diff --git a/src/view/com/util/PostEmbeds/index.tsx b/src/view/com/util/PostEmbeds/index.tsx index d2186b600..3d3356712 100644 --- a/src/view/com/util/PostEmbeds/index.tsx +++ b/src/view/com/util/PostEmbeds/index.tsx @@ -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) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index a07d91899..0c5d41cab 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -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%', }, diff --git a/src/view/index.ts b/src/view/index.ts index 7cd2c1dfd..c2ad84671 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -8,7 +8,10 @@ import {faAngleUp} from '@fortawesome/free-solid-svg-icons/faAngleUp' import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight' import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp' -import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons' +import { + faArrowRightFromBracket, + faQuoteLeft, +} from '@fortawesome/free-solid-svg-icons' import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate' @@ -100,6 +103,7 @@ export function setup() { faEllipsis, faEnvelope, faExclamation, + faQuoteLeft, farEyeSlash, faGear, faGlobe, diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/mobile/Composer.tsx index 304c17725..5fca118bd 100644 --- a/src/view/shell/mobile/Composer.tsx +++ b/src/view/shell/mobile/Composer.tsx @@ -14,6 +14,7 @@ export const Composer = observer( imagesOpen, onPost, onClose, + quote, }: { active: boolean winHeight: number @@ -21,6 +22,7 @@ export const Composer = observer( imagesOpen?: ComposerOpts['imagesOpen'] onPost?: ComposerOpts['onPost'] onClose: () => void + quote?: ComposerOpts['quote'] }) => { const pal = usePalette('default') const initInterp = useAnimatedValue(0) @@ -62,6 +64,7 @@ export const Composer = observer( imagesOpen={imagesOpen} onPost={onPost} onClose={onClose} + quote={quote} /> </Animated.View> ) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index 80403a6de..89a834ee1 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -550,6 +550,7 @@ export const MobileShell: React.FC = observer(() => { replyTo={store.shell.composerOpts?.replyTo} imagesOpen={store.shell.composerOpts?.imagesOpen} onPost={store.shell.composerOpts?.onPost} + quote={store.shell.composerOpts?.quote} /> </View> ) |