Implement FeedFeedback API (#3498)
* Implement onViewableItemsChanged on List.web.tsx * Introduce onItemSeen to List API * Add FeedFeedback tracker * Add clickthrough interaction tracking * Add engagement interaction tracking * Reduce duplicate sends, introduce a flushAndReset to be triggered on refreshes, and modify the api design a bit * Wire up SDK types and feedContext * Avoid needless function allocations * Fix schema usage * Add show more / show less buttons * Fix minor rendering issue on mobile menu * Wire up sendInteractions() * Fix logic error * Fix: it's item not uri * Update 'seen' to mean 3 seconds on-screen with some significant portion visible * Fix non-reactive debounce * Move methods out * Use a WeakSet for deduping * Reset timeout * 3 -> 2 seconds * Oopsie * Throttle instead * Fix divider * Remove explicit flush calls * Rm unused --------- Co-authored-by: dan <dan.abramov@gmail.com>
This commit is contained in:
parent
e264dfbb87
commit
4fad18b2fa
22 changed files with 516 additions and 64 deletions
|
@ -220,6 +220,7 @@ export const TextLink = memo(function TextLink({
|
|||
)
|
||||
},
|
||||
[
|
||||
onBeforePress,
|
||||
onPress,
|
||||
closeModal,
|
||||
openModal,
|
||||
|
@ -229,7 +230,6 @@ export const TextLink = memo(function TextLink({
|
|||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
openLink,
|
||||
onBeforePress,
|
||||
],
|
||||
)
|
||||
const hrefAttrs = useMemo(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, {memo} from 'react'
|
||||
import {FlatListProps, RefreshControl} from 'react-native'
|
||||
import {FlatListProps, RefreshControl, ViewToken} from 'react-native'
|
||||
import {runOnJS, useSharedValue} from 'react-native-reanimated'
|
||||
|
||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||
|
@ -23,6 +23,7 @@ export type ListProps<ItemT> = Omit<
|
|||
headerOffset?: number
|
||||
refreshing?: boolean
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
containWeb?: boolean
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
||||
|
@ -34,6 +35,7 @@ function ListImpl<ItemT>(
|
|||
onScrolledDownChange,
|
||||
refreshing,
|
||||
onRefresh,
|
||||
onItemSeen,
|
||||
headerOffset,
|
||||
style,
|
||||
...props
|
||||
|
@ -73,6 +75,25 @@ function ListImpl<ItemT>(
|
|||
},
|
||||
})
|
||||
|
||||
const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => {
|
||||
if (!onItemSeen) {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
return [
|
||||
(info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
|
||||
for (const item of info.changed) {
|
||||
if (item.isViewable) {
|
||||
onItemSeen(item.item)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
itemVisiblePercentThreshold: 40,
|
||||
minimumViewTime: 2e3,
|
||||
},
|
||||
]
|
||||
}, [onItemSeen])
|
||||
|
||||
let refreshControl
|
||||
if (refreshing !== undefined || onRefresh !== undefined) {
|
||||
refreshControl = (
|
||||
|
@ -102,6 +123,8 @@ function ListImpl<ItemT>(
|
|||
refreshControl={refreshControl}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={1}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
style={style}
|
||||
ref={ref}
|
||||
/>
|
||||
|
|
|
@ -20,11 +20,17 @@ export type ListProps<ItemT> = Omit<
|
|||
headerOffset?: number
|
||||
refreshing?: boolean
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
desktopFixedHeight: any // TODO: Better types.
|
||||
containWeb?: boolean
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
|
||||
|
||||
const ON_ITEM_SEEN_WAIT_DURATION = 2e3 // post must be "seen" 2 seconds before capturing
|
||||
const ON_ITEM_SEEN_INTERSECTION_OPTS = {
|
||||
rootMargin: '-200px 0px -200px 0px',
|
||||
} // post must be 200px visible to be "seen"
|
||||
|
||||
function ListImpl<ItemT>(
|
||||
{
|
||||
ListHeaderComponent,
|
||||
|
@ -43,6 +49,7 @@ function ListImpl<ItemT>(
|
|||
onRefresh: _unsupportedOnRefresh,
|
||||
onScrolledDownChange,
|
||||
onContentSizeChange,
|
||||
onItemSeen,
|
||||
renderItem,
|
||||
extraData,
|
||||
style,
|
||||
|
@ -319,15 +326,19 @@ function ListImpl<ItemT>(
|
|||
/>
|
||||
)}
|
||||
{header}
|
||||
{(data as Array<ItemT>).map((item, index) => (
|
||||
<Row<ItemT>
|
||||
key={keyExtractor!(item, index)}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
/>
|
||||
))}
|
||||
{(data as Array<ItemT>).map((item, index) => {
|
||||
const key = keyExtractor!(item, index)
|
||||
return (
|
||||
<Row<ItemT>
|
||||
key={key}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
onItemSeen={onItemSeen}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{onEndReached && (
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
|
@ -372,6 +383,7 @@ let Row = function RowImpl<ItemT>({
|
|||
index,
|
||||
renderItem,
|
||||
extraData: _unused,
|
||||
onItemSeen,
|
||||
}: {
|
||||
item: ItemT
|
||||
index: number
|
||||
|
@ -380,12 +392,57 @@ let Row = function RowImpl<ItemT>({
|
|||
| undefined
|
||||
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
|
||||
extraData: any
|
||||
onItemSeen: ((item: any) => void) | undefined
|
||||
}): React.ReactNode {
|
||||
const rowRef = React.useRef(null)
|
||||
const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined)
|
||||
|
||||
const handleIntersection = useNonReactiveCallback(
|
||||
(entries: IntersectionObserverEntry[]) => {
|
||||
batchedUpdates(() => {
|
||||
if (!onItemSeen) {
|
||||
return
|
||||
}
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
if (!intersectionTimeout.current) {
|
||||
intersectionTimeout.current = setTimeout(() => {
|
||||
intersectionTimeout.current = undefined
|
||||
onItemSeen!(item)
|
||||
}, ON_ITEM_SEEN_WAIT_DURATION)
|
||||
}
|
||||
} else {
|
||||
if (intersectionTimeout.current) {
|
||||
clearTimeout(intersectionTimeout.current)
|
||||
intersectionTimeout.current = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!onItemSeen) {
|
||||
return
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
handleIntersection,
|
||||
ON_ITEM_SEEN_INTERSECTION_OPTS,
|
||||
)
|
||||
const row: Element | null = rowRef.current!
|
||||
observer.observe(row)
|
||||
return () => {
|
||||
observer.unobserve(row)
|
||||
}
|
||||
}, [handleIntersection, onItemSeen])
|
||||
|
||||
if (!renderItem) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<View style={styles.row} ref={rowRef}>
|
||||
{renderItem({item, index, separators: null as any})}
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -28,6 +28,7 @@ interface PostMetaOpts {
|
|||
avatarSize?: number
|
||||
displayNameType?: TypographyVariant
|
||||
displayNameStyle?: StyleProp<TextStyle>
|
||||
onOpenAuthor?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
|
@ -43,7 +44,12 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||
: undefined
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const onBeforePress = useCallback(() => {
|
||||
const onOpenAuthor = opts.onOpenAuthor
|
||||
const onBeforePressAuthor = useCallback(() => {
|
||||
precacheProfile(queryClient, opts.author)
|
||||
onOpenAuthor?.()
|
||||
}, [queryClient, opts.author, onOpenAuthor])
|
||||
const onBeforePressPost = useCallback(() => {
|
||||
precacheProfile(queryClient, opts.author)
|
||||
}, [queryClient, opts.author])
|
||||
|
||||
|
@ -77,7 +83,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||
</>
|
||||
}
|
||||
href={profileLink}
|
||||
onBeforePress={onBeforePress}
|
||||
onBeforePress={onBeforePressAuthor}
|
||||
onPointerEnter={onPointerEnter}
|
||||
/>
|
||||
<TextLinkOnWebOnly
|
||||
|
@ -86,7 +92,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||
style={[pal.textLight, {flexShrink: 4}]}
|
||||
text={'\xa0' + sanitizeHandle(handle, '@')}
|
||||
href={profileLink}
|
||||
onBeforePress={onBeforePress}
|
||||
onBeforePress={onBeforePressAuthor}
|
||||
onPointerEnter={onPointerEnter}
|
||||
anchorNoUnderline
|
||||
/>
|
||||
|
@ -112,7 +118,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||
title={niceDate(opts.timestamp)}
|
||||
accessibilityHint=""
|
||||
href={opts.postHref}
|
||||
onBeforePress={onBeforePress}
|
||||
onBeforePress={onBeforePressPost}
|
||||
/>
|
||||
)}
|
||||
</TimeElapsed>
|
||||
|
|
|
@ -50,6 +50,7 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps {
|
|||
|
||||
interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
|
||||
moderation?: ModerationUI
|
||||
onBeforePress?: () => void
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
}
|
||||
|
||||
|
@ -382,14 +383,16 @@ export {EditableUserAvatar}
|
|||
let PreviewableUserAvatar = ({
|
||||
moderation,
|
||||
profile,
|
||||
onBeforePress,
|
||||
...rest
|
||||
}: PreviewableUserAvatarProps): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
onBeforePress?.()
|
||||
precacheProfile(queryClient, profile)
|
||||
}, [profile, queryClient])
|
||||
}, [profile, queryClient, onBeforePress])
|
||||
|
||||
return (
|
||||
<ProfileHoverCard did={profile.did}>
|
||||
|
|
|
@ -18,6 +18,7 @@ import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
|||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
|
||||
|
@ -36,6 +37,10 @@ import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons
|
|||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
||||
import {
|
||||
EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
|
||||
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
|
||||
} from '#/components/icons/Emoji'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
|
@ -53,6 +58,7 @@ let PostDropdownBtn = ({
|
|||
postAuthor,
|
||||
postCid,
|
||||
postUri,
|
||||
postFeedContext,
|
||||
record,
|
||||
richText,
|
||||
style,
|
||||
|
@ -63,6 +69,7 @@ let PostDropdownBtn = ({
|
|||
postAuthor: AppBskyActorDefs.ProfileViewBasic
|
||||
postCid: string
|
||||
postUri: string
|
||||
postFeedContext: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
|
@ -81,6 +88,7 @@ let PostDropdownBtn = ({
|
|||
const postDeleteMutation = usePostDeleteMutation()
|
||||
const hiddenPosts = useHiddenPosts()
|
||||
const {hidePost} = useHiddenPostsApi()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const openLink = useOpenLink()
|
||||
const navigation = useNavigation()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
|
@ -183,6 +191,24 @@ let PostDropdownBtn = ({
|
|||
shareUrl(url)
|
||||
}, [href])
|
||||
|
||||
const onPressShowMore = React.useCallback(() => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestMore',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
Toast.show('Feedback sent!')
|
||||
}, [feedFeedback, postUri, postFeedContext])
|
||||
|
||||
const onPressShowLess = React.useCallback(() => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestLess',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
Toast.show('Feedback sent!')
|
||||
}, [feedFeedback, postUri, postFeedContext])
|
||||
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
|
||||
return (
|
||||
|
@ -262,10 +288,32 @@ let PostDropdownBtn = ({
|
|||
)}
|
||||
</Menu.Group>
|
||||
|
||||
{hasSession && feedFeedback.enabled && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShowMoreBtn"
|
||||
label={_(msg`Show more like this`)}
|
||||
onPress={onPressShowMore}>
|
||||
<Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSmile} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShowLessBtn"
|
||||
label={_(msg`Show less like this`)}
|
||||
onPress={onPressShowLess}>
|
||||
<Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSad} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteThreadBtn"
|
||||
|
@ -308,7 +356,6 @@ let PostDropdownBtn = ({
|
|||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Group>
|
||||
{!isAuthor && (
|
||||
<Menu.Item
|
||||
|
|
|
@ -23,6 +23,7 @@ import {toShareUrl} from '#/lib/strings/url-helpers'
|
|||
import {s} from '#/lib/styles'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
usePostLikeMutationQueue,
|
||||
|
@ -43,6 +44,7 @@ let PostCtrls = ({
|
|||
post,
|
||||
record,
|
||||
richText,
|
||||
feedContext,
|
||||
style,
|
||||
onPressReply,
|
||||
logContext,
|
||||
|
@ -51,6 +53,7 @@ let PostCtrls = ({
|
|||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
feedContext?: string | undefined
|
||||
style?: StyleProp<ViewStyle>
|
||||
onPressReply: () => void
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
|
@ -66,6 +69,7 @@ let PostCtrls = ({
|
|||
)
|
||||
const requireAuth = useRequireAuth()
|
||||
const loggedOutWarningPromptControl = useDialogControl()
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
const playHaptic = useHaptics()
|
||||
|
||||
const shouldShowLoggedOutWarning = React.useMemo(() => {
|
||||
|
@ -85,6 +89,11 @@ let PostCtrls = ({
|
|||
try {
|
||||
if (!post.viewer?.like) {
|
||||
playHaptic()
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionLike',
|
||||
feedContext,
|
||||
})
|
||||
await queueLike()
|
||||
} else {
|
||||
await queueUnlike()
|
||||
|
@ -94,13 +103,26 @@ let PostCtrls = ({
|
|||
throw e
|
||||
}
|
||||
}
|
||||
}, [playHaptic, post.viewer?.like, queueLike, queueUnlike])
|
||||
}, [
|
||||
playHaptic,
|
||||
post.uri,
|
||||
post.viewer?.like,
|
||||
queueLike,
|
||||
queueUnlike,
|
||||
sendInteraction,
|
||||
feedContext,
|
||||
])
|
||||
|
||||
const onRepost = useCallback(async () => {
|
||||
closeModal()
|
||||
try {
|
||||
if (!post.viewer?.repost) {
|
||||
playHaptic()
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionRepost',
|
||||
feedContext,
|
||||
})
|
||||
await queueRepost()
|
||||
} else {
|
||||
await queueUnrepost()
|
||||
|
@ -110,10 +132,24 @@ let PostCtrls = ({
|
|||
throw e
|
||||
}
|
||||
}
|
||||
}, [closeModal, post.viewer?.repost, playHaptic, queueRepost, queueUnrepost])
|
||||
}, [
|
||||
closeModal,
|
||||
post.uri,
|
||||
post.viewer?.repost,
|
||||
playHaptic,
|
||||
queueRepost,
|
||||
queueUnrepost,
|
||||
sendInteraction,
|
||||
feedContext,
|
||||
])
|
||||
|
||||
const onQuote = useCallback(() => {
|
||||
closeModal()
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionQuote',
|
||||
feedContext,
|
||||
})
|
||||
openComposer({
|
||||
quote: {
|
||||
uri: post.uri,
|
||||
|
@ -133,6 +169,8 @@ let PostCtrls = ({
|
|||
post.indexedAt,
|
||||
record.text,
|
||||
playHaptic,
|
||||
sendInteraction,
|
||||
feedContext,
|
||||
])
|
||||
|
||||
const onShare = useCallback(() => {
|
||||
|
@ -140,7 +178,12 @@ let PostCtrls = ({
|
|||
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
}, [post.uri, post.author])
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionShare',
|
||||
feedContext,
|
||||
})
|
||||
}, [post.uri, post.author, sendInteraction, feedContext])
|
||||
|
||||
return (
|
||||
<View style={[styles.ctrls, style]}>
|
||||
|
@ -268,6 +311,7 @@ let PostCtrls = ({
|
|||
postAuthor={post.author}
|
||||
postCid={post.cid}
|
||||
postUri={post.uri}
|
||||
postFeedContext={feedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={styles.btnPad}
|
||||
|
|
|
@ -19,10 +19,12 @@ import {Text} from '../text/Text'
|
|||
|
||||
export const ExternalLinkEmbed = ({
|
||||
link,
|
||||
onOpen,
|
||||
style,
|
||||
hideAlt,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
hideAlt?: boolean
|
||||
}) => {
|
||||
|
@ -44,7 +46,7 @@ export const ExternalLinkEmbed = ({
|
|||
|
||||
return (
|
||||
<View style={[a.flex_col, a.rounded_sm, a.overflow_hidden, a.mt_sm]}>
|
||||
<LinkWrapper link={link} style={style}>
|
||||
<LinkWrapper link={link} onOpen={onOpen} style={style}>
|
||||
{link.thumb && !embedPlayerParams ? (
|
||||
<Image
|
||||
style={{
|
||||
|
@ -97,10 +99,12 @@ export const ExternalLinkEmbed = ({
|
|||
|
||||
function LinkWrapper({
|
||||
link,
|
||||
onOpen,
|
||||
style,
|
||||
children,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
|
@ -125,6 +129,7 @@ function LinkWrapper({
|
|||
style,
|
||||
]}
|
||||
hoverStyle={t.atoms.border_contrast_high}
|
||||
onBeforePress={onOpen}
|
||||
onLongPress={onShareExternal}>
|
||||
{children}
|
||||
</Link>
|
||||
|
|
|
@ -42,9 +42,11 @@ import {PostEmbeds} from '.'
|
|||
|
||||
export function MaybeQuoteEmbed({
|
||||
embed,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
|
@ -57,6 +59,7 @@ export function MaybeQuoteEmbed({
|
|||
<QuoteEmbedModerated
|
||||
viewRecord={embed.record}
|
||||
postRecord={embed.record.value}
|
||||
onOpen={onOpen}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
|
@ -85,10 +88,12 @@ export function MaybeQuoteEmbed({
|
|||
function QuoteEmbedModerated({
|
||||
viewRecord,
|
||||
postRecord,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
viewRecord: AppBskyEmbedRecord.ViewRecord
|
||||
postRecord: AppBskyFeedPost.Record
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
@ -108,16 +113,25 @@ function QuoteEmbedModerated({
|
|||
embeds: viewRecord.embeds,
|
||||
}
|
||||
|
||||
return <QuoteEmbed quote={quote} moderation={moderation} style={style} />
|
||||
return (
|
||||
<QuoteEmbed
|
||||
quote={quote}
|
||||
moderation={moderation}
|
||||
onOpen={onOpen}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function QuoteEmbed({
|
||||
quote,
|
||||
moderation,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
quote: ComposerOptsQuote
|
||||
moderation?: ModerationDecision
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
|
@ -150,7 +164,8 @@ export function QuoteEmbed({
|
|||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
precacheProfile(queryClient, quote.author)
|
||||
}, [queryClient, quote.author])
|
||||
onOpen?.()
|
||||
}, [queryClient, quote.author, onOpen])
|
||||
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentList')}>
|
||||
|
|
|
@ -38,10 +38,12 @@ type Embed =
|
|||
export function PostEmbeds({
|
||||
embed,
|
||||
moderation,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
embed?: Embed
|
||||
moderation?: ModerationDecision
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
|
@ -52,8 +54,12 @@ export function PostEmbeds({
|
|||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<PostEmbeds embed={embed.media} moderation={moderation} />
|
||||
<MaybeQuoteEmbed embed={embed.record} />
|
||||
<PostEmbeds
|
||||
embed={embed.media}
|
||||
moderation={moderation}
|
||||
onOpen={onOpen}
|
||||
/>
|
||||
<MaybeQuoteEmbed embed={embed.record} onOpen={onOpen} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -80,7 +86,7 @@ export function PostEmbeds({
|
|||
|
||||
// quote post
|
||||
// =
|
||||
return <MaybeQuoteEmbed embed={embed} style={style} />
|
||||
return <MaybeQuoteEmbed embed={embed} style={style} onOpen={onOpen} />
|
||||
}
|
||||
|
||||
// image embed
|
||||
|
@ -151,7 +157,7 @@ export function PostEmbeds({
|
|||
const link = embed.external
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<ExternalLinkEmbed link={link} style={style} />
|
||||
<ExternalLinkEmbed link={link} onOpen={onOpen} style={style} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue