diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index f533d7cb0d..df7ee6923f 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1055,11 +1055,6 @@ "count": 1 } }, - "src/screens/Profile/components/ProfileFeedHeader.tsx": { - "typescript/no-misused-promises": { - "count": 5 - } - }, "src/screens/ProfileList/FeedSection.tsx": { "typescript/no-floating-promises": { "count": 1 @@ -1406,11 +1401,6 @@ "count": 2 } }, - "src/state/queries/feed.ts": { - "typescript/no-floating-promises": { - "count": 1 - } - }, "src/state/queries/handle.ts": { "typescript/no-floating-promises": { "count": 1 @@ -1756,11 +1746,6 @@ "count": 2 } }, - "src/view/com/posts/PostFeed.tsx": { - "typescript/no-explicit-any": { - "count": 1 - } - }, "src/view/com/posts/PostFeedErrorMessage.tsx": { "typescript/no-explicit-any": { "count": 1 diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 91d1319c6b..80d8639ff2 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -146,6 +146,9 @@ export const BSKY_FEED_OWNER_DIDS = [ 'did:plc:q6gjnaw2blty4crticxkmujt', ] +export const TRENDING_DID = 'did:plc:qrz3lhbyuxbeilrc6nekdqme' +export const TRENDING_HANDLE = 'trending.bsky.app' + export const DISCOVER_FEED_URI = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' export const VIDEO_FEED_URI = diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx index 6ed5f4b5b6..69f743c03a 100644 --- a/src/screens/Profile/ProfileFeed/index.tsx +++ b/src/screens/Profile/ProfileFeed/index.tsx @@ -1,13 +1,12 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {useAnimatedRef} from 'react-native-reanimated' import {AppBskyFeedDefs} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' +import {useLingui} from '@lingui/react/macro' import {useIsFocused} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' -import {VIDEO_FEED_URIS} from '#/lib/constants' +import {TRENDING_DID, TRENDING_HANDLE, VIDEO_FEED_URIS} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useSetTitle} from '#/lib/hooks/useSetTitle' import {type CommonNavigatorParams} from '#/lib/routes/types' @@ -52,7 +51,7 @@ export function ProfileFeedScreen(props: Props) { const feedParams: FeedParams | undefined = props.route.params.feedCacheKey ? {feedCacheKey: props.route.params.feedCacheKey} : undefined - const {_} = useLingui() + const {t: l} = useLingui() const uri = useMemo( () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), @@ -70,7 +69,7 @@ export function ProfileFeedScreen(props: Props) { void refetch()} /> @@ -131,7 +130,7 @@ export function ProfileFeedScreenInner({ feedInfo: FeedSourceFeedInfo feedParams: FeedParams | undefined }) { - const {_} = useLingui() + const {t: l} = useLingui() const {hasSession} = useSession() const {openComposer} = useOpenComposer() const isScreenFocused = useIsFocused() @@ -168,10 +167,10 @@ export function ProfileFeedScreenInner({ ) - }, [_]) + }, [l]) const isVideoFeed = useMemo(() => { const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri) @@ -181,13 +180,17 @@ export function ProfileFeedScreenInner({ return IS_NATIVE && _isVideoFeed }, [feedInfo]) + const isTrending = + feedInfo.creatorDid.toLowerCase() === TRENDING_DID || + feedInfo.creatorHandle.toLowerCase() === TRENDING_HANDLE + return ( <> - - + - {(isScrolledDown || hasNew) && ( )} - {hasSession && ( openComposer({logContext: 'Fab'})} icon={} accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} + accessibilityLabel={l`New post`} accessibilityHint="" /> )} diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx index c640c84f33..4cfe838e13 100644 --- a/src/screens/Profile/components/ProfileFeedHeader.tsx +++ b/src/screens/Profile/components/ProfileFeedHeader.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native' import {AtUri} from '@atproto/api' import {Plural, Trans, useLingui} from '@lingui/react/macro' +import {TRENDING_HANDLE} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {makeCustomFeedLink, makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' @@ -24,20 +25,20 @@ import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' -import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' +import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' +import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' import { - Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, - Heart2_Stroke2_Corner0_Rounded as Heart, + Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilledIcon, + Heart2_Stroke2_Corner0_Rounded as HeartIcon, } from '#/components/icons/Heart2' import { - Pin_Filled_Corner0_Rounded as PinFilled, - Pin_Stroke2_Corner0_Rounded as Pin, + Pin_Filled_Corner0_Rounded as PinFilledIcon, + Pin_Stroke2_Corner0_Rounded as PinIcon, } from '#/components/icons/Pin' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' +import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' import * as Layout from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import * as Menu from '#/components/Menu' @@ -74,14 +75,20 @@ export function ProfileFeedHeaderSkeleton() { width: 34, }, ]}> - + ) } -export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { +export function ProfileFeedHeader({ + info, + isTrending, +}: { + info: FeedSourceFeedInfo + isTrending: boolean +}) { const t = useTheme() const {t: l, i18n} = useLingui() const ax = useAnalytics() @@ -188,105 +195,143 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { - + + ) : ( + + + + + )} + + )} - {hasSession && ( + {!isTrending && hasSession ? ( {isPinned ? ( @@ -300,7 +345,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { variant="ghost" shape="square" color="secondary"> - + ) }} @@ -310,23 +358,23 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { + onPress={() => void onTogglePinned()}> {l`Unpin from home`} - + + onPress={() => void onToggleSaved()}> {isSaved ? l`Remove from my feeds` : l`Save to my feeds`} @@ -339,12 +387,12 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { variant="ghost" shape="square" color="secondary" - onPress={onTogglePinned}> - + onPress={() => void onTogglePinned()}> + )} - )} + ) : null} @@ -358,7 +406,8 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { setLikeUri={setLikeUri} likeCount={likeCount} isPinned={isPinned} - onTogglePinned={onTogglePinned} + isTrending={isTrending} + onTogglePinned={() => void onTogglePinned()} isFeedStateChangePending={isFeedStateChangePending} /> @@ -373,6 +422,7 @@ function DialogInner({ setLikeUri, likeCount, isPinned, + isTrending, onTogglePinned, isFeedStateChangePending, }: { @@ -381,6 +431,7 @@ function DialogInner({ setLikeUri: (uri: string) => void likeCount: number isPinned: boolean + isTrending: boolean onTogglePinned: () => void isFeedStateChangePending: boolean }) { @@ -459,7 +510,9 @@ function DialogInner({ style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]} numberOfLines={1} onPress={() => control.close()}> - {sanitizeHandle(info.creatorHandle, '@')} + {info.creatorHandle === TRENDING_HANDLE + ? l`Bluesky` + : sanitizeHandle(info.creatorHandle, '@')} @@ -472,12 +525,13 @@ function DialogInner({ color="secondary" shape="round" onPress={onPressShare}> - + - - {typeof likeCount === 'number' && ( + + {typeof likeCount === 'number' && likeCount > 0 ? ( + - )} - - {hasSession && ( + + ) : null} + {hasSession ? ( <> - - - - + + {isLiked ? Unlike : Like} + + + + + ) : null} @@ -541,7 +601,7 @@ function DialogInner({ Report feed - + @@ -556,7 +616,7 @@ function DialogInner({ )} - )} + ) : null} ) } diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index c0eb25f54a..c3ac1acd53 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -327,7 +327,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { count += page.feeds.length } if (count < limit && (data?.pages.length || 0) < 6) { - query.fetchNextPage() + void query.fetchNextPage() lastPageCountRef.current = data?.pages?.length || 0 } }, [query, limit]) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 2c947ff3ec..bc40880bf4 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -17,6 +17,7 @@ import { AppBskyEmbedImages, AppBskyEmbedVideo, type AppBskyFeedDefs, + type RichText as RichTextType, } from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' @@ -50,7 +51,7 @@ import {List, type ListRef} from '#/view/com/util/List' import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types' -import {useBreakpoints, useLayoutBreakpoints} from '#/alf' +import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf' import { AgeAssuranceDismissibleFeedBanner, useInternalState as useAgeAssuranceBannerState, @@ -64,6 +65,7 @@ import {FeedTrendingTopicsInterstitial} from '#/components/interstitials/FeedTre import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' +import {RichText} from '#/components/RichText' import {useAnalytics} from '#/analytics' import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' @@ -105,6 +107,11 @@ type FeedRow = type: 'fallbackMarker' key: string } + | { + type: 'description' + key: string + value: RichTextType + } | { type: 'sliceItem' key: string @@ -194,6 +201,7 @@ const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY let PostFeed = ({ feed, + description, feedParams, ignoreFilterFor, style, @@ -216,6 +224,7 @@ let PostFeed = ({ isVideoFeed = false, }: { feed: FeedDescriptor + description?: RichTextType feedParams?: FeedParams ignoreFilterFor?: string style?: StyleProp @@ -232,13 +241,14 @@ let PostFeed = ({ progressViewOffset?: number desktopFixedHeightOffset?: number ListHeaderComponent?: () => React.ReactElement - extraData?: any + extraData?: Record savedFeedConfig?: AppBskyActorDefs.SavedFeed initialNumToRender?: number isVideoFeed?: boolean lastFetchDate?: () => number }): React.ReactNode => { const ax = useAnalytics() + const t = useTheme() const {t: l} = useLingui() const queryClient = useQueryClient() const {currentAccount, hasSession} = useSession() @@ -389,6 +399,7 @@ let PostFeed = ({ * Cached value of whether the current feed was selected at startup. We don't * want this to update when user swipes. */ + // oxlint-disable-next-line react/hook-use-state const [isCurrentFeedAtStartupSelected] = useState(selectedFeed === feed) const blockedOrMutedAuthors = usePostAuthorShadowFilter( @@ -680,8 +691,17 @@ let PostFeed = ({ } } + if (description?.text) { + arr.unshift({ + key: 'description', + type: 'description', + value: description, + }) + } + return arr }, [ + description, isFetched, isError, isEmpty, @@ -793,6 +813,18 @@ let PostFeed = ({ return } else if (row.type === 'feedShutdownMsg') { return + } else if (row.type === 'description') { + return ( + + ) } else if (row.type === 'interstitialFollows') { return } else if (row.type === 'interstitialProgressGuide') { @@ -893,6 +925,7 @@ let PostFeed = ({ feedTab, feedCacheKey, onPressShowLess, + t, ], )