Refactor photo embed analytics to post:photoEmbed:* namespace (#10784)
Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1175,18 +1175,37 @@ export type Events = {
|
|||||||
'profile:associated:germ:self-disconnect': {}
|
'profile:associated:germ:self-disconnect': {}
|
||||||
'profile:associated:germ:self-reconnect': {}
|
'profile:associated:germ:self-reconnect': {}
|
||||||
|
|
||||||
// Gallery carousel events
|
// Post photo embed events
|
||||||
'post:gallery:swipe': {
|
'post:photoEmbed:impression': {
|
||||||
|
layout: 'single' | 'grid' | 'carousel'
|
||||||
|
totalImages: number
|
||||||
|
postUri: string
|
||||||
|
postAuthorDid: string
|
||||||
|
feedDescriptor?: string
|
||||||
|
}
|
||||||
|
'post:photoEmbed:open': {
|
||||||
|
layout: 'single' | 'grid' | 'carousel'
|
||||||
|
fromImage: number
|
||||||
|
totalImages: number
|
||||||
|
postUri: string
|
||||||
|
postAuthorDid: string
|
||||||
|
feedDescriptor?: string
|
||||||
|
}
|
||||||
|
'post:photoEmbed:carouselSwipe': {
|
||||||
fromImage: number
|
fromImage: number
|
||||||
toImage: number
|
toImage: number
|
||||||
totalImages: number
|
totalImages: number
|
||||||
|
postUri: string
|
||||||
|
postAuthorDid: string
|
||||||
|
feedDescriptor?: string
|
||||||
}
|
}
|
||||||
'post:gallery:openLightbox': {
|
'post:photoEmbed:lightboxSwipe': {
|
||||||
|
layout: 'single' | 'grid' | 'carousel'
|
||||||
fromImage: number
|
fromImage: number
|
||||||
totalImages: number
|
toImage: number
|
||||||
}
|
|
||||||
'post:gallery:impression': {
|
|
||||||
totalImages: number
|
totalImages: number
|
||||||
postUri: string
|
postUri: string
|
||||||
|
postAuthorDid: string
|
||||||
|
feedDescriptor?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {type Dimensions} from '#/lib/media/types'
|
|||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {setSystemUITheme} from '#/alf/util/systemUI'
|
import {setSystemUITheme} from '#/alf/util/systemUI'
|
||||||
import {type Lightbox} from '#/components/Lightbox/state'
|
import {type Lightbox} from '#/components/Lightbox/state'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
||||||
import {Footer} from '../chrome/Footer'
|
import {Footer} from '../chrome/Footer'
|
||||||
@@ -228,7 +229,8 @@ function ImageView({
|
|||||||
openProgress: SharedValue<number>
|
openProgress: SharedValue<number>
|
||||||
thumbRects: SharedValue<Record<number, MeasuredDimensions | null>>
|
thumbRects: SharedValue<Record<number, MeasuredDimensions | null>>
|
||||||
}) {
|
}) {
|
||||||
const {images, index: initialImageIndex} = lightbox
|
const {images, index: initialImageIndex, metricsContext} = lightbox
|
||||||
|
const ax = useAnalytics()
|
||||||
const isAnimated = useMemo(() => canAnimate(lightbox), [lightbox])
|
const isAnimated = useMemo(() => canAnimate(lightbox), [lightbox])
|
||||||
const [isScaled, setIsScaled] = useState(false)
|
const [isScaled, setIsScaled] = useState(false)
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
@@ -377,7 +379,21 @@ function ImageView({
|
|||||||
scrollEnabled={!isScaled}
|
scrollEnabled={!isScaled}
|
||||||
initialPage={initialImageIndex}
|
initialPage={initialImageIndex}
|
||||||
onPageSelected={e => {
|
onPageSelected={e => {
|
||||||
setImageIndex(e.nativeEvent.position)
|
const next = e.nativeEvent.position
|
||||||
|
setImageIndex(prev => {
|
||||||
|
if (metricsContext && prev !== next) {
|
||||||
|
ax.metric('post:photoEmbed:lightboxSwipe', {
|
||||||
|
layout: metricsContext.layout,
|
||||||
|
fromImage: prev + 1,
|
||||||
|
toImage: next + 1,
|
||||||
|
totalImages: images.length,
|
||||||
|
postUri: metricsContext.postUri,
|
||||||
|
postAuthorDid: metricsContext.postAuthorDid,
|
||||||
|
feedDescriptor: metricsContext.feedDescriptor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
setIsScaled(false)
|
setIsScaled(false)
|
||||||
}}
|
}}
|
||||||
onPageScrollStateChanged={e => {
|
onPageScrollStateChanged={e => {
|
||||||
|
|||||||
@@ -11,10 +11,20 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
|||||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||||
import {type ImageSource} from '#/components/Lightbox/types'
|
import {type ImageSource} from '#/components/Lightbox/types'
|
||||||
|
|
||||||
|
export type LightboxMetricsContext = {
|
||||||
|
layout: 'single' | 'grid' | 'carousel'
|
||||||
|
postUri: string
|
||||||
|
postAuthorDid: string
|
||||||
|
feedDescriptor?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type Lightbox = {
|
export type Lightbox = {
|
||||||
id: string
|
id: string
|
||||||
images: ImageSource[]
|
images: ImageSource[]
|
||||||
index: number
|
index: number
|
||||||
|
// Set for post photo embeds so the lightbox can emit post:photoEmbed:lightboxSwipe.
|
||||||
|
// Left unset for non-post contexts (e.g. profile avatar/banner lightbox).
|
||||||
|
metricsContext?: LightboxMetricsContext
|
||||||
}
|
}
|
||||||
|
|
||||||
const LightboxContext = createContext<{
|
const LightboxContext = createContext<{
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {atoms as a, tokens} from '#/alf'
|
|||||||
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
||||||
import {Gallery} from '#/components/images/Gallery'
|
import {Gallery} from '#/components/images/Gallery'
|
||||||
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
|
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
|
||||||
import {useLightboxControls} from '#/components/Lightbox/state'
|
import {
|
||||||
|
type LightboxMetricsContext,
|
||||||
|
useLightboxControls,
|
||||||
|
} from '#/components/Lightbox/state'
|
||||||
import {type Dimensions} from '#/components/Lightbox/types'
|
import {type Dimensions} from '#/components/Lightbox/types'
|
||||||
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
|
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
@@ -40,6 +43,20 @@ export function ImageEmbed({
|
|||||||
? images.length > MAX_GRID_IMAGES
|
? images.length > MAX_GRID_IMAGES
|
||||||
: ax.features.enabled(ax.features.PostGalleryEmbedEnable)
|
: ax.features.enabled(ax.features.PostGalleryEmbedEnable)
|
||||||
|
|
||||||
|
const layout: 'single' | 'grid' | 'carousel' =
|
||||||
|
images.length === 1 ? 'single' : useExpandedLayout ? 'carousel' : 'grid'
|
||||||
|
|
||||||
|
const postContext = rest.post
|
||||||
|
? {
|
||||||
|
postUri: rest.post.uri,
|
||||||
|
postAuthorDid: rest.post.author.did,
|
||||||
|
feedDescriptor: rest.feedDescriptor,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
const metricsContext: LightboxMetricsContext | undefined = postContext
|
||||||
|
? {layout, ...postContext}
|
||||||
|
: undefined
|
||||||
|
|
||||||
// Captured from AutoSizedImage so the peek-commit handler can reuse the same
|
// Captured from AutoSizedImage so the peek-commit handler can reuse the same
|
||||||
// ref + dims that a tap would — keeps the lightbox's return animation intact.
|
// ref + dims that a tap would — keeps the lightbox's return animation intact.
|
||||||
const singleContainerRef = useRef<AnimatedRef<any> | null>(null)
|
const singleContainerRef = useRef<AnimatedRef<any> | null>(null)
|
||||||
@@ -57,6 +74,14 @@ export function ImageEmbed({
|
|||||||
refs: AnimatedRef<any>[],
|
refs: AnimatedRef<any>[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => {
|
) => {
|
||||||
|
if (postContext) {
|
||||||
|
ax.metric('post:photoEmbed:open', {
|
||||||
|
layout,
|
||||||
|
fromImage: index + 1,
|
||||||
|
totalImages: images.length,
|
||||||
|
...postContext,
|
||||||
|
})
|
||||||
|
}
|
||||||
openLightbox({
|
openLightbox({
|
||||||
images: items.map((item, i) => ({
|
images: items.map((item, i) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -67,6 +92,7 @@ export function ImageEmbed({
|
|||||||
type: 'image',
|
type: 'image',
|
||||||
})),
|
})),
|
||||||
index,
|
index,
|
||||||
|
metricsContext,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const onPressIn = (_: number) => {
|
const onPressIn = (_: number) => {
|
||||||
@@ -132,6 +158,7 @@ export function ImageEmbed({
|
|||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
viewContext={rest.viewContext}
|
viewContext={rest.viewContext}
|
||||||
isWithinQuote={rest.isWithinQuote}
|
isWithinQuote={rest.isWithinQuote}
|
||||||
|
metricsPostContext={postContext}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -345,6 +345,9 @@ export function QuoteEmbed({
|
|||||||
allowNestedQuotes={
|
allowNestedQuotes={
|
||||||
parentIsWithinQuote ? false : parentAllowNestedQuotes
|
parentIsWithinQuote ? false : parentAllowNestedQuotes
|
||||||
}
|
}
|
||||||
|
// The photo embed belongs to the quoted post, so attribute its
|
||||||
|
// analytics to the quoted post rather than the parent.
|
||||||
|
post={quote}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ export type CommonProps = {
|
|||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
isWithinQuote?: boolean
|
isWithinQuote?: boolean
|
||||||
allowNestedQuotes?: boolean
|
allowNestedQuotes?: boolean
|
||||||
|
/**
|
||||||
|
* The post that contains this embed. Used for analytics on photo embed
|
||||||
|
* events (post:photoEmbed:*). When the embed has no owning post (e.g.
|
||||||
|
* composer previews), leave this undefined and no events will be emitted.
|
||||||
|
*/
|
||||||
|
post?: AppBskyFeedDefs.PostView
|
||||||
|
feedDescriptor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EmbedProps = CommonProps & {
|
export type EmbedProps = CommonProps & {
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ interface GalleryProps {
|
|||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
isWithinQuote?: boolean
|
isWithinQuote?: boolean
|
||||||
|
// Post context for the in-feed carousel swipe metric. Omit for non-post
|
||||||
|
// contexts (no event will be emitted).
|
||||||
|
metricsPostContext?: {
|
||||||
|
postUri: string
|
||||||
|
postAuthorDid: string
|
||||||
|
feedDescriptor?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Context = createContext<{
|
const Context = createContext<{
|
||||||
@@ -99,6 +106,7 @@ export function Gallery({
|
|||||||
onPressIn,
|
onPressIn,
|
||||||
viewContext,
|
viewContext,
|
||||||
isWithinQuote,
|
isWithinQuote,
|
||||||
|
metricsPostContext,
|
||||||
}: GalleryProps) {
|
}: GalleryProps) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
@@ -169,13 +177,17 @@ export function Gallery({
|
|||||||
const emitSwipeMetric = useMemo(
|
const emitSwipeMetric = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce((fromIndex: number, toIndex: number) => {
|
debounce((fromIndex: number, toIndex: number) => {
|
||||||
ax.metric('post:gallery:swipe', {
|
if (!metricsPostContext) return
|
||||||
|
ax.metric('post:photoEmbed:carouselSwipe', {
|
||||||
fromImage: fromIndex + 1, // convert to 1-based index for easier analysis
|
fromImage: fromIndex + 1, // convert to 1-based index for easier analysis
|
||||||
toImage: toIndex + 1, // convert to 1-based index for easier analysis
|
toImage: toIndex + 1, // convert to 1-based index for easier analysis
|
||||||
totalImages: images.length,
|
totalImages: images.length,
|
||||||
|
postUri: metricsPostContext.postUri,
|
||||||
|
postAuthorDid: metricsPostContext.postAuthorDid,
|
||||||
|
feedDescriptor: metricsPostContext.feedDescriptor,
|
||||||
})
|
})
|
||||||
}, 200),
|
}, 200),
|
||||||
[ax, images.length],
|
[ax, images.length, metricsPostContext],
|
||||||
)
|
)
|
||||||
|
|
||||||
const setCurrentIndex = (index: number) => {
|
const setCurrentIndex = (index: number) => {
|
||||||
@@ -277,10 +289,6 @@ export function Gallery({
|
|||||||
renderItem={({item, index}) => {
|
renderItem={({item, index}) => {
|
||||||
const openLightboxAtIndex = onPress
|
const openLightboxAtIndex = onPress
|
||||||
? () => {
|
? () => {
|
||||||
ax.metric('post:gallery:openLightbox', {
|
|
||||||
fromImage: index + 1, // convert to 1-based index for easier analysis
|
|
||||||
totalImages: images.length,
|
|
||||||
})
|
|
||||||
const refs: AnimatedRef<any>[] = []
|
const refs: AnimatedRef<any>[] = []
|
||||||
const dims: (Dimensions | null)[] = []
|
const dims: (Dimensions | null)[] = []
|
||||||
for (let i = 0; i < images.length; i++) {
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
|||||||
@@ -414,6 +414,8 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
||||||
onOpen={onOpenEmbed}
|
onOpen={onOpenEmbed}
|
||||||
|
post={post}
|
||||||
|
feedDescriptor={feedFeedback.feedDescriptor}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
|
|||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
post={post}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
|
|||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
post={post}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ function PostInner({
|
|||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
post={post}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
type AppBskyActorDefs,
|
type AppBskyActorDefs,
|
||||||
AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
|
AppBskyEmbedGallery,
|
||||||
|
AppBskyEmbedImages,
|
||||||
AppBskyEmbedVideo,
|
AppBskyEmbedVideo,
|
||||||
type AppBskyFeedDefs,
|
type AppBskyFeedDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
@@ -907,7 +909,9 @@ let PostFeed = ({
|
|||||||
|
|
||||||
const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
||||||
const seenPostUrisRef = useRef<Set<string>>(new Set())
|
const seenPostUrisRef = useRef<Set<string>>(new Set())
|
||||||
const seenStandardSiteUrisRef = useRef<Set<string>>(new Set())
|
// Tracks every post we've seen so we can fire per-post events exactly once,
|
||||||
|
// regardless of the post's position within its slice.
|
||||||
|
const seenPerPostUrisRef = useRef<Set<string>>(new Set())
|
||||||
|
|
||||||
// Helper to calculate position in feed (count only root posts, not interstitials or thread replies)
|
// Helper to calculate position in feed (count only root posts, not interstitials or thread replies)
|
||||||
const getPostPosition = useNonReactiveCallback(
|
const getPostPosition = useNonReactiveCallback(
|
||||||
@@ -940,6 +944,48 @@ let PostFeed = ({
|
|||||||
(item: FeedRow) => {
|
(item: FeedRow) => {
|
||||||
feedFeedback.onItemSeen(item)
|
feedFeedback.onItemSeen(item)
|
||||||
|
|
||||||
|
// Events that should fire exactly once for every new post, regardless of
|
||||||
|
// its position within a slice or video grid row.
|
||||||
|
const onPostSeen = (post: AppBskyFeedDefs.PostView) => {
|
||||||
|
if (seenPerPostUrisRef.current.has(post.uri)) return
|
||||||
|
seenPerPostUrisRef.current.add(post.uri)
|
||||||
|
|
||||||
|
// Standard site embed view tracking
|
||||||
|
if (
|
||||||
|
AppBskyEmbedExternal.isView(post.embed) &&
|
||||||
|
isStandardSiteEmbed(post.embed.external)
|
||||||
|
) {
|
||||||
|
ax.metric('embed:standardSite:view', {url: post.embed.external.uri})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Photo embed impression tracking
|
||||||
|
if (
|
||||||
|
AppBskyEmbedImages.isView(post.embed) ||
|
||||||
|
AppBskyEmbedGallery.isView(post.embed)
|
||||||
|
) {
|
||||||
|
const totalImages = AppBskyEmbedGallery.isView(post.embed)
|
||||||
|
? post.embed.items.filter(AppBskyEmbedGallery.isViewImage).length
|
||||||
|
: post.embed.images.length
|
||||||
|
const useExpandedLayout = AppBskyEmbedGallery.isView(post.embed)
|
||||||
|
? totalImages > 4
|
||||||
|
: ax.features.enabled(ax.features.PostGalleryEmbedEnable)
|
||||||
|
const layout =
|
||||||
|
totalImages === 1
|
||||||
|
? 'single'
|
||||||
|
: useExpandedLayout
|
||||||
|
? 'carousel'
|
||||||
|
: 'grid'
|
||||||
|
|
||||||
|
ax.metric('post:photoEmbed:impression', {
|
||||||
|
layout,
|
||||||
|
totalImages,
|
||||||
|
postUri: post.uri,
|
||||||
|
postAuthorDid: post.author.did,
|
||||||
|
feedDescriptor: feedFeedback.feedDescriptor || feed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Track post:view events
|
// Track post:view events
|
||||||
if (item.type === 'sliceItem') {
|
if (item.type === 'sliceItem') {
|
||||||
const slice = item.slice
|
const slice = item.slice
|
||||||
@@ -947,6 +993,8 @@ let PostFeed = ({
|
|||||||
const postItem = slice.items[indexInSlice]
|
const postItem = slice.items[indexInSlice]
|
||||||
const post = postItem.post
|
const post = postItem.post
|
||||||
|
|
||||||
|
onPostSeen(post)
|
||||||
|
|
||||||
// Only track the root post of each slice (index 0) to avoid double-counting thread items
|
// Only track the root post of each slice (index 0) to avoid double-counting thread items
|
||||||
if (indexInSlice === 0 && !seenPostUrisRef.current.has(post.uri)) {
|
if (indexInSlice === 0 && !seenPostUrisRef.current.has(post.uri)) {
|
||||||
seenPostUrisRef.current.add(post.uri)
|
seenPostUrisRef.current.add(post.uri)
|
||||||
@@ -977,16 +1025,6 @@ let PostFeed = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard site embed view tracking
|
|
||||||
if (
|
|
||||||
AppBskyEmbedExternal.isView(post.embed) &&
|
|
||||||
isStandardSiteEmbed(post.embed.external) &&
|
|
||||||
!seenStandardSiteUrisRef.current.has(post.embed.external.uri)
|
|
||||||
) {
|
|
||||||
seenStandardSiteUrisRef.current.add(post.embed.external.uri)
|
|
||||||
ax.metric('embed:standardSite:view', {url: post.embed.external.uri})
|
|
||||||
}
|
|
||||||
} else if (item.type === 'videoGridRow') {
|
} else if (item.type === 'videoGridRow') {
|
||||||
// Track each video in the grid row
|
// Track each video in the grid row
|
||||||
for (let i = 0; i < item.items.length; i++) {
|
for (let i = 0; i < item.items.length; i++) {
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ let FeedItemInner = ({
|
|||||||
onOpenEmbed={onOpenEmbed}
|
onOpenEmbed={onOpenEmbed}
|
||||||
post={post}
|
post={post}
|
||||||
additionalPostAlerts={additionalPostAlerts}
|
additionalPostAlerts={additionalPostAlerts}
|
||||||
|
feedDescriptor={feedDescriptor}
|
||||||
/>
|
/>
|
||||||
<PostControls
|
<PostControls
|
||||||
post={post}
|
post={post}
|
||||||
@@ -460,6 +461,7 @@ let PostContent = ({
|
|||||||
postAuthor,
|
postAuthor,
|
||||||
onOpenEmbed,
|
onOpenEmbed,
|
||||||
additionalPostAlerts,
|
additionalPostAlerts,
|
||||||
|
feedDescriptor,
|
||||||
}: {
|
}: {
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
richText: RichTextAPI
|
richText: RichTextAPI
|
||||||
@@ -468,6 +470,7 @@ let PostContent = ({
|
|||||||
onOpenEmbed: () => void
|
onOpenEmbed: () => void
|
||||||
post: AppBskyFeedDefs.PostView
|
post: AppBskyFeedDefs.PostView
|
||||||
additionalPostAlerts?: AppModerationCause[]
|
additionalPostAlerts?: AppModerationCause[]
|
||||||
|
feedDescriptor?: string
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const [limitLines, setLimitLines] = useState(
|
const [limitLines, setLimitLines] = useState(
|
||||||
() => countLines(richText.text) >= MAX_POST_LINES,
|
() => countLines(richText.text) >= MAX_POST_LINES,
|
||||||
@@ -528,6 +531,8 @@ let PostContent = ({
|
|||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
onOpen={onOpenEmbed}
|
onOpen={onOpenEmbed}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
post={post}
|
||||||
|
feedDescriptor={feedDescriptor}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user