34582edf3e
* tweaks to constants (#7478) * add did * use correct did * typo * tweak * Prevent Drawer gesture conflicting with Suggestions scroll (#7468) * Extract BlockDrawerGeesture * Block drawer when scrolling interstitials (cherry picked from commit9e3f2f4374) * yolo interstitial * yolo mode * right swipe * fix nav gesture * vibe controls * collapsible post text * rm blurview, cover for tall videos * smarter video source handling * use thumbnails, improve perf significantly * better android loading * improve aspect ratio * optimize source changes * rm spinner on ios * whoops, remove debug `false` * FIX WRONG VIDEOS SHOWING UP * don't spring on way down * release video players when leaving screen * remove jank animation * Add grid * improve contract, fix double tap * Filter out posts without videos * Only do grid on native * Pipe through feedSourceUri and link to feed * Handle passed through params * Partial revert, just filter posts to start at index * Clean up cards, remove entry interstitial * Tweak handle * Change constant name * Rename some things * Make types legit * Clean up more naming * Add placeholder for grid view * Handle web, set up new organization * Begin work on Header * Replace types * Squashed commit of the following: commit3d1be4c0f1Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 01:08:05 2025 +0000 extend animation commitc9f199413bAuthor: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 01:01:24 2025 +0000 fix gap commit22e520795fAuthor: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:50:16 2025 +0000 thinner bar, format time commitc32427f214Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:47:57 2025 +0000 fix 2 in 3 screens commitcbf84c08d6Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:45:46 2025 +0000 rm unneeded var commit7e0e100177Author: Samuel Newman <mozzius@protonmail.com> Date: Fri Jan 17 00:41:18 2025 +0000 scrubberrrrr * use white with opacity rather than gray * Simultaneous gesture * cleanup attempt * fix jank * link to profile on press * fix jitter fr this time * mostly fix android flicker * Maybe fix row generation * Add content hider to video card * emoji in post text * reduce update rate * fix type error * Fix grid layout trailing single item * Add Discover interstitial, settings, includes pin for now * Explore interstitial, handle dimissal, pinning, compact card * Only use grid placeholder on native * Update events * Add feature gate * android nav bar fixes + lower update speed * fix interval + decel rate on interstitials * attempt to fix broken scrub on android (not working) * follow button * Part out the interstitials for perf, add view more * Remove prod web route * Wrap interstitials with BlockDrawerGesture * Bring video cropping in line with images (#7462) * Mimic image cropping for videos on web * Same on native * Rename variables for clarity * Fix Android scrubbing * Add FeedFeedbackProvider * Remove swipe gesture * fix light status bar behaviour * bump * feedback * Copy pasta to new location * Copy pasta part deux * Filter only videos * Make whole text clickable to expand (cherry picked from commit 4cf31120779f4e06eb4c296b3d4b53814d432b07) * move scrubber to own file * end card * add icon to end card * add min view time to viewability config * play haptic on like * tweak feedback * tweak feedback again * Moderation (cherry picked from commit 6b6b471cfb363031284b3e7a1f6e0ade3ac4ae47) * remove bad check * fix feedback for new video grid * change prop name to items as well * Simplify logic * Fix mod footer * Give scrubber more space on android * Add subtle track behind scrubber, adjust opacity * wire in feed context again... * Add better a11y desc to card * Fix key issue * Update a11y copy * Fix scrubber height * improve scrubber animation * Make follow button more obvious * Make header back button more clear * Disable interactions with actual video el * keep content away from the bottom safe area * fix blur * fix moderation issue * improve contrast on mod screen * Make moderation static per item * Memoize rows * Optimizations * Take video moderation into account * Only blur titles for list blur * Change copy * Bump blur radius * animate text in both directions * Rm unused field * Filter by root early * Refactor for clarity * add compose prompt to scrubber * rm log * tweak gradient * Bump SDK, use contentMode to power video feed * Ensure ProfileFeed view also supports video feed * improve scrubber on android * rm border from footer * Update prod video feed did * Separate caches * Add lil hover to View More * Fix undefined logic, remove header for interstitial * Ungate * Fix stuckness * remove extra useless map * Fix effect cleanup * Send seen without cleanup * Simplify react stuff * Earlier early return to avoid loading flash * remove scrubber placeholder * Remove opacity hack * Render useEvent conditionally * Fix Android flash --------- Co-authored-by: dan <dan.abramov@gmail.com> Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Eric Bailey <git@esb.lol>
260 lines
7.1 KiB
TypeScript
260 lines
7.1 KiB
TypeScript
import React from 'react'
|
|
import {AppState, AppStateStatus} from 'react-native'
|
|
import {AppBskyFeedDefs} from '@atproto/api'
|
|
import throttle from 'lodash.throttle'
|
|
|
|
import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants'
|
|
import {logEvent} from '#/lib/statsig/statsig'
|
|
import {logger} from '#/logger'
|
|
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
|
|
import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
|
|
import {useAgent} from './session'
|
|
|
|
type StateContext = {
|
|
enabled: boolean
|
|
onItemSeen: (item: any) => void
|
|
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
|
|
}
|
|
|
|
const stateContext = React.createContext<StateContext>({
|
|
enabled: false,
|
|
onItemSeen: (_item: any) => {},
|
|
sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
|
|
})
|
|
|
|
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
|
const agent = useAgent()
|
|
const enabled = isDiscoverFeed(feed) && hasSession
|
|
|
|
const queue = React.useRef<Set<string>>(new Set())
|
|
const history = React.useRef<
|
|
// Use a WeakSet so that we don't need to clear it.
|
|
// This assumes that referential identity of slice items maps 1:1 to feed (re)fetches.
|
|
WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction>
|
|
>(new WeakSet())
|
|
|
|
const aggregatedStats = React.useRef<AggregatedStats | null>(null)
|
|
const throttledFlushAggregatedStats = React.useMemo(
|
|
() =>
|
|
throttle(() => flushToStatsig(aggregatedStats.current), 45e3, {
|
|
leading: true, // The outer call is already throttled somewhat.
|
|
trailing: true,
|
|
}),
|
|
[],
|
|
)
|
|
|
|
const sendToFeedNoDelay = React.useCallback(() => {
|
|
const interactions = Array.from(queue.current).map(toInteraction)
|
|
queue.current.clear()
|
|
|
|
let proxyDid = 'did:web:discover.bsky.app'
|
|
if (STAGING_FEEDS.includes(feed)) {
|
|
proxyDid = 'did:web:algo.pop2.bsky.app'
|
|
}
|
|
|
|
// Send to the feed
|
|
agent.app.bsky.feed
|
|
.sendInteractions(
|
|
{interactions},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: {
|
|
// TODO when we start sending to other feeds, we need to grab their DID -prf
|
|
'atproto-proxy': `${proxyDid}#bsky_fg`,
|
|
},
|
|
},
|
|
)
|
|
.catch((e: any) => {
|
|
logger.warn('Failed to send feed interactions', {error: e})
|
|
})
|
|
|
|
// Send to Statsig
|
|
if (aggregatedStats.current === null) {
|
|
aggregatedStats.current = createAggregatedStats()
|
|
}
|
|
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
|
|
throttledFlushAggregatedStats()
|
|
}, [agent, throttledFlushAggregatedStats, feed])
|
|
|
|
const sendToFeed = React.useMemo(
|
|
() =>
|
|
throttle(sendToFeedNoDelay, 10e3, {
|
|
leading: false,
|
|
trailing: true,
|
|
}),
|
|
[sendToFeedNoDelay],
|
|
)
|
|
|
|
React.useEffect(() => {
|
|
if (!enabled) {
|
|
return
|
|
}
|
|
const sub = AppState.addEventListener('change', (state: AppStateStatus) => {
|
|
if (state === 'background') {
|
|
sendToFeed.flush()
|
|
}
|
|
})
|
|
return () => sub.remove()
|
|
}, [enabled, sendToFeed])
|
|
|
|
const onItemSeen = React.useCallback(
|
|
(feedItem: any) => {
|
|
if (!enabled) {
|
|
return
|
|
}
|
|
const items = getItemsForFeedback(feedItem)
|
|
for (const {item: postItem, feedContext} of items) {
|
|
if (!history.current.has(postItem)) {
|
|
history.current.add(postItem)
|
|
queue.current.add(
|
|
toString({
|
|
item: postItem.uri,
|
|
event: 'app.bsky.feed.defs#interactionSeen',
|
|
feedContext,
|
|
}),
|
|
)
|
|
sendToFeed()
|
|
}
|
|
}
|
|
},
|
|
[enabled, sendToFeed],
|
|
)
|
|
|
|
const sendInteraction = React.useCallback(
|
|
(interaction: AppBskyFeedDefs.Interaction) => {
|
|
if (!enabled) {
|
|
return
|
|
}
|
|
if (!history.current.has(interaction)) {
|
|
history.current.add(interaction)
|
|
queue.current.add(toString(interaction))
|
|
sendToFeed()
|
|
}
|
|
},
|
|
[enabled, sendToFeed],
|
|
)
|
|
|
|
return React.useMemo(() => {
|
|
return {
|
|
enabled,
|
|
// pass this method to the <List> onItemSeen
|
|
onItemSeen,
|
|
// call on various events
|
|
// queues the event to be sent with the throttled sendToFeed call
|
|
sendInteraction,
|
|
}
|
|
}, [enabled, onItemSeen, sendInteraction])
|
|
}
|
|
|
|
export const FeedFeedbackProvider = stateContext.Provider
|
|
|
|
export function useFeedFeedbackContext() {
|
|
return React.useContext(stateContext)
|
|
}
|
|
|
|
// TODO
|
|
// We will introduce a permissions framework for 3p feeds to
|
|
// take advantage of the feed feedback API. Until that's in
|
|
// place, we're hardcoding it to the discover feed.
|
|
// -prf
|
|
function isDiscoverFeed(feed: FeedDescriptor) {
|
|
return FEEDBACK_FEEDS.includes(feed)
|
|
}
|
|
|
|
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
|
return `${interaction.item}|${interaction.event}|${
|
|
interaction.feedContext || ''
|
|
}`
|
|
}
|
|
|
|
function toInteraction(str: string): AppBskyFeedDefs.Interaction {
|
|
const [item, event, feedContext] = str.split('|')
|
|
return {item, event, feedContext}
|
|
}
|
|
|
|
type AggregatedStats = {
|
|
clickthroughCount: number
|
|
engagedCount: number
|
|
seenCount: number
|
|
}
|
|
|
|
function createAggregatedStats(): AggregatedStats {
|
|
return {
|
|
clickthroughCount: 0,
|
|
engagedCount: 0,
|
|
seenCount: 0,
|
|
}
|
|
}
|
|
|
|
function sendOrAggregateInteractionsForStats(
|
|
stats: AggregatedStats,
|
|
interactions: AppBskyFeedDefs.Interaction[],
|
|
) {
|
|
for (let interaction of interactions) {
|
|
switch (interaction.event) {
|
|
// Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them.
|
|
// This lets us send the feed context together with them.
|
|
case 'app.bsky.feed.defs#requestLess': {
|
|
logEvent('discover:showLess', {
|
|
feedContext: interaction.feedContext ?? '',
|
|
})
|
|
break
|
|
}
|
|
case 'app.bsky.feed.defs#requestMore': {
|
|
logEvent('discover:showMore', {
|
|
feedContext: interaction.feedContext ?? '',
|
|
})
|
|
break
|
|
}
|
|
|
|
// The rest of the events are aggregated and sent later in batches.
|
|
case 'app.bsky.feed.defs#clickthroughAuthor':
|
|
case 'app.bsky.feed.defs#clickthroughEmbed':
|
|
case 'app.bsky.feed.defs#clickthroughItem':
|
|
case 'app.bsky.feed.defs#clickthroughReposter': {
|
|
stats.clickthroughCount++
|
|
break
|
|
}
|
|
case 'app.bsky.feed.defs#interactionLike':
|
|
case 'app.bsky.feed.defs#interactionQuote':
|
|
case 'app.bsky.feed.defs#interactionReply':
|
|
case 'app.bsky.feed.defs#interactionRepost':
|
|
case 'app.bsky.feed.defs#interactionShare': {
|
|
stats.engagedCount++
|
|
break
|
|
}
|
|
case 'app.bsky.feed.defs#interactionSeen': {
|
|
stats.seenCount++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function flushToStatsig(stats: AggregatedStats | null) {
|
|
if (stats === null) {
|
|
return
|
|
}
|
|
|
|
if (stats.clickthroughCount > 0) {
|
|
logEvent('discover:clickthrough', {
|
|
count: stats.clickthroughCount,
|
|
})
|
|
stats.clickthroughCount = 0
|
|
}
|
|
|
|
if (stats.engagedCount > 0) {
|
|
logEvent('discover:engaged', {
|
|
count: stats.engagedCount,
|
|
})
|
|
stats.engagedCount = 0
|
|
}
|
|
|
|
if (stats.seenCount > 0) {
|
|
logEvent('discover:seen', {
|
|
count: stats.seenCount,
|
|
})
|
|
stats.seenCount = 0
|
|
}
|
|
}
|