From ee19718c4ff05e2f44154896c2e0bdf3054f1461 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 11 Apr 2024 20:25:26 -0700 Subject: [PATCH 01/25] Implement onViewableItemsChanged on List.web.tsx --- src/view/com/posts/Feed.tsx | 11 +++ src/view/com/util/List.web.tsx | 134 +++++++++++++++++++++++++++++---- 2 files changed, 129 insertions(+), 16 deletions(-) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index fb67d35c5c..35d3001484 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -45,6 +45,11 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} // const REFRESH_AFTER = STALE.HOURS.ONE const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY +const VIEWABILITY_CFG = { + itemVisiblePercentThreshold: 100, + minimumViewTime: 2000, +} + let Feed = ({ feed, feedParams, @@ -265,6 +270,10 @@ let Feed = ({ fetchNextPage() }, [fetchNextPage]) + const onViewableItemsChanged = React.useCallback(info => { + console.log(info) + }, []) + // rendering // = @@ -353,6 +362,8 @@ let Feed = ({ } initialNumToRender={initialNumToRender} windowSize={11} + viewabilityConfig={VIEWABILITY_CFG} + onViewableItemsChanged={onViewableItemsChanged} /> ) diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 936bac198d..959dfb61f2 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -1,11 +1,18 @@ -import React, {isValidElement, memo, useRef, startTransition} from 'react' -import {FlatListProps, StyleSheet, View, ViewProps} from 'react-native' -import {addStyle} from 'lib/styles' +import React, {isValidElement, memo, startTransition, useRef} from 'react' +import { + FlatListProps, + StyleSheet, + View, + ViewProps, + ViewToken, +} from 'react-native' + +import {batchedUpdates} from '#/lib/batchedUpdates' +import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {useScrollHandlers} from '#/lib/ScrollContext' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {useScrollHandlers} from '#/lib/ScrollContext' -import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {batchedUpdates} from '#/lib/batchedUpdates' +import {addStyle} from 'lib/styles' export type ListMethods = any // TODO: Better types. export type ListProps = Omit< @@ -37,6 +44,7 @@ function ListImpl( onRefresh: _unsupportedOnRefresh, onScrolledDownChange, onContentSizeChange, + onViewableItemsChanged, renderItem, extraData, style, @@ -182,15 +190,20 @@ function ListImpl( style={[styles.aboveTheFoldDetector, {height: headerOffset}]} /> {header} - {(data as Array).map((item, index) => ( - - key={keyExtractor!(item, index)} - item={item} - index={index} - renderItem={renderItem} - extraData={extraData} - /> - ))} + {(data as Array).map((item, index) => { + const key = keyExtractor!(item, index) + return ( + + key={key} + itemKey={key} + item={item} + index={index} + renderItem={renderItem} + extraData={extraData} + onViewableItemsChanged={onViewableItemsChanged} + /> + ) + })} {onEndReached && ( ({ + itemKey, item, index, renderItem, extraData: _unused, + onViewableItemsChanged, }: { + itemKey: string item: ItemT index: number renderItem: @@ -242,18 +258,104 @@ let Row = function RowImpl({ | undefined | ((data: {index: number; item: any; separators: any}) => React.ReactNode) extraData: any + onViewableItemsChanged: FlatListProps['onViewableItemsChanged'] }): React.ReactNode { if (!renderItem) { return null } return ( - {renderItem({item, index, separators: null as any})} + + {renderItem({item, index, separators: null as any})} + ) } Row = React.memo(Row) +let RowVisibility = function ({ + itemKey, + item, + index, + onViewableItemsChanged, + children, +}: { + itemKey: string + item: ItemT + index: number + onViewableItemsChanged: FlatListProps['onViewableItemsChanged'] + children: React.ReactNode +}): React.ReactNode { + if (!onViewableItemsChanged) { + return children + } + return ( + + {children} + + ) +} +RowVisibility = React.memo(RowVisibility) + +let RowVisibilityInner = function ({ + itemKey, + item, + index, + onViewableItemsChanged, + children, +}: { + itemKey: string + item: ItemT + index: number + onViewableItemsChanged: FlatListProps['onViewableItemsChanged'] + children: React.ReactNode +}): React.ReactNode { + const tailRef = React.useRef(null) + const isIntersecting = React.useRef(false) + + const handleIntersection = useNonReactiveCallback( + (entries: IntersectionObserverEntry[]) => { + batchedUpdates(() => { + entries.forEach(entry => { + if (entry.isIntersecting !== isIntersecting.current) { + isIntersecting.current = entry.isIntersecting + const itemInfo: ViewToken = { + index, + item, + key: itemKey, + isViewable: entry.isIntersecting, + } + onViewableItemsChanged!({ + viewableItems: [itemInfo], + changed: [itemInfo], + }) + } + }) + }) + }, + ) + + React.useEffect(() => { + const observer = new IntersectionObserver(handleIntersection) + const tail: Element | null = tailRef.current! + observer.observe(tail) + return () => { + observer.unobserve(tail) + } + }, [handleIntersection]) + + return {children} +} +RowVisibilityInner = React.memo(RowVisibilityInner) + let Visibility = ({ topMargin = '0px', onVisibleChange, From 18c824727bd583d66af58603e717b5ab2cfc910b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 16 Apr 2024 20:05:00 -0700 Subject: [PATCH 02/25] Introduce onItemSeen to List API --- src/view/com/posts/Feed.tsx | 24 +++++--- src/view/com/util/List.tsx | 32 ++++++++-- src/view/com/util/List.web.tsx | 109 ++++++++------------------------- 3 files changed, 67 insertions(+), 98 deletions(-) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 35d3001484..81f8183ad2 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -45,11 +45,6 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} // const REFRESH_AFTER = STALE.HOURS.ONE const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY -const VIEWABILITY_CFG = { - itemVisiblePercentThreshold: 100, - minimumViewTime: 2000, -} - let Feed = ({ feed, feedParams, @@ -270,9 +265,19 @@ let Feed = ({ fetchNextPage() }, [fetchNextPage]) - const onViewableItemsChanged = React.useCallback(info => { - console.log(info) - }, []) + const set = React.useRef(new Set()) + const onItemSeen = React.useCallback( + item => { + if (!item.items?.[0]) { + return + } + if (!set.current.has(item.items[0].post.record.text)) { + console.log(item.items[0].post.record.text, item) + set.current.add(item.items[0].post.record.text) + } + }, + [set], + ) // rendering // = @@ -362,8 +367,7 @@ let Feed = ({ } initialNumToRender={initialNumToRender} windowSize={11} - viewabilityConfig={VIEWABILITY_CFG} - onViewableItemsChanged={onViewableItemsChanged} + onItemSeen={onItemSeen} /> ) diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index d30a9d805b..59d1be9cf7 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -1,11 +1,12 @@ import React, {memo} from 'react' -import {FlatListProps, RefreshControl} from 'react-native' -import {FlatList_INTERNAL} from './Views' -import {addStyle} from 'lib/styles' -import {useScrollHandlers} from '#/lib/ScrollContext' +import {FlatListProps, RefreshControl, ViewToken} from 'react-native' import {runOnJS, useSharedValue} from 'react-native-reanimated' + import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {usePalette} from '#/lib/hooks/usePalette' +import {useScrollHandlers} from '#/lib/ScrollContext' +import {addStyle} from 'lib/styles' +import {FlatList_INTERNAL} from './Views' export type ListMethods = FlatList_INTERNAL export type ListProps = Omit< @@ -18,6 +19,7 @@ export type ListProps = Omit< headerOffset?: number refreshing?: boolean onRefresh?: () => void + onItemSeen?: (item: ItemT) => void } export type ListRef = React.MutableRefObject @@ -28,6 +30,7 @@ function ListImpl( onScrolledDownChange, refreshing, onRefresh, + onItemSeen, headerOffset, style, ...props @@ -62,6 +65,25 @@ function ListImpl( }, }) + const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => { + if (!onItemSeen) { + return [undefined, undefined] + } + return [ + (info: {viewableItems: Array; changed: Array}) => { + for (const item of info.changed) { + if (item.isViewable) { + onItemSeen(item.item) + } + } + }, + { + itemVisiblePercentThreshold: 40, + minimumViewTime: 100, + }, + ] + }, [onItemSeen]) + let refreshControl if (refreshing !== undefined || onRefresh !== undefined) { refreshControl = ( @@ -91,6 +113,8 @@ function ListImpl( refreshControl={refreshControl} onScroll={scrollHandler} scrollEventThrottle={1} + onViewableItemsChanged={onViewableItemsChanged} + viewabilityConfig={viewabilityConfig} style={style} ref={ref} /> diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 959dfb61f2..caa5387241 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -1,11 +1,5 @@ import React, {isValidElement, memo, startTransition, useRef} from 'react' -import { - FlatListProps, - StyleSheet, - View, - ViewProps, - ViewToken, -} from 'react-native' +import {FlatListProps, StyleSheet, View, ViewProps} from 'react-native' import {batchedUpdates} from '#/lib/batchedUpdates' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -25,6 +19,7 @@ export type ListProps = Omit< headerOffset?: number refreshing?: boolean onRefresh?: () => void + onItemSeen?: (item: ItemT) => void desktopFixedHeight: any // TODO: Better types. } export type ListRef = React.MutableRefObject // TODO: Better types. @@ -44,7 +39,7 @@ function ListImpl( onRefresh: _unsupportedOnRefresh, onScrolledDownChange, onContentSizeChange, - onViewableItemsChanged, + onItemSeen, renderItem, extraData, style, @@ -195,12 +190,11 @@ function ListImpl( return ( key={key} - itemKey={key} item={item} index={index} renderItem={renderItem} extraData={extraData} - onViewableItemsChanged={onViewableItemsChanged} + onItemSeen={onItemSeen} /> ) })} @@ -243,14 +237,12 @@ function useResizeObserver( } let Row = function RowImpl({ - itemKey, item, index, renderItem, extraData: _unused, - onViewableItemsChanged, + onItemSeen, }: { - itemKey: string item: ItemT index: number renderItem: @@ -258,65 +250,7 @@ let Row = function RowImpl({ | undefined | ((data: {index: number; item: any; separators: any}) => React.ReactNode) extraData: any - onViewableItemsChanged: FlatListProps['onViewableItemsChanged'] -}): React.ReactNode { - if (!renderItem) { - return null - } - return ( - - - {renderItem({item, index, separators: null as any})} - - - ) -} -Row = React.memo(Row) - -let RowVisibility = function ({ - itemKey, - item, - index, - onViewableItemsChanged, - children, -}: { - itemKey: string - item: ItemT - index: number - onViewableItemsChanged: FlatListProps['onViewableItemsChanged'] - children: React.ReactNode -}): React.ReactNode { - if (!onViewableItemsChanged) { - return children - } - return ( - - {children} - - ) -} -RowVisibility = React.memo(RowVisibility) - -let RowVisibilityInner = function ({ - itemKey, - item, - index, - onViewableItemsChanged, - children, -}: { - itemKey: string - item: ItemT - index: number - onViewableItemsChanged: FlatListProps['onViewableItemsChanged'] - children: React.ReactNode + onItemSeen?: (item: ItemT) => void }): React.ReactNode { const tailRef = React.useRef(null) const isIntersecting = React.useRef(false) @@ -324,19 +258,15 @@ let RowVisibilityInner = function ({ const handleIntersection = useNonReactiveCallback( (entries: IntersectionObserverEntry[]) => { batchedUpdates(() => { + if (!onItemSeen) { + return + } entries.forEach(entry => { if (entry.isIntersecting !== isIntersecting.current) { isIntersecting.current = entry.isIntersecting - const itemInfo: ViewToken = { - index, - item, - key: itemKey, - isViewable: entry.isIntersecting, + if (entry.isIntersecting) { + onItemSeen!(item) } - onViewableItemsChanged!({ - viewableItems: [itemInfo], - changed: [itemInfo], - }) } }) }) @@ -344,17 +274,28 @@ let RowVisibilityInner = function ({ ) React.useEffect(() => { + if (!onItemSeen) { + return + } const observer = new IntersectionObserver(handleIntersection) const tail: Element | null = tailRef.current! observer.observe(tail) return () => { observer.unobserve(tail) } - }, [handleIntersection]) + }, [handleIntersection, onItemSeen]) + + if (!renderItem) { + return null + } - return {children} + return ( + + {renderItem({item, index, separators: null as any})} + + ) } -RowVisibilityInner = React.memo(RowVisibilityInner) +Row = React.memo(Row) let Visibility = ({ topMargin = '0px', From e1e2bbf38665ac0395c5aeeee9a3bda514737937 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 16 Apr 2024 21:08:55 -0700 Subject: [PATCH 03/25] Add FeedFeedback tracker --- src/state/feed-feedback.tsx | 108 +++++++++++++++++++++++++++++++ src/state/queries/post-feed.ts | 8 +++ src/view/com/feeds/FeedPage.tsx | 31 +++++---- src/view/com/posts/Feed.tsx | 16 +---- src/view/com/posts/FeedItem.tsx | 1 + src/view/screens/ProfileFeed.tsx | 25 +++---- 6 files changed, 150 insertions(+), 39 deletions(-) create mode 100644 src/state/feed-feedback.tsx diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx new file mode 100644 index 0000000000..789089acc2 --- /dev/null +++ b/src/state/feed-feedback.tsx @@ -0,0 +1,108 @@ +import React from 'react' +import {AppState, AppStateStatus} from 'react-native' +import debounce from 'lodash.debounce' + +import {PROD_DEFAULT_FEED} from '#/lib/constants' +import {FeedDescriptor, isFeedPostSlice} from '#/state/queries/post-feed' + +// TODO replace with atproto api +interface Interaction { + uri: string + event: string + feedContext: string +} + +type StateContext = { + enabled: boolean + onItemSeen: (item: any) => void + send: (interaction: Interaction) => void +} + +const stateContext = React.createContext({ + enabled: false, + onItemSeen: (_item: any) => {}, + send: (_interaction: Interaction) => {}, +}) + +export function Provider({ + feed, + children, +}: React.PropsWithChildren<{feed: FeedDescriptor}>) { + const enabled = isDiscoverFeed(feed) + const queue = React.useRef>(new Set()) + + const sendToFeed = React.useRef( + debounce( + () => { + console.log(Array.from(queue.current).map(toInteraction)) + queue.current.clear() + }, + 15e3, + {maxWait: 60e3}, + ), + ) + + React.useEffect(() => { + if (!enabled) { + return + } + const sub = AppState.addEventListener('change', (state: AppStateStatus) => { + if (state === 'background') { + sendToFeed.current.flush() + } + }) + return () => sub.remove() + }, [enabled, sendToFeed]) + + const state = React.useMemo(() => { + return { + enabled, + onItemSeen: (slice: any) => { + if (!enabled) { + return + } + if (!isFeedPostSlice(slice)) { + return + } + for (const postItem of slice.items) { + const str = toString({ + uri: postItem.uri, + event: 'app.bsky.feed.defs#interactionSeen', + feedContext: 'TODO', + }) + if (!queue.current.has(str)) { + queue.current.add(str) + sendToFeed.current() + } + } + }, + send: (interaction: Interaction) => { + queue.current.add(toString(interaction)) + }, + } + }, [enabled, queue, sendToFeed]) + + return {children} +} + +export function useFeedFeedback() { + 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 feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}` +} + +function toString(interaction: Interaction): string { + return `${interaction.uri}|${interaction.event}|${interaction.feedContext}` +} + +function toInteraction(str: string): Interaction { + const [uri, event, feedContext] = str.split('|') + return {uri, event, feedContext} +} diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index ee22bac691..b986a1b3c5 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -73,6 +73,7 @@ export interface FeedPostSliceItem { } export interface FeedPostSlice { + _isFeedPostSlice: boolean _reactKey: string rootUri: string isThread: boolean @@ -268,6 +269,7 @@ export function usePostFeedQuery( return { _reactKey: slice._reactKey, + _isFeedPostSlice: true, rootUri: slice.rootItem.post.uri, isThread: slice.items.length > 1 && @@ -474,3 +476,9 @@ export function resetProfilePostsQueries( }) }, timeout) } + +export function isFeedPostSlice(v: any): v is FeedPostSlice { + return ( + v && typeof v === 'object' && '_isFeedPostSlice' in v && v._isFeedPostSlice + ) +} diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 25c7e1006d..850838bb32 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -9,6 +9,7 @@ import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers' import {logEvent, useGate} from '#/lib/statsig/statsig' import {isNative} from '#/platform/detection' import {listenSoftReset} from '#/state/events' +import * as FeedFeedback from '#/state/feed-feedback' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {truncateAndInvalidate} from '#/state/queries/util' @@ -117,20 +118,22 @@ export function FeedPage({ return ( - + + + {(isScrolledDown || hasNew) && ( void) | null>(null) const lastFetchRef = React.useRef(Date.now()) @@ -265,20 +267,6 @@ let Feed = ({ fetchNextPage() }, [fetchNextPage]) - const set = React.useRef(new Set()) - const onItemSeen = React.useCallback( - item => { - if (!item.items?.[0]) { - return - } - if (!set.current.has(item.items[0].post.record.text)) { - console.log(item.items[0].post.record.text, item) - set.current.add(item.items[0].post.record.text) - } - }, - [set], - ) - // rendering // = diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 0fbcc4a13c..d5ed7a7f90 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -36,6 +36,7 @@ import {FeedNameText} from '../util/FeedInfoText' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {atoms as a} from '#/alf' +import {useFeedFeedback} from '#/state/feed-feedback' export function FeedItem({ post, diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 814c1e8558..0370a8f0fb 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -26,6 +26,7 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {truncateAndInvalidate} from '#/state/queries/util' import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' +import * as FeedFeedback from '#/state/feed-feedback' import {useAnalytics} from 'lib/analytics/analytics' import {useHaptics} from 'lib/haptics' import {usePalette} from 'lib/hooks/usePalette' @@ -490,17 +491,19 @@ const FeedSection = React.forwardRef( return ( - + + + {(isScrolledDown || hasNew) && ( Date: Tue, 16 Apr 2024 21:30:01 -0700 Subject: [PATCH 04/25] Add clickthrough interaction tracking --- src/state/feed-feedback.tsx | 7 +- src/view/com/posts/FeedItem.tsx | 94 +++++++++++++++----- src/view/com/util/Link.tsx | 36 ++++---- src/view/com/util/PostMeta.tsx | 20 +++-- src/view/com/util/UserAvatar.tsx | 32 ++++--- src/view/com/util/UserPreviewLink.tsx | 9 +- src/view/com/util/post-embeds/QuoteEmbed.tsx | 52 +++++++---- src/view/com/util/post-embeds/index.tsx | 42 +++++---- 8 files changed, 190 insertions(+), 102 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 789089acc2..8be07cadb2 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -15,13 +15,13 @@ interface Interaction { type StateContext = { enabled: boolean onItemSeen: (item: any) => void - send: (interaction: Interaction) => void + sendInteraction: (interaction: Interaction) => void } const stateContext = React.createContext({ enabled: false, onItemSeen: (_item: any) => {}, - send: (_interaction: Interaction) => {}, + sendInteraction: (_interaction: Interaction) => {}, }) export function Provider({ @@ -76,8 +76,9 @@ export function Provider({ } } }, - send: (interaction: Interaction) => { + sendInteraction: (interaction: Interaction) => { queue.current.add(toString(interaction)) + sendToFeed.current() }, } }, [enabled, queue, sendToFeed]) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index d5ed7a7f90..3b106f036e 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -11,32 +11,33 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {ReasonFeedSource, isReasonFeedSource} from 'lib/api/feed/types' -import {Link, TextLinkOnWebOnly, TextLink} from '../util/Link' -import {Text} from '../util/text/Text' -import {UserInfoText} from '../util/UserInfoText' -import {PostMeta} from '../util/PostMeta' -import {PostCtrls} from '../util/post-ctrls/PostCtrls' -import {PostEmbeds} from '../util/post-embeds' -import {ContentHider} from '#/components/moderation/ContentHider' -import {PostAlerts} from '../../../components/moderation/PostAlerts' -import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' -import {RichText} from '#/components/RichText' -import {PreviewableUserAvatar} from '../util/UserAvatar' -import {s} from 'lib/styles' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' +import {useFeedFeedback} from '#/state/feed-feedback' +import {useComposerControls} from '#/state/shell/composer' +import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types' +import {MAX_POST_LINES} from 'lib/constants' import {usePalette} from 'lib/hooks/usePalette' +import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink} from 'lib/routes/links' -import {MAX_POST_LINES} from 'lib/constants' import {countLines} from 'lib/strings/helpers' -import {useComposerControls} from '#/state/shell/composer' -import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' -import {FeedNameText} from '../util/FeedInfoText' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {s} from 'lib/styles' import {atoms as a} from '#/alf' -import {useFeedFeedback} from '#/state/feed-feedback' +import {ContentHider} from '#/components/moderation/ContentHider' +import {RichText} from '#/components/RichText' +import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' +import {PostAlerts} from '../../../components/moderation/PostAlerts' +import {FeedNameText} from '../util/FeedInfoText' +import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' +import {PostCtrls} from '../util/post-ctrls/PostCtrls' +import {PostEmbeds} from '../util/post-embeds' +import {PostMeta} from '../util/PostMeta' +import {Text} from '../util/text/Text' +import {PreviewableUserAvatar} from '../util/UserAvatar' +import {UserInfoText} from '../util/UserInfoText' export function FeedItem({ post, @@ -112,6 +113,7 @@ let FeedItemInner = ({ const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) + const {sendInteraction} = useFeedFeedback() const replyAuthorDid = useMemo(() => { if (!record?.reply) { @@ -134,6 +136,38 @@ let FeedItemInner = ({ }) }, [post, record, openComposer, moderation]) + const onOpenPost = React.useCallback(() => { + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#clickthroughItem', + feedContext: 'TODO', + }) + }, [sendInteraction, post]) + + const onOpenAuthor = React.useCallback(() => { + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#clickthroughAuthor', + feedContext: 'TODO', + }) + }, [sendInteraction, post]) + + const onOpenReposter = React.useCallback(() => { + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#clickthroughReposter', + feedContext: 'TODO', + }) + }, [sendInteraction, post]) + + const onOpenEmbed = React.useCallback(() => { + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#clickthroughEmbed', + feedContext: 'TODO', + }) + }, [sendInteraction, post]) + const outerStyles = [ styles.outer, { @@ -152,7 +186,8 @@ let FeedItemInner = ({ style={outerStyles} href={href} noFeedback - accessible={false}> + accessible={false} + onBeforePress={onOpenPost}> {isThreadChild && ( @@ -198,7 +233,8 @@ let FeedItemInner = ({ msg`Reposted by ${sanitizeDisplayName( reason.by.displayName || reason.by.handle, )}`, - )}> + )} + onBeforePress={onOpenReposter}> @@ -241,6 +278,7 @@ let FeedItemInner = ({ avatar={post.author.avatar} moderation={moderation.ui('avatar')} type={post.author.associated?.labeler ? 'labeler' : 'user'} + onBeforePress={onOpenAuthor} /> {isThreadParent && ( {!isThreadChild && replyAuthorDid !== '' && ( @@ -296,6 +335,7 @@ let FeedItemInner = ({ richText={richText} postEmbed={post.embed} postAuthor={post.author} + onOpenEmbed={onOpenEmbed} /> void }): React.ReactNode => { const pal = usePalette('default') const {_} = useLingui() @@ -361,7 +403,11 @@ let PostContent = ({ ) : undefined} {postEmbed ? ( - + ) : null} diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index b6c512b09e..5d06f4a377 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -2,34 +2,35 @@ import React, {ComponentProps, memo, useMemo} from 'react' import { GestureResponderEvent, Platform, + Pressable, StyleProp, - TextStyle, TextProps, + TextStyle, + TouchableOpacity, View, ViewStyle, - Pressable, - TouchableOpacity, } from 'react-native' -import {useLinkProps, StackActions} from '@react-navigation/native' -import {Text} from './text/Text' -import {TypographyVariant} from 'lib/ThemeContext' -import {router} from '../../../routes' -import { - convertBskyAppUrlIfNeeded, - isExternalUrl, - linkRequiresWarning, -} from 'lib/strings/url-helpers' -import {isAndroid, isWeb} from 'platform/detection' import {sanitizeUrl} from '@braintree/sanitize-url' -import {PressableWithHover} from './PressableWithHover' +import {StackActions, useLinkProps} from '@react-navigation/native' + import {useModalControls} from '#/state/modals' import {useOpenLink} from '#/state/preferences/in-app-browser' -import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper' import { DebouncedNavigationProp, useNavigationDeduped, } from 'lib/hooks/useNavigationDeduped' +import { + convertBskyAppUrlIfNeeded, + isExternalUrl, + linkRequiresWarning, +} from 'lib/strings/url-helpers' +import {TypographyVariant} from 'lib/ThemeContext' +import {isAndroid, isWeb} from 'platform/detection' +import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper' import {useTheme} from '#/alf' +import {router} from '../../../routes' +import {PressableWithHover} from './PressableWithHover' +import {Text} from './text/Text' type Event = | React.MouseEvent @@ -149,6 +150,7 @@ export const TextLink = memo(function TextLink({ onPress, disableMismatchWarning, navigationAction, + onBeforePress, ...orgProps }: { testID?: string @@ -162,6 +164,7 @@ export const TextLink = memo(function TextLink({ title?: string disableMismatchWarning?: boolean navigationAction?: 'push' | 'replace' | 'navigate' + onBeforePress?: () => void } & TextProps) { const {...props} = useLinkProps({to: sanitizeUrl(href)}) const navigation = useNavigationDeduped() @@ -194,6 +197,7 @@ export const TextLink = memo(function TextLink({ // Let the browser handle opening in new tab etc. return } + onBeforePress?.() if (onPress) { e?.preventDefault?.() // @ts-ignore function signature differs by platform -prf @@ -209,6 +213,7 @@ export const TextLink = memo(function TextLink({ ) }, [ + onBeforePress, onPress, closeModal, openModal, @@ -267,6 +272,7 @@ interface TextLinkOnWebOnlyProps extends TextProps { navigationAction?: 'push' | 'replace' | 'navigate' disableMismatchWarning?: boolean onPointerEnter?: () => void + onBeforePress?: () => void } export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ testID, diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 529fc54e01..502b4a4db5 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -1,18 +1,19 @@ import React, {memo} from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' -import {Text} from './text/Text' -import {TextLinkOnWebOnly} from './Link' -import {niceDate} from 'lib/strings/time' +import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' + +import {usePrefetchProfileQuery} from '#/state/queries/profile' import {usePalette} from 'lib/hooks/usePalette' -import {TypographyVariant} from 'lib/ThemeContext' -import {UserAvatar} from './UserAvatar' +import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' +import {niceDate} from 'lib/strings/time' +import {TypographyVariant} from 'lib/ThemeContext' import {isAndroid, isWeb} from 'platform/detection' +import {TextLinkOnWebOnly} from './Link' +import {Text} from './text/Text' import {TimeElapsed} from './TimeElapsed' -import {makeProfileLink} from 'lib/routes/links' -import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' -import {usePrefetchProfileQuery} from '#/state/queries/profile' +import {UserAvatar} from './UserAvatar' interface PostMetaOpts { author: AppBskyActorDefs.ProfileViewBasic @@ -25,6 +26,7 @@ interface PostMetaOpts { avatarSize?: number displayNameType?: TypographyVariant displayNameStyle?: StyleProp + onOpenAuthor?: () => void style?: StyleProp } @@ -73,6 +75,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { onPointerEnter={ isWeb ? () => prefetchProfileQuery(opts.author.did) : undefined } + onBeforePress={opts.onOpenAuthor} /> {!isAndroid && ( @@ -95,6 +98,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { title={niceDate(opts.timestamp)} accessibilityHint="" href={opts.postHref} + onBeforePress={opts.onOpenAuthor} /> )} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 4beedbd5b4..3a24c7c324 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -1,30 +1,30 @@ import React, {memo, useMemo} from 'react' import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' -import Svg, {Circle, Rect, Path} from 'react-native-svg' import {Image as RNImage} from 'react-native-image-crop-picker' -import {useLingui} from '@lingui/react' -import {msg, Trans} from '@lingui/macro' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import Svg, {Circle, Path, Rect} from 'react-native-svg' import {ModerationUI} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import {HighPriorityImage} from 'view/com/util/images/Image' -import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' +import {usePalette} from 'lib/hooks/usePalette' import { - usePhotoLibraryPermission, useCameraPermission, + usePhotoLibraryPermission, } from 'lib/hooks/usePermissions' import {colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {isWeb, isAndroid, isNative} from 'platform/detection' -import {UserPreviewLink} from './UserPreviewLink' -import * as Menu from '#/components/Menu' +import {isAndroid, isNative, isWeb} from 'platform/detection' +import {HighPriorityImage} from 'view/com/util/images/Image' +import {tokens, useTheme} from '#/alf' import { - Camera_Stroke2_Corner0_Rounded as Camera, Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, + Camera_Stroke2_Corner0_Rounded as Camera, } from '#/components/icons/Camera' import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' -import {useTheme, tokens} from '#/alf' +import * as Menu from '#/components/Menu' +import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' +import {UserPreviewLink} from './UserPreviewLink' export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' @@ -47,6 +47,7 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps { moderation?: ModerationUI did: string handle: string + onBeforePress?: () => void } const BLUR_AMOUNT = isWeb ? 5 : 100 @@ -373,7 +374,10 @@ let PreviewableUserAvatar = ( props: PreviewableUserAvatarProps, ): React.ReactNode => { return ( - + ) diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx index a2c46afc01..eea3bb5f97 100644 --- a/src/view/com/util/UserPreviewLink.tsx +++ b/src/view/com/util/UserPreviewLink.tsx @@ -1,13 +1,15 @@ import React from 'react' import {StyleProp, ViewStyle} from 'react-native' -import {Link} from './Link' -import {isWeb} from 'platform/detection' -import {makeProfileLink} from 'lib/routes/links' + import {usePrefetchProfileQuery} from '#/state/queries/profile' +import {makeProfileLink} from 'lib/routes/links' +import {isWeb} from 'platform/detection' +import {Link} from './Link' interface UserPreviewLinkProps { did: string handle: string + onBeforePress?: () => void style?: StyleProp } export function UserPreviewLink( @@ -24,6 +26,7 @@ export function UserPreviewLink( href={makeProfileLink(props)} title={props.handle} asAnchor + onBeforePress={props.onBeforePress} style={props.style}> {props.children} diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 2b1c3e6179..cb76869ad3 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -1,37 +1,40 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import { - AppBskyFeedDefs, - AppBskyEmbedRecord, - AppBskyFeedPost, + AppBskyEmbedExternal, AppBskyEmbedImages, + AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, - AppBskyEmbedExternal, - RichText as RichTextAPI, + AppBskyFeedDefs, + AppBskyFeedPost, moderatePost, ModerationDecision, + RichText as RichTextAPI, } from '@atproto/api' import {AtUri} from '@atproto/api' -import {PostMeta} from '../PostMeta' -import {Link} from '../Link' -import {Text} from '../text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {ComposerOptsQuote} from 'state/shell/composer' -import {PostEmbeds} from '.' -import {PostAlerts} from '../../../../components/moderation/PostAlerts' -import {makeProfileLink} from 'lib/routes/links' -import {InfoCircleIcon} from 'lib/icons' import {Trans} from '@lingui/macro' + import {useModerationOpts} from '#/state/queries/preferences' -import {ContentHider} from '../../../../components/moderation/ContentHider' -import {RichText} from '#/components/RichText' +import {usePalette} from 'lib/hooks/usePalette' +import {InfoCircleIcon} from 'lib/icons' +import {makeProfileLink} from 'lib/routes/links' +import {ComposerOptsQuote} from 'state/shell/composer' import {atoms as a} from '#/alf' +import {RichText} from '#/components/RichText' +import {ContentHider} from '../../../../components/moderation/ContentHider' +import {PostAlerts} from '../../../../components/moderation/PostAlerts' +import {Link} from '../Link' +import {PostMeta} from '../PostMeta' +import {Text} from '../text/Text' +import {PostEmbeds} from '.' export function MaybeQuoteEmbed({ embed, + onOpen, style, }: { embed: AppBskyEmbedRecord.View + onOpen?: () => void style?: StyleProp }) { const pal = usePalette('default') @@ -44,6 +47,7 @@ export function MaybeQuoteEmbed({ ) @@ -72,10 +76,12 @@ export function MaybeQuoteEmbed({ function QuoteEmbedModerated({ viewRecord, postRecord, + onOpen, style, }: { viewRecord: AppBskyEmbedRecord.ViewRecord postRecord: AppBskyFeedPost.Record + onOpen?: () => void style?: StyleProp }) { const moderationOpts = useModerationOpts() @@ -95,16 +101,25 @@ function QuoteEmbedModerated({ embeds: viewRecord.embeds, } - return + return ( + + ) } export function QuoteEmbed({ quote, moderation, + onOpen, style, }: { quote: ComposerOptsQuote moderation?: ModerationDecision + onOpen?: () => void style?: StyleProp }) { const pal = usePalette('default') @@ -140,7 +155,8 @@ export function QuoteEmbed({ style={[styles.container, pal.borderDark, style]} hoverStyle={{borderColor: pal.colors.borderLinkHover}} href={itemHref} - title={itemTitle}> + title={itemTitle} + onBeforePress={onOpen}> void style?: StyleProp }) { const pal = usePalette('default') @@ -64,8 +67,12 @@ export function PostEmbeds({ if (AppBskyEmbedRecordWithMedia.isView(embed)) { return ( - - + + ) } @@ -92,7 +99,7 @@ export function PostEmbeds({ // quote post // = - return + return } // image embed @@ -170,7 +177,8 @@ export function PostEmbeds({ href={link.uri} style={[styles.extOuter, pal.view, pal.borderDark, style]} hoverStyle={{borderColor: pal.colors.borderLinkHover}} - onLongPress={onShareExternal}> + onLongPress={onShareExternal} + onBeforePress={onOpen}> From 7574c8fb680c51672c1523a3ee90db09540e9178 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 16 Apr 2024 21:48:09 -0700 Subject: [PATCH 05/25] Add engagement interaction tracking --- src/view/com/posts/FeedItem.tsx | 7 +++- src/view/com/util/post-ctrls/PostCtrls.tsx | 44 ++++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 3b106f036e..1693d4f79b 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -124,6 +124,11 @@ let FeedItemInner = ({ }, [record?.reply]) const onPressReply = React.useCallback(() => { + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#interactionReply', + feedContext: 'TODO', + }) openComposer({ replyTo: { uri: post.uri, @@ -134,7 +139,7 @@ let FeedItemInner = ({ moderation, }, }) - }, [post, record, openComposer, moderation]) + }, [post, record, openComposer, moderation, sendInteraction]) const onOpenPost = React.useCallback(() => { sendInteraction({ diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index cd4a363730..ee8f1a6aed 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -24,6 +24,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 {useFeedFeedback} from '#/state/feed-feedback' import {useModalControls} from '#/state/modals' import { usePostLikeMutationQueue, @@ -67,6 +68,7 @@ let PostCtrls = ({ ) const requireAuth = useRequireAuth() const loggedOutWarningPromptControl = useDialogControl() + const {sendInteraction} = useFeedFeedback() const playHaptic = useHaptics() const shouldShowLoggedOutWarning = React.useMemo(() => { @@ -86,6 +88,11 @@ let PostCtrls = ({ try { if (!post.viewer?.like) { playHaptic() + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#interactionLike', + feedContext: 'TODO', + }) await queueLike() } else { await queueUnlike() @@ -95,13 +102,25 @@ let PostCtrls = ({ throw e } } - }, [playHaptic, post.viewer?.like, queueLike, queueUnlike]) + }, [ + playHaptic, + post.uri, + post.viewer?.like, + queueLike, + queueUnlike, + sendInteraction, + ]) const onRepost = useCallback(async () => { closeModal() try { if (!post.viewer?.repost) { playHaptic() + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#interactionRepost', + feedContext: 'TODO', + }) await queueRepost() } else { await queueUnrepost() @@ -111,10 +130,23 @@ let PostCtrls = ({ throw e } } - }, [closeModal, post.viewer?.repost, playHaptic, queueRepost, queueUnrepost]) + }, [ + closeModal, + post.uri, + post.viewer?.repost, + playHaptic, + queueRepost, + queueUnrepost, + sendInteraction, + ]) const onQuote = useCallback(() => { closeModal() + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#interactionQuote', + feedContext: 'TODO', + }) openComposer({ quote: { uri: post.uri, @@ -134,6 +166,7 @@ let PostCtrls = ({ post.indexedAt, record.text, playHaptic, + sendInteraction, ]) const onShare = useCallback(() => { @@ -141,7 +174,12 @@ let PostCtrls = ({ const href = makeProfileLink(post.author, 'post', urip.rkey) const url = toShareUrl(href) shareUrl(url) - }, [post.uri, post.author]) + sendInteraction({ + uri: post.uri, + event: 'app.bsky.feed.defs#interactionShare', + feedContext: 'TODO', + }) + }, [post.uri, post.author, sendInteraction]) return ( From b3e68f631ed34adb87fc7b1e60e304f1c56db085 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 17 Apr 2024 12:37:53 -0700 Subject: [PATCH 06/25] Reduce duplicate sends, introduce a flushAndReset to be triggered on refreshes, and modify the api design a bit --- src/state/feed-feedback.tsx | 46 ++++++++++++++++------ src/view/com/feeds/FeedPage.tsx | 21 +++++++--- src/view/com/posts/Feed.tsx | 9 +++-- src/view/com/posts/FeedItem.tsx | 4 +- src/view/com/util/post-ctrls/PostCtrls.tsx | 4 +- src/view/screens/ProfileFeed.tsx | 10 +++-- 6 files changed, 66 insertions(+), 28 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 8be07cadb2..a656b72ae8 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -16,25 +16,28 @@ type StateContext = { enabled: boolean onItemSeen: (item: any) => void sendInteraction: (interaction: Interaction) => void + flushAndReset: () => void } const stateContext = React.createContext({ enabled: false, onItemSeen: (_item: any) => {}, sendInteraction: (_interaction: Interaction) => {}, + flushAndReset: () => {}, }) -export function Provider({ - feed, - children, -}: React.PropsWithChildren<{feed: FeedDescriptor}>) { +export function useFeedFeedback(feed: FeedDescriptor) { const enabled = isDiscoverFeed(feed) const queue = React.useRef>(new Set()) + const history = React.useRef>(new Set()) const sendToFeed = React.useRef( debounce( () => { console.log(Array.from(queue.current).map(toInteraction)) + for (const v of queue.current) { + history.current.add(v) + } queue.current.clear() }, 15e3, @@ -54,9 +57,11 @@ export function Provider({ return () => sub.remove() }, [enabled, sendToFeed]) - const state = React.useMemo(() => { + return React.useMemo(() => { return { enabled, + + // pass this method to the onItemSeen onItemSeen: (slice: any) => { if (!enabled) { return @@ -70,23 +75,42 @@ export function Provider({ event: 'app.bsky.feed.defs#interactionSeen', feedContext: 'TODO', }) - if (!queue.current.has(str)) { + if (!history.current.has(str)) { queue.current.add(str) sendToFeed.current() } } }, + + // call on various events + // queues the event to be sent with the debounced sendToFeed call sendInteraction: (interaction: Interaction) => { - queue.current.add(toString(interaction)) - sendToFeed.current() + if (!enabled) { + return + } + const str = toString(interaction) + if (!history.current.has(str)) { + queue.current.add(str) + sendToFeed.current() + } + }, + + // call on feed refresh + // immediately sends all queued events and clears the history tracker + flushAndReset: () => { + if (!enabled) { + return + } + sendToFeed.current.flush() + history.current.clear() }, } }, [enabled, queue, sendToFeed]) - - return {children} } -export function useFeedFeedback() { +export const FeedFeedbackProvider = stateContext.Provider + +export function useFeedFeedbackContext() { return React.useContext(stateContext) } diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 850838bb32..087822b264 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -9,7 +9,7 @@ import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers' import {logEvent, useGate} from '#/lib/statsig/statsig' import {isNative} from '#/platform/detection' import {listenSoftReset} from '#/state/events' -import * as FeedFeedback from '#/state/feed-feedback' +import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {truncateAndInvalidate} from '#/state/queries/util' @@ -52,6 +52,7 @@ export function FeedPage({ const setMinimalShellMode = useSetMinimalShellMode() const {screen, track} = useAnalytics() const headerOffset = useHeaderOffset() + const feedFeedback = useFeedFeedback(feed) const scrollElRef = React.useRef(null) const [hasNew, setHasNew] = React.useState(false) @@ -68,6 +69,7 @@ export function FeedPage({ getTabState(getRootNavigation(navigation).getState(), 'Home') === TabState.InsideAtRoot if (isScreenFocused && isPageFocused) { + feedFeedback.flushAndReset() scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) @@ -77,7 +79,15 @@ export function FeedPage({ reason: 'soft-reset', }) } - }, [navigation, isPageFocused, scrollToTop, queryClient, feed, setHasNew]) + }, [ + navigation, + isPageFocused, + scrollToTop, + queryClient, + feed, + setHasNew, + feedFeedback, + ]) // fires when page within screen is activated/deactivated React.useEffect(() => { @@ -94,6 +104,7 @@ export function FeedPage({ }, [openComposer, track]) const onPressLoadLatest = React.useCallback(() => { + feedFeedback.flushAndReset() scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) @@ -102,7 +113,7 @@ export function FeedPage({ feedUrl: feed, reason: 'load-latest', }) - }, [scrollToTop, feed, queryClient, setHasNew]) + }, [scrollToTop, feed, queryClient, setHasNew, feedFeedback]) let feedPollInterval if ( @@ -118,7 +129,7 @@ export function FeedPage({ return ( - + - + {(isScrolledDown || hasNew) && ( void) | null>(null) const lastFetchRef = React.useRef(Date.now()) @@ -226,12 +226,13 @@ let Feed = ({ setIsPTRing(true) try { await refetch() + feedFeedback.flushAndReset() onHasNew?.(false) } catch (err) { logger.error('Failed to refresh posts feed', {message: err}) } setIsPTRing(false) - }, [refetch, track, setIsPTRing, onHasNew, feed, feedType]) + }, [refetch, track, setIsPTRing, onHasNew, feed, feedType, feedFeedback]) const onEndReached = React.useCallback(async () => { if (isFetching || !hasNextPage || isError) return @@ -355,7 +356,7 @@ let Feed = ({ } initialNumToRender={initialNumToRender} windowSize={11} - onItemSeen={onItemSeen} + onItemSeen={feedFeedback.onItemSeen} /> ) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 1693d4f79b..3aeb530cb4 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -15,7 +15,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' -import {useFeedFeedback} from '#/state/feed-feedback' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useComposerControls} from '#/state/shell/composer' import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types' import {MAX_POST_LINES} from 'lib/constants' @@ -113,7 +113,7 @@ let FeedItemInner = ({ const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) }, [post.uri, post.author]) - const {sendInteraction} = useFeedFeedback() + const {sendInteraction} = useFeedFeedbackContext() const replyAuthorDid = useMemo(() => { if (!record?.reply) { diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index ee8f1a6aed..ff2d1ee2da 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -24,7 +24,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 {useFeedFeedback} from '#/state/feed-feedback' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useModalControls} from '#/state/modals' import { usePostLikeMutationQueue, @@ -68,7 +68,7 @@ let PostCtrls = ({ ) const requireAuth = useRequireAuth() const loggedOutWarningPromptControl = useDialogControl() - const {sendInteraction} = useFeedFeedback() + const {sendInteraction} = useFeedFeedbackContext() const playHaptic = useHaptics() const shouldShowLoggedOutWarning = React.useMemo(() => { diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 0370a8f0fb..121bf20231 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -10,6 +10,7 @@ import {HITSLOP_20} from '#/lib/constants' import {logger} from '#/logger' import {isNative} from '#/platform/detection' import {listenSoftReset} from '#/state/events' +import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' import {FeedDescriptor} from '#/state/queries/post-feed' @@ -26,7 +27,6 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {truncateAndInvalidate} from '#/state/queries/util' import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' -import * as FeedFeedback from '#/state/feed-feedback' import {useAnalytics} from 'lib/analytics/analytics' import {useHaptics} from 'lib/haptics' import {usePalette} from 'lib/hooks/usePalette' @@ -464,15 +464,17 @@ const FeedSection = React.forwardRef( const [isScrolledDown, setIsScrolledDown] = React.useState(false) const queryClient = useQueryClient() const isScreenFocused = useIsFocused() + const feedFeedback = useFeedFeedback(feed) const onScrollToTop = useCallback(() => { + feedFeedback.flushAndReset() scrollElRef.current?.scrollToOffset({ animated: isNative, offset: -headerHeight, }) truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) - }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) + }, [scrollElRef, headerHeight, queryClient, feed, setHasNew, feedFeedback]) React.useImperativeHandle(ref, () => ({ scrollToTop: onScrollToTop, @@ -491,7 +493,7 @@ const FeedSection = React.forwardRef( return ( - + ( renderEmptyState={renderPostsEmpty} headerOffset={headerHeight} /> - + {(isScrolledDown || hasNew) && ( Date: Wed, 17 Apr 2024 13:10:31 -0700 Subject: [PATCH 07/25] Wire up SDK types and feedContext --- package.json | 2 +- src/state/feed-feedback.tsx | 24 +++++++++------------ src/state/queries/post-feed.ts | 2 ++ src/view/com/posts/FeedItem.tsx | 25 +++++++++++++--------- src/view/com/posts/FeedSlice.tsx | 15 ++++++++----- src/view/com/util/List.web.tsx | 12 +++++------ src/view/com/util/post-ctrls/PostCtrls.tsx | 15 ++++++++----- yarn.lock | 12 +++++++++++ 8 files changed, 66 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 5eaba7a972..8c64a11382 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "^0.12.2", + "@atproto/api": "^0.12.3", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1", diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index a656b72ae8..53eb92cf29 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -1,28 +1,22 @@ import React from 'react' import {AppState, AppStateStatus} from 'react-native' +import {AppBskyFeedDefs} from '@atproto/api' import debounce from 'lodash.debounce' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {FeedDescriptor, isFeedPostSlice} from '#/state/queries/post-feed' -// TODO replace with atproto api -interface Interaction { - uri: string - event: string - feedContext: string -} - type StateContext = { enabled: boolean onItemSeen: (item: any) => void - sendInteraction: (interaction: Interaction) => void + sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void flushAndReset: () => void } const stateContext = React.createContext({ enabled: false, onItemSeen: (_item: any) => {}, - sendInteraction: (_interaction: Interaction) => {}, + sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, flushAndReset: () => {}, }) @@ -73,7 +67,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { const str = toString({ uri: postItem.uri, event: 'app.bsky.feed.defs#interactionSeen', - feedContext: 'TODO', + feedContext: postItem.feedContext, }) if (!history.current.has(str)) { queue.current.add(str) @@ -84,7 +78,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { // call on various events // queues the event to be sent with the debounced sendToFeed call - sendInteraction: (interaction: Interaction) => { + sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => { if (!enabled) { return } @@ -123,11 +117,13 @@ function isDiscoverFeed(feed: FeedDescriptor) { return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}` } -function toString(interaction: Interaction): string { - return `${interaction.uri}|${interaction.event}|${interaction.feedContext}` +function toString(interaction: AppBskyFeedDefs.Interaction): string { + return `${interaction.uri}|${interaction.event}|${ + interaction.feedContext || '' + }` } -function toInteraction(str: string): Interaction { +function toInteraction(str: string): AppBskyFeedDefs.Interaction { const [uri, event, feedContext] = str.split('|') return {uri, event, feedContext} } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index b986a1b3c5..85997c6ad3 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -69,6 +69,7 @@ export interface FeedPostSliceItem { post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource + feedContext: string | undefined moderation: ModerationDecision } @@ -294,6 +295,7 @@ export function usePostFeedQuery( i === 0 && slice.source ? slice.source : item.reason, + feedContext: item.feedContext, moderation: moderations[i], } } diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 3aeb530cb4..e4a7e51fbf 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -43,6 +43,7 @@ export function FeedItem({ post, record, reason, + feedContext, moderation, isThreadChild, isThreadLastChild, @@ -51,6 +52,7 @@ export function FeedItem({ post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined + feedContext: string | undefined moderation: ModerationDecision isThreadChild?: boolean isThreadLastChild?: boolean @@ -76,6 +78,7 @@ export function FeedItem({ post={postShadowed} record={record} reason={reason} + feedContext={feedContext} richText={richText} moderation={moderation} isThreadChild={isThreadChild} @@ -91,6 +94,7 @@ let FeedItemInner = ({ post, record, reason, + feedContext, richText, moderation, isThreadChild, @@ -100,6 +104,7 @@ let FeedItemInner = ({ post: Shadow record: AppBskyFeedPost.Record reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined + feedContext: string | undefined richText: RichTextAPI moderation: ModerationDecision isThreadChild?: boolean @@ -127,7 +132,7 @@ let FeedItemInner = ({ sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#interactionReply', - feedContext: 'TODO', + feedContext, }) openComposer({ replyTo: { @@ -139,39 +144,39 @@ let FeedItemInner = ({ moderation, }, }) - }, [post, record, openComposer, moderation, sendInteraction]) + }, [post, record, openComposer, moderation, sendInteraction, feedContext]) const onOpenPost = React.useCallback(() => { sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#clickthroughItem', - feedContext: 'TODO', + feedContext, }) - }, [sendInteraction, post]) + }, [sendInteraction, post, feedContext]) const onOpenAuthor = React.useCallback(() => { sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#clickthroughAuthor', - feedContext: 'TODO', + feedContext, }) - }, [sendInteraction, post]) + }, [sendInteraction, post, feedContext]) const onOpenReposter = React.useCallback(() => { sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#clickthroughReposter', - feedContext: 'TODO', + feedContext, }) - }, [sendInteraction, post]) + }, [sendInteraction, post, feedContext]) const onOpenEmbed = React.useCallback(() => { sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#clickthroughEmbed', - feedContext: 'TODO', + feedContext, }) - }, [sendInteraction, post]) + }, [sendInteraction, post, feedContext]) const outerStyles = [ styles.outer, diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index 49e48aa202..27a9ff8c06 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -1,14 +1,15 @@ import React, {memo} from 'react' import {StyleSheet, View} from 'react-native' -import {FeedPostSlice} from '#/state/queries/post-feed' +import Svg, {Circle, Line} from 'react-native-svg' import {AtUri} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {FeedPostSlice} from '#/state/queries/post-feed' +import {usePalette} from 'lib/hooks/usePalette' +import {makeProfileLink} from 'lib/routes/links' import {Link} from '../util/Link' import {Text} from '../util/text/Text' -import Svg, {Circle, Line} from 'react-native-svg' import {FeedItem} from './FeedItem' -import {usePalette} from 'lib/hooks/usePalette' -import {makeProfileLink} from 'lib/routes/links' -import {Trans} from '@lingui/macro' let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => { if (slice.isThread && slice.items.length > 3) { @@ -20,6 +21,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => { post={slice.items[0].post} record={slice.items[0].record} reason={slice.items[0].reason} + feedContext={slice.items[0].feedContext} moderation={slice.items[0].moderation} isThreadParent={isThreadParentAt(slice.items, 0)} isThreadChild={isThreadChildAt(slice.items, 0)} @@ -29,6 +31,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => { post={slice.items[1].post} record={slice.items[1].record} reason={slice.items[1].reason} + feedContext={slice.items[1].feedContext} moderation={slice.items[1].moderation} isThreadParent={isThreadParentAt(slice.items, 1)} isThreadChild={isThreadChildAt(slice.items, 1)} @@ -39,6 +42,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => { post={slice.items[last].post} record={slice.items[last].record} reason={slice.items[last].reason} + feedContext={slice.items[last].feedContext} moderation={slice.items[last].moderation} isThreadParent={isThreadParentAt(slice.items, last)} isThreadChild={isThreadChildAt(slice.items, last)} @@ -56,6 +60,7 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => { post={slice.items[i].post} record={slice.items[i].record} reason={slice.items[i].reason} + feedContext={slice.items[i].feedContext} moderation={slice.items[i].moderation} isThreadParent={isThreadParentAt(slice.items, i)} isThreadChild={isThreadChildAt(slice.items, i)} diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index caa5387241..d8f566109e 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -250,9 +250,9 @@ let Row = function RowImpl({ | undefined | ((data: {index: number; item: any; separators: any}) => React.ReactNode) extraData: any - onItemSeen?: (item: ItemT) => void + onItemSeen: ((item: any) => void) | undefined }): React.ReactNode { - const tailRef = React.useRef(null) + const rowRef = React.useRef(null) const isIntersecting = React.useRef(false) const handleIntersection = useNonReactiveCallback( @@ -278,10 +278,10 @@ let Row = function RowImpl({ return } const observer = new IntersectionObserver(handleIntersection) - const tail: Element | null = tailRef.current! - observer.observe(tail) + const row: Element | null = rowRef.current! + observer.observe(row) return () => { - observer.unobserve(tail) + observer.unobserve(row) } }, [handleIntersection, onItemSeen]) @@ -290,7 +290,7 @@ let Row = function RowImpl({ } return ( - + {renderItem({item, index, separators: null as any})} ) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index ff2d1ee2da..6788b3569f 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -45,6 +45,7 @@ let PostCtrls = ({ post, record, richText, + feedContext, style, onPressReply, logContext, @@ -53,6 +54,7 @@ let PostCtrls = ({ post: Shadow record: AppBskyFeedPost.Record richText: RichTextAPI + feedContext: string | undefined style?: StyleProp onPressReply: () => void logContext: 'FeedItem' | 'PostThreadItem' | 'Post' @@ -91,7 +93,7 @@ let PostCtrls = ({ sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#interactionLike', - feedContext: 'TODO', + feedContext, }) await queueLike() } else { @@ -109,6 +111,7 @@ let PostCtrls = ({ queueLike, queueUnlike, sendInteraction, + feedContext, ]) const onRepost = useCallback(async () => { @@ -119,7 +122,7 @@ let PostCtrls = ({ sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#interactionRepost', - feedContext: 'TODO', + feedContext, }) await queueRepost() } else { @@ -138,6 +141,7 @@ let PostCtrls = ({ queueRepost, queueUnrepost, sendInteraction, + feedContext, ]) const onQuote = useCallback(() => { @@ -145,7 +149,7 @@ let PostCtrls = ({ sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#interactionQuote', - feedContext: 'TODO', + feedContext, }) openComposer({ quote: { @@ -167,6 +171,7 @@ let PostCtrls = ({ record.text, playHaptic, sendInteraction, + feedContext, ]) const onShare = useCallback(() => { @@ -177,9 +182,9 @@ let PostCtrls = ({ sendInteraction({ uri: post.uri, event: 'app.bsky.feed.defs#interactionShare', - feedContext: 'TODO', + feedContext, }) - }, [post.uri, post.author, sendInteraction]) + }, [post.uri, post.author, sendInteraction, feedContext]) return ( diff --git a/yarn.lock b/yarn.lock index 1a61c8b037..927d7bee12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46,6 +46,18 @@ multiformats "^9.9.0" tlds "^1.234.0" +"@atproto/api@^0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.3.tgz#5b7b1c7d4210ee9315961504900c8409395cbb17" + integrity sha512-y/kGpIEo+mKGQ7VOphpqCAigTI0LZRmDThNChTfSzDKm9TzEobwiw0zUID0Yw6ot1iLLFx3nKURmuZAYlEuobw== + dependencies: + "@atproto/common-web" "^0.3.0" + "@atproto/lexicon" "^0.4.0" + "@atproto/syntax" "^0.3.0" + "@atproto/xrpc" "^0.5.0" + multiformats "^9.9.0" + tlds "^1.234.0" + "@atproto/aws@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.0.tgz#17f3faf744824457cabd62f87be8bf08cacf8029" From 0803a4d53bde9dd76f2e58aa5e98645a3dd746a7 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 Apr 2024 12:51:16 -0700 Subject: [PATCH 08/25] Avoid needless function allocations --- src/state/feed-feedback.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 53eb92cf29..bbf8e34ede 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -25,7 +25,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { const queue = React.useRef>(new Set()) const history = React.useRef>(new Set()) - const sendToFeed = React.useRef( + const [sendToFeed] = React.useState(() => debounce( () => { console.log(Array.from(queue.current).map(toInteraction)) @@ -45,7 +45,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { } const sub = AppState.addEventListener('change', (state: AppStateStatus) => { if (state === 'background') { - sendToFeed.current.flush() + sendToFeed.flush() } }) return () => sub.remove() @@ -71,7 +71,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { }) if (!history.current.has(str)) { queue.current.add(str) - sendToFeed.current() + sendToFeed() } } }, @@ -85,7 +85,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { const str = toString(interaction) if (!history.current.has(str)) { queue.current.add(str) - sendToFeed.current() + sendToFeed() } }, @@ -95,7 +95,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { if (!enabled) { return } - sendToFeed.current.flush() + sendToFeed.flush() history.current.clear() }, } From 6e66a4a3b0a91d874739fbe3e457afa5073f1fa6 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 19 Apr 2024 12:35:49 -0700 Subject: [PATCH 09/25] Fix schema usage --- src/state/feed-feedback.tsx | 4 ++-- src/view/com/posts/FeedItem.tsx | 10 +++++----- src/view/com/util/post-ctrls/PostCtrls.tsx | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index bbf8e34ede..079ff6283e 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -65,7 +65,7 @@ export function useFeedFeedback(feed: FeedDescriptor) { } for (const postItem of slice.items) { const str = toString({ - uri: postItem.uri, + item: postItem.uri, event: 'app.bsky.feed.defs#interactionSeen', feedContext: postItem.feedContext, }) @@ -118,7 +118,7 @@ function isDiscoverFeed(feed: FeedDescriptor) { } function toString(interaction: AppBskyFeedDefs.Interaction): string { - return `${interaction.uri}|${interaction.event}|${ + return `${interaction.item}|${interaction.event}|${ interaction.feedContext || '' }` } diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index e4a7e51fbf..44fbf37010 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -130,7 +130,7 @@ let FeedItemInner = ({ const onPressReply = React.useCallback(() => { sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#interactionReply', feedContext, }) @@ -148,7 +148,7 @@ let FeedItemInner = ({ const onOpenPost = React.useCallback(() => { sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#clickthroughItem', feedContext, }) @@ -156,7 +156,7 @@ let FeedItemInner = ({ const onOpenAuthor = React.useCallback(() => { sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#clickthroughAuthor', feedContext, }) @@ -164,7 +164,7 @@ let FeedItemInner = ({ const onOpenReposter = React.useCallback(() => { sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#clickthroughReposter', feedContext, }) @@ -172,7 +172,7 @@ let FeedItemInner = ({ const onOpenEmbed = React.useCallback(() => { sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#clickthroughEmbed', feedContext, }) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 6788b3569f..3be5bdd34a 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -91,7 +91,7 @@ let PostCtrls = ({ if (!post.viewer?.like) { playHaptic() sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#interactionLike', feedContext, }) @@ -120,7 +120,7 @@ let PostCtrls = ({ if (!post.viewer?.repost) { playHaptic() sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#interactionRepost', feedContext, }) @@ -147,7 +147,7 @@ let PostCtrls = ({ const onQuote = useCallback(() => { closeModal() sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#interactionQuote', feedContext, }) @@ -180,7 +180,7 @@ let PostCtrls = ({ const url = toShareUrl(href) shareUrl(url) sendInteraction({ - uri: post.uri, + item: post.uri, event: 'app.bsky.feed.defs#interactionShare', feedContext, }) From b308bb695131beb908d3036c832461a940c889e5 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 19 Apr 2024 12:36:19 -0700 Subject: [PATCH 10/25] Add show more / show less buttons --- .../emojiSmile_stroke2_corner0_rounded.svg | 1 + src/components/icons/Emoji.tsx | 4 ++ src/state/feed-feedback.tsx | 4 +- src/view/com/feeds/FeedPage.tsx | 2 +- src/view/com/util/forms/PostDropdownBtn.tsx | 56 ++++++++++++++++++- src/view/com/util/post-ctrls/PostCtrls.tsx | 1 + src/view/screens/ProfileFeed.tsx | 3 +- 7 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 assets/icons/emojiSmile_stroke2_corner0_rounded.svg diff --git a/assets/icons/emojiSmile_stroke2_corner0_rounded.svg b/assets/icons/emojiSmile_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..fd329b5098 --- /dev/null +++ b/assets/icons/emojiSmile_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/icons/Emoji.tsx b/src/components/icons/Emoji.tsx index 568cd71e69..6047054058 100644 --- a/src/components/icons/Emoji.tsx +++ b/src/components/icons/Emoji.tsx @@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE' export const EmojiSad_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M6.343 6.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 6.343 6.343ZM19.071 4.93c-3.905-3.905-10.237-3.905-14.142 0-3.905 3.905-3.905 10.237 0 14.142 3.905 3.905 10.237 3.905 14.142 0 3.905-3.905 3.905-10.237 0-14.142Zm-3.537 9.535a5 5 0 0 0-7.07 0 1 1 0 1 0 1.413 1.415 3 3 0 0 1 4.243 0 1 1 0 0 0 1.414-1.415ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5ZM9.25 11c.69 0 1.25-.672 1.25-1.5S9.94 8 9.25 8 8 8.672 8 9.5 8.56 11 9.25 11Z', }) + +export const EmojiSmile_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M17.657 6.343A8 8 0 1 0 6.343 17.657 8 8 0 0 0 17.657 6.343ZM4.929 4.93c3.905-3.905 10.237-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0-3.905-3.905-3.905-10.237 0-14.142Zm3.536 9.192a1 1 0 0 1 1.414 0 3 3 0 0 0 4.243 0 1 1 0 0 1 1.414 1.415 5 5 0 0 1-7.071 0 1 1 0 0 1 0-1.415ZM10.5 9.5c0 .828-.56 1.5-1.25 1.5S8 10.328 8 9.5 8.56 8 9.25 8s1.25.672 1.25 1.5ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5Z', +}) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 079ff6283e..dcf8081871 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -20,8 +20,8 @@ const stateContext = React.createContext({ flushAndReset: () => {}, }) -export function useFeedFeedback(feed: FeedDescriptor) { - const enabled = isDiscoverFeed(feed) +export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { + const enabled = isDiscoverFeed(feed) && hasSession const queue = React.useRef>(new Set()) const history = React.useRef>(new Set()) diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 087822b264..aea2241546 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -52,7 +52,7 @@ export function FeedPage({ const setMinimalShellMode = useSetMinimalShellMode() const {screen, track} = useAnalytics() const headerOffset = useHeaderOffset() - const feedFeedback = useFeedFeedback(feed) + const feedFeedback = useFeedFeedback(feed, hasSession) const scrollElRef = React.useRef(null) const [hasNew, setHasNew] = React.useState(false) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 959e0f692e..6f5dc010df 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -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' @@ -34,6 +35,10 @@ import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' +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' @@ -51,6 +56,7 @@ let PostDropdownBtn = ({ postAuthor, postCid, postUri, + postFeedContext, record, richText, style, @@ -60,6 +66,7 @@ let PostDropdownBtn = ({ postAuthor: AppBskyActorDefs.ProfileViewBasic postCid: string postUri: string + postFeedContext: string | undefined record: AppBskyFeedPost.Record richText: RichTextAPI style?: StyleProp @@ -76,6 +83,7 @@ let PostDropdownBtn = ({ const postDeleteMutation = usePostDeleteMutation() const hiddenPosts = useHiddenPosts() const {hidePost} = useHiddenPostsApi() + const feedFeedback = useFeedFeedbackContext() const openLink = useOpenLink() const navigation = useNavigation() const {mutedWordsDialogControl} = useGlobalDialogsControlContext() @@ -177,6 +185,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]) + return ( @@ -242,9 +268,35 @@ let PostDropdownBtn = ({ {hasSession && ( <> - - + {feedFeedback.enabled && ( + <> + + + + + {_(msg`Show more like this`)} + + + + + + + {_(msg`Show less like this`)} + + + + + )} + + + ( const [isScrolledDown, setIsScrolledDown] = React.useState(false) const queryClient = useQueryClient() const isScreenFocused = useIsFocused() - const feedFeedback = useFeedFeedback(feed) + const {hasSession} = useSession() + const feedFeedback = useFeedFeedback(feed, hasSession) const onScrollToTop = useCallback(() => { feedFeedback.flushAndReset() From 4ec18c733f45b3cf13782b4c4993fd6f38485f1e Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 19 Apr 2024 12:44:11 -0700 Subject: [PATCH 11/25] Fix minor rendering issue on mobile menu --- src/view/com/util/forms/PostDropdownBtn.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 6f5dc010df..9853308373 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -17,7 +17,7 @@ import {CommonNavigatorParams} from '#/lib/routes/types' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {getTranslatorLink} from '#/locale/helpers' import {logger} from '#/logger' -import {isWeb} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' @@ -269,10 +269,10 @@ let PostDropdownBtn = ({ {hasSession && ( <> + {!isNative && } + {feedFeedback.enabled && ( <> - - + + )} - - Date: Mon, 29 Apr 2024 17:49:59 -0700 Subject: [PATCH 12/25] Wire up sendInteractions() --- src/state/feed-feedback.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index dcf8081871..3e9a3da1bc 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -1,10 +1,12 @@ import React from 'react' import {AppState, AppStateStatus} from 'react-native' -import {AppBskyFeedDefs} from '@atproto/api' +import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' import debounce from 'lodash.debounce' import {PROD_DEFAULT_FEED} from '#/lib/constants' +import {logger} from '#/logger' import {FeedDescriptor, isFeedPostSlice} from '#/state/queries/post-feed' +import {useAgent} from './session' type StateContext = { enabled: boolean @@ -21,6 +23,7 @@ const stateContext = React.createContext({ }) export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { + const {getAgent} = useAgent() const enabled = isDiscoverFeed(feed) && hasSession const queue = React.useRef>(new Set()) const history = React.useRef>(new Set()) @@ -28,7 +31,20 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { const [sendToFeed] = React.useState(() => debounce( () => { - console.log(Array.from(queue.current).map(toInteraction)) + const proxyAgent = getAgent().withProxy( + // @ts-ignore TODO need to update withProxy() to support this key -prf + 'bsky_fg', + // TODO when we start sending to other feeds, we need to grab their DID -prf + 'did:web:discover.bsky.app', + ) as BskyAgent + proxyAgent.app.bsky.feed + .sendInteractions({ + interactions: Array.from(queue.current).map(toInteraction), + }) + .catch((e: any) => { + logger.warn('Failed to send feed interactions', {error: e}) + }) + for (const v of queue.current) { history.current.add(v) } From 7551a45d23b0e6721665a877d9a3c25767d24481 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 29 Apr 2024 17:52:59 -0700 Subject: [PATCH 13/25] Fix logic error --- src/view/com/util/post-ctrls/PostCtrls.tsx | 48 +++++++++------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 8f2a21d8e9..8983c14142 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -90,13 +90,11 @@ let PostCtrls = ({ try { if (!post.viewer?.like) { playHaptic() - if (feedContext) { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionLike', - feedContext, - }) - } + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionLike', + feedContext, + }) await queueLike() } else { await queueUnlike() @@ -121,13 +119,11 @@ let PostCtrls = ({ try { if (!post.viewer?.repost) { playHaptic() - if (feedContext) { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionRepost', - feedContext, - }) - } + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionRepost', + feedContext, + }) await queueRepost() } else { await queueUnrepost() @@ -150,13 +146,11 @@ let PostCtrls = ({ const onQuote = useCallback(() => { closeModal() - if (feedContext) { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionQuote', - feedContext, - }) - } + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionQuote', + feedContext, + }) openComposer({ quote: { uri: post.uri, @@ -185,13 +179,11 @@ let PostCtrls = ({ const href = makeProfileLink(post.author, 'post', urip.rkey) const url = toShareUrl(href) shareUrl(url) - if (feedContext) { - sendInteraction({ - item: post.uri, - event: 'app.bsky.feed.defs#interactionShare', - feedContext, - }) - } + sendInteraction({ + item: post.uri, + event: 'app.bsky.feed.defs#interactionShare', + feedContext, + }) }, [post.uri, post.author, sendInteraction, feedContext]) return ( From b392f68ff1288dec0039f2bbcef00d5f000266ce Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 30 Apr 2024 09:55:55 -0700 Subject: [PATCH 14/25] Fix: it's item not uri --- src/state/feed-feedback.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 3e9a3da1bc..bca57ec28f 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -140,6 +140,6 @@ function toString(interaction: AppBskyFeedDefs.Interaction): string { } function toInteraction(str: string): AppBskyFeedDefs.Interaction { - const [uri, event, feedContext] = str.split('|') - return {uri, event, feedContext} + const [item, event, feedContext] = str.split('|') + return {item, event, feedContext} } From cb798f5f908c0cecd06bd56f389dbeb6da975c81 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 3 May 2024 09:50:39 -0700 Subject: [PATCH 15/25] Update 'seen' to mean 3 seconds on-screen with some significant portion visible --- src/view/com/util/List.tsx | 2 +- src/view/com/util/List.web.tsx | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index f747ff17d2..1600494ab0 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -91,7 +91,7 @@ function ListImpl( }, { itemVisiblePercentThreshold: 40, - minimumViewTime: 100, + minimumViewTime: 3e3, }, ] }, [onItemSeen]) diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index d8f566109e..5b7328fa75 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -24,6 +24,11 @@ export type ListProps = Omit< } export type ListRef = React.MutableRefObject // TODO: Better types. +const ON_ITEM_SEEN_WAIT_DURATION = 3e3 // post must be "seen" 3 seconds before capturing +const ON_ITEM_SEEN_INTERSECTION_OPTS = { + rootMargin: '-200px 0px -200px 0px', +} // post must be 200px visible to be "seen" + function ListImpl( { ListHeaderComponent, @@ -253,7 +258,7 @@ let Row = function RowImpl({ onItemSeen: ((item: any) => void) | undefined }): React.ReactNode { const rowRef = React.useRef(null) - const isIntersecting = React.useRef(false) + const intersectionTimeout = React.useRef(undefined) const handleIntersection = useNonReactiveCallback( (entries: IntersectionObserverEntry[]) => { @@ -262,10 +267,15 @@ let Row = function RowImpl({ return } entries.forEach(entry => { - if (entry.isIntersecting !== isIntersecting.current) { - isIntersecting.current = entry.isIntersecting - if (entry.isIntersecting) { - onItemSeen!(item) + if (entry.isIntersecting) { + if (!intersectionTimeout.current) { + intersectionTimeout.current = setTimeout(() => { + onItemSeen!(item) + }, ON_ITEM_SEEN_WAIT_DURATION) + } + } else { + if (intersectionTimeout.current) { + clearTimeout(intersectionTimeout.current) } } }) @@ -277,7 +287,10 @@ let Row = function RowImpl({ if (!onItemSeen) { return } - const observer = new IntersectionObserver(handleIntersection) + const observer = new IntersectionObserver( + handleIntersection, + ON_ITEM_SEEN_INTERSECTION_OPTS, + ) const row: Element | null = rowRef.current! observer.observe(row) return () => { From 94779cb72bb8dfb89db2a95756a6e389a64fe72e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 6 May 2024 22:46:02 +0100 Subject: [PATCH 16/25] Fix non-reactive debounce --- src/state/feed-feedback.tsx | 47 ++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index bca57ec28f..70b436fdea 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -28,31 +28,30 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { const queue = React.useRef>(new Set()) const history = React.useRef>(new Set()) - const [sendToFeed] = React.useState(() => - debounce( - () => { - const proxyAgent = getAgent().withProxy( - // @ts-ignore TODO need to update withProxy() to support this key -prf - 'bsky_fg', - // TODO when we start sending to other feeds, we need to grab their DID -prf - 'did:web:discover.bsky.app', - ) as BskyAgent - proxyAgent.app.bsky.feed - .sendInteractions({ - interactions: Array.from(queue.current).map(toInteraction), - }) - .catch((e: any) => { - logger.warn('Failed to send feed interactions', {error: e}) - }) + const sendToFeedNotDebounced = React.useCallback(() => { + const proxyAgent = getAgent().withProxy( + // @ts-ignore TODO need to update withProxy() to support this key -prf + 'bsky_fg', + // TODO when we start sending to other feeds, we need to grab their DID -prf + 'did:web:discover.bsky.app', + ) as BskyAgent + proxyAgent.app.bsky.feed + .sendInteractions({ + interactions: Array.from(queue.current).map(toInteraction), + }) + .catch((e: any) => { + logger.warn('Failed to send feed interactions', {error: e}) + }) + + for (const v of queue.current) { + history.current.add(v) + } + queue.current.clear() + }, [getAgent]) - for (const v of queue.current) { - history.current.add(v) - } - queue.current.clear() - }, - 15e3, - {maxWait: 60e3}, - ), + const sendToFeed = React.useMemo( + () => debounce(sendToFeedNotDebounced, 15e3, {maxWait: 60e3}), + [sendToFeedNotDebounced], ) React.useEffect(() => { From 03a023531ff06da6e07a282edd43e796ef5587a4 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 6 May 2024 22:49:51 +0100 Subject: [PATCH 17/25] Move methods out --- src/state/feed-feedback.tsx | 89 ++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 70b436fdea..a7a26437d8 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -66,55 +66,64 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { return () => sub.remove() }, [enabled, sendToFeed]) + const onItemSeen = React.useCallback( + (slice: any) => { + if (!enabled) { + return + } + if (!isFeedPostSlice(slice)) { + return + } + for (const postItem of slice.items) { + const str = toString({ + item: postItem.uri, + event: 'app.bsky.feed.defs#interactionSeen', + feedContext: postItem.feedContext, + }) + if (!history.current.has(str)) { + queue.current.add(str) + sendToFeed() + } + } + }, + [enabled, sendToFeed], + ) + + const sendInteraction = React.useCallback( + (interaction: AppBskyFeedDefs.Interaction) => { + if (!enabled) { + return + } + const str = toString(interaction) + if (!history.current.has(str)) { + queue.current.add(str) + sendToFeed() + } + }, + [enabled, sendToFeed], + ) + + const flushAndReset = React.useCallback(() => { + if (!enabled) { + return + } + sendToFeed.flush() + history.current.clear() + }, [enabled, sendToFeed]) + return React.useMemo(() => { return { enabled, - // pass this method to the onItemSeen - onItemSeen: (slice: any) => { - if (!enabled) { - return - } - if (!isFeedPostSlice(slice)) { - return - } - for (const postItem of slice.items) { - const str = toString({ - item: postItem.uri, - event: 'app.bsky.feed.defs#interactionSeen', - feedContext: postItem.feedContext, - }) - if (!history.current.has(str)) { - queue.current.add(str) - sendToFeed() - } - } - }, - + onItemSeen, // call on various events // queues the event to be sent with the debounced sendToFeed call - sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => { - if (!enabled) { - return - } - const str = toString(interaction) - if (!history.current.has(str)) { - queue.current.add(str) - sendToFeed() - } - }, - + sendInteraction, // call on feed refresh // immediately sends all queued events and clears the history tracker - flushAndReset: () => { - if (!enabled) { - return - } - sendToFeed.flush() - history.current.clear() - }, + flushAndReset, } - }, [enabled, queue, sendToFeed]) + }, [enabled, onItemSeen, sendInteraction, flushAndReset]) } export const FeedFeedbackProvider = stateContext.Provider From 0b65b915cdcf7ffe83658159ed67505fc139ab0f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 6 May 2024 23:18:34 +0100 Subject: [PATCH 18/25] Use a WeakSet for deduping --- src/state/feed-feedback.tsx | 60 ++++++++++++++++++-------------- src/view/com/feeds/FeedPage.tsx | 4 +-- src/view/com/posts/Feed.tsx | 2 +- src/view/screens/ProfileFeed.tsx | 2 +- 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index a7a26437d8..08b0242af5 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -5,28 +5,36 @@ import debounce from 'lodash.debounce' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {logger} from '#/logger' -import {FeedDescriptor, isFeedPostSlice} from '#/state/queries/post-feed' +import { + FeedDescriptor, + FeedPostSliceItem, + isFeedPostSlice, +} from '#/state/queries/post-feed' import {useAgent} from './session' type StateContext = { enabled: boolean onItemSeen: (item: any) => void sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void - flushAndReset: () => void + flush: () => void } const stateContext = React.createContext({ enabled: false, onItemSeen: (_item: any) => {}, sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, - flushAndReset: () => {}, + flush: () => {}, }) export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { const {getAgent} = useAgent() const enabled = isDiscoverFeed(feed) && hasSession const queue = React.useRef>(new Set()) - const history = React.useRef>(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 + >(new WeakSet()) const sendToFeedNotDebounced = React.useCallback(() => { const proxyAgent = getAgent().withProxy( @@ -35,18 +43,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { // TODO when we start sending to other feeds, we need to grab their DID -prf 'did:web:discover.bsky.app', ) as BskyAgent + + const interactions = Array.from(queue.current).map(toInteraction) + queue.current.clear() + proxyAgent.app.bsky.feed - .sendInteractions({ - interactions: Array.from(queue.current).map(toInteraction), - }) + .sendInteractions({interactions}) .catch((e: any) => { logger.warn('Failed to send feed interactions', {error: e}) }) - - for (const v of queue.current) { - history.current.add(v) - } - queue.current.clear() }, [getAgent]) const sendToFeed = React.useMemo( @@ -75,13 +80,15 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { return } for (const postItem of slice.items) { - const str = toString({ - item: postItem.uri, - event: 'app.bsky.feed.defs#interactionSeen', - feedContext: postItem.feedContext, - }) - if (!history.current.has(str)) { - queue.current.add(str) + if (!history.current.has(postItem)) { + history.current.add(postItem) + queue.current.add( + toString({ + item: postItem.uri, + event: 'app.bsky.feed.defs#interactionSeen', + feedContext: postItem.feedContext, + }), + ) sendToFeed() } } @@ -94,21 +101,20 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { if (!enabled) { return } - const str = toString(interaction) - if (!history.current.has(str)) { - queue.current.add(str) + if (!history.current.has(interaction)) { + history.current.add(interaction) + queue.current.add(toString(interaction)) sendToFeed() } }, [enabled, sendToFeed], ) - const flushAndReset = React.useCallback(() => { + const flush = React.useCallback(() => { if (!enabled) { return } sendToFeed.flush() - history.current.clear() }, [enabled, sendToFeed]) return React.useMemo(() => { @@ -120,10 +126,10 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { // queues the event to be sent with the debounced sendToFeed call sendInteraction, // call on feed refresh - // immediately sends all queued events and clears the history tracker - flushAndReset, + // immediately sends all queued events + flush, } - }, [enabled, onItemSeen, sendInteraction, flushAndReset]) + }, [enabled, onItemSeen, sendInteraction, flush]) } export const FeedFeedbackProvider = stateContext.Provider diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 10c8f946cf..36986bd59b 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -70,7 +70,7 @@ export function FeedPage({ getTabState(getRootNavigation(navigation).getState(), 'Home') === TabState.InsideAtRoot if (isScreenFocused && isPageFocused) { - feedFeedback.flushAndReset() + feedFeedback.flush() scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) @@ -105,7 +105,7 @@ export function FeedPage({ }, [openComposer, track]) const onPressLoadLatest = React.useCallback(() => { - feedFeedback.flushAndReset() + feedFeedback.flush() scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 7aba838f40..11c2029609 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -226,7 +226,7 @@ let Feed = ({ setIsPTRing(true) try { await refetch() - feedFeedback.flushAndReset() + feedFeedback.flush() onHasNew?.(false) } catch (err) { logger.error('Failed to refresh posts feed', {message: err}) diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 4908a6b71e..4d8b1fd6c8 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -468,7 +468,7 @@ const FeedSection = React.forwardRef( const feedFeedback = useFeedFeedback(feed, hasSession) const onScrollToTop = useCallback(() => { - feedFeedback.flushAndReset() + feedFeedback.flush() scrollElRef.current?.scrollToOffset({ animated: isNative, offset: -headerHeight, From 87c53e01f00ae64053fd05d404c25332995a3694 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 6 May 2024 23:34:11 +0100 Subject: [PATCH 19/25] Reset timeout --- src/view/com/util/List.web.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 47121856a3..b062779695 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -407,11 +407,13 @@ let Row = function RowImpl({ if (entry.isIntersecting) { if (!intersectionTimeout.current) { intersectionTimeout.current = setTimeout(() => { + intersectionTimeout.current = undefined onItemSeen!(item) }, ON_ITEM_SEEN_WAIT_DURATION) } } else { if (intersectionTimeout.current) { + intersectionTimeout.current = undefined clearTimeout(intersectionTimeout.current) } } From 20ce8667af35444a555efee5237244311eae3516 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 6 May 2024 23:37:40 +0100 Subject: [PATCH 20/25] 3 -> 2 seconds --- src/view/com/util/List.tsx | 2 +- src/view/com/util/List.web.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index e05592a20e..194f81c5c1 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -89,7 +89,7 @@ function ListImpl( }, { itemVisiblePercentThreshold: 40, - minimumViewTime: 3e3, + minimumViewTime: 2e3, }, ] }, [onItemSeen]) diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index b062779695..df9f0686d6 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -26,7 +26,7 @@ export type ListProps = Omit< } export type ListRef = React.MutableRefObject // TODO: Better types. -const ON_ITEM_SEEN_WAIT_DURATION = 3e3 // post must be "seen" 3 seconds before capturing +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" From 2ae15d5e54b3cf86c6d19c3cb846118fdbaa6de4 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 6 May 2024 23:50:52 +0100 Subject: [PATCH 21/25] Oopsie --- src/view/com/util/List.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index df9f0686d6..b6ecf02ec8 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -413,8 +413,8 @@ let Row = function RowImpl({ } } else { if (intersectionTimeout.current) { - intersectionTimeout.current = undefined clearTimeout(intersectionTimeout.current) + intersectionTimeout.current = undefined } } }) From fd1b66334d55eb8d023d947bd907dc4ebfc45874 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 7 May 2024 00:20:30 +0100 Subject: [PATCH 22/25] Throttle instead --- package.json | 2 ++ src/state/feed-feedback.tsx | 14 +++++++++----- yarn.lock | 19 +++++++------------ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 39b90b3c32..62e5a50371 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@tiptap/react": "^2.0.0-beta.220", "@tiptap/suggestion": "^2.0.0-beta.220", "@types/invariant": "^2.2.37", + "@types/lodash.throttle": "^4.1.9", "@types/node": "^18.16.2", "@zxing/text-encoding": "^0.9.0", "array.prototype.findlast": "^1.2.3", @@ -149,6 +150,7 @@ "lodash.samplesize": "^4.2.0", "lodash.set": "^4.3.2", "lodash.shuffle": "^4.2.0", + "lodash.throttle": "^4.1.1", "mobx": "^6.6.1", "mobx-react-lite": "^3.4.0", "mobx-utils": "^6.0.6", diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 08b0242af5..99420a2e4f 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -1,7 +1,7 @@ import React from 'react' import {AppState, AppStateStatus} from 'react-native' import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' -import debounce from 'lodash.debounce' +import throttle from 'lodash.throttle' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {logger} from '#/logger' @@ -36,7 +36,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { WeakSet >(new WeakSet()) - const sendToFeedNotDebounced = React.useCallback(() => { + const sendToFeedNoDelay = React.useCallback(() => { const proxyAgent = getAgent().withProxy( // @ts-ignore TODO need to update withProxy() to support this key -prf 'bsky_fg', @@ -55,8 +55,12 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { }, [getAgent]) const sendToFeed = React.useMemo( - () => debounce(sendToFeedNotDebounced, 15e3, {maxWait: 60e3}), - [sendToFeedNotDebounced], + () => + throttle(sendToFeedNoDelay, 15e3, { + leading: false, + trailing: true, + }), + [sendToFeedNoDelay], ) React.useEffect(() => { @@ -123,7 +127,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { // pass this method to the onItemSeen onItemSeen, // call on various events - // queues the event to be sent with the debounced sendToFeed call + // queues the event to be sent with the throttled sendToFeed call sendInteraction, // call on feed refresh // immediately sends all queued events diff --git a/yarn.lock b/yarn.lock index d56738c6f3..536ea1c8f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,18 +70,6 @@ multiformats "^9.9.0" tlds "^1.234.0" -"@atproto/api@^0.12.3": - version "0.12.3" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.3.tgz#5b7b1c7d4210ee9315961504900c8409395cbb17" - integrity sha512-y/kGpIEo+mKGQ7VOphpqCAigTI0LZRmDThNChTfSzDKm9TzEobwiw0zUID0Yw6ot1iLLFx3nKURmuZAYlEuobw== - dependencies: - "@atproto/common-web" "^0.3.0" - "@atproto/lexicon" "^0.4.0" - "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.5.0" - multiformats "^9.9.0" - tlds "^1.234.0" - "@atproto/aws@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.0.tgz#17f3faf744824457cabd62f87be8bf08cacf8029" @@ -7693,6 +7681,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.throttle@^4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz#f17a6ae084f7c0117bd7df145b379537bc9615c5" + integrity sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g== + dependencies: + "@types/lodash" "*" + "@types/lodash@*": version "4.14.197" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.197.tgz#e95c5ddcc814ec3e84c891910a01e0c8a378c54b" From 05f605613bfa9f6e5408129655aeec9db4bfcbdc Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 7 May 2024 02:35:25 +0100 Subject: [PATCH 23/25] Fix divider --- src/view/com/util/forms/PostDropdownBtn.tsx | 57 ++++++++++----------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 74c3995ab6..7a62ce7cbc 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -17,7 +17,7 @@ import {CommonNavigatorParams} from '#/lib/routes/types' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {getTranslatorLink} from '#/locale/helpers' import {logger} from '#/logger' -import {isNative, isWeb} from '#/platform/detection' +import {isWeb} from '#/platform/detection' import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' @@ -36,11 +36,11 @@ import {EmbedDialog} from '#/components/dialogs/Embed' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' 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 {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' 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' @@ -288,37 +288,33 @@ let PostDropdownBtn = ({ )} - {hasSession && ( + {hasSession && feedFeedback.enabled && ( <> + - {!isNative && } - - {feedFeedback.enabled && ( - <> - - - {_(msg`Show more like this`)} - - - - - - - {_(msg`Show less like this`)} - - - - - - - )} + + {_(msg`Show more like this`)} + + + + + {_(msg`Show less like this`)} + + + + + )} + {hasSession && ( + <> + + - {!isAuthor && ( Date: Tue, 7 May 2024 02:39:37 +0100 Subject: [PATCH 24/25] Remove explicit flush calls --- src/state/feed-feedback.tsx | 14 +------------- src/view/com/feeds/FeedPage.tsx | 14 ++------------ src/view/com/posts/Feed.tsx | 3 +-- src/view/screens/ProfileFeed.tsx | 3 +-- 4 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 99420a2e4f..5bfc77d0a5 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -16,14 +16,12 @@ type StateContext = { enabled: boolean onItemSeen: (item: any) => void sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void - flush: () => void } const stateContext = React.createContext({ enabled: false, onItemSeen: (_item: any) => {}, sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {}, - flush: () => {}, }) export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { @@ -114,13 +112,6 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { [enabled, sendToFeed], ) - const flush = React.useCallback(() => { - if (!enabled) { - return - } - sendToFeed.flush() - }, [enabled, sendToFeed]) - return React.useMemo(() => { return { enabled, @@ -129,11 +120,8 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { // call on various events // queues the event to be sent with the throttled sendToFeed call sendInteraction, - // call on feed refresh - // immediately sends all queued events - flush, } - }, [enabled, onItemSeen, sendInteraction, flush]) + }, [enabled, onItemSeen, sendInteraction]) } export const FeedFeedbackProvider = stateContext.Provider diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 36986bd59b..bb782809df 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -70,7 +70,6 @@ export function FeedPage({ getTabState(getRootNavigation(navigation).getState(), 'Home') === TabState.InsideAtRoot if (isScreenFocused && isPageFocused) { - feedFeedback.flush() scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) @@ -80,15 +79,7 @@ export function FeedPage({ reason: 'soft-reset', }) } - }, [ - navigation, - isPageFocused, - scrollToTop, - queryClient, - feed, - setHasNew, - feedFeedback, - ]) + }, [navigation, isPageFocused, scrollToTop, queryClient, feed, setHasNew]) // fires when page within screen is activated/deactivated React.useEffect(() => { @@ -105,7 +96,6 @@ export function FeedPage({ }, [openComposer, track]) const onPressLoadLatest = React.useCallback(() => { - feedFeedback.flush() scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) @@ -114,7 +104,7 @@ export function FeedPage({ feedUrl: feed, reason: 'load-latest', }) - }, [scrollToTop, feed, queryClient, setHasNew, feedFeedback]) + }, [scrollToTop, feed, queryClient, setHasNew]) const isDiscoverFeed = feed === diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 11c2029609..8969f7cd2c 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -226,13 +226,12 @@ let Feed = ({ setIsPTRing(true) try { await refetch() - feedFeedback.flush() onHasNew?.(false) } catch (err) { logger.error('Failed to refresh posts feed', {message: err}) } setIsPTRing(false) - }, [refetch, track, setIsPTRing, onHasNew, feed, feedType, feedFeedback]) + }, [refetch, track, setIsPTRing, onHasNew, feed, feedType]) const onEndReached = React.useCallback(async () => { if (isFetching || !hasNextPage || isError) return diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 4d8b1fd6c8..b9042ff327 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -468,14 +468,13 @@ const FeedSection = React.forwardRef( const feedFeedback = useFeedFeedback(feed, hasSession) const onScrollToTop = useCallback(() => { - feedFeedback.flush() scrollElRef.current?.scrollToOffset({ animated: isNative, offset: -headerHeight, }) truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) - }, [scrollElRef, headerHeight, queryClient, feed, setHasNew, feedFeedback]) + }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) React.useImperativeHandle(ref, () => ({ scrollToTop: onScrollToTop, From a0a60572260d7fb145bac8d9325f8d10158f547d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 7 May 2024 02:48:44 +0100 Subject: [PATCH 25/25] Rm unused --- src/view/com/util/UserPreviewLink.tsx | 34 --------------------------- 1 file changed, 34 deletions(-) delete mode 100644 src/view/com/util/UserPreviewLink.tsx diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx deleted file mode 100644 index eea3bb5f97..0000000000 --- a/src/view/com/util/UserPreviewLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import {StyleProp, ViewStyle} from 'react-native' - -import {usePrefetchProfileQuery} from '#/state/queries/profile' -import {makeProfileLink} from 'lib/routes/links' -import {isWeb} from 'platform/detection' -import {Link} from './Link' - -interface UserPreviewLinkProps { - did: string - handle: string - onBeforePress?: () => void - style?: StyleProp -} -export function UserPreviewLink( - props: React.PropsWithChildren, -) { - const prefetchProfileQuery = usePrefetchProfileQuery() - return ( - { - if (isWeb) { - prefetchProfileQuery(props.did) - } - }} - href={makeProfileLink(props)} - title={props.handle} - asAnchor - onBeforePress={props.onBeforePress} - style={props.style}> - {props.children} - - ) -}