An ATproto social media client -- with an independent Appview.

Live via service config (#8378)

* add config (with temp config)

* only allow whitelisted domains in form

* move config to generic config

* use array-based config

* update deps

* rm expect-error

authored by samuel.fm and committed by

GitHub 1cdbfc70 75ffb3d2

+190 -153
+2 -2
package.json
··· 69 "icons:optimize": "svgo -f ./assets/icons" 70 }, 71 "dependencies": { 72 - "@atproto/api": "^0.15.6", 73 "@bitdrift/react-native": "^0.6.8", 74 "@braintree/sanitize-url": "^6.0.2", 75 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", ··· 219 "zod": "^3.20.2" 220 }, 221 "devDependencies": { 222 - "@atproto/dev-env": "^0.3.129", 223 "@babel/core": "^7.26.0", 224 "@babel/preset-env": "^7.26.0", 225 "@babel/runtime": "^7.26.0",
··· 69 "icons:optimize": "svgo -f ./assets/icons" 70 }, 71 "dependencies": { 72 + "@atproto/api": "^0.15.7", 73 "@bitdrift/react-native": "^0.6.8", 74 "@braintree/sanitize-url": "^6.0.2", 75 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", ··· 219 "zod": "^3.20.2" 220 }, 221 "devDependencies": { 222 + "@atproto/dev-env": "^0.3.131", 223 "@babel/core": "^7.26.0", 224 "@babel/preset-env": "^7.26.0", 225 "@babel/runtime": "^7.26.0",
+3 -3
src/App.native.tsx
··· 43 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 44 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' 45 import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' 46 import { 47 Provider as SessionProvider, 48 type SessionAccount, ··· 57 import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 58 import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 59 import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' 60 - import {Provider as TrendingConfigProvider} from '#/state/trending-config' 61 import {TestCtrls} from '#/view/com/testing/TestCtrls' 62 import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' 63 import * as Toast from '#/view/com/util/Toast' ··· 149 <BackgroundNotificationPreferencesProvider> 150 <MutedThreadsProvider> 151 <ProgressGuideProvider> 152 - <TrendingConfigProvider> 153 <GestureHandlerRootView 154 style={s.h100pct}> 155 <IntentDialogProvider> ··· 158 <NuxDialogs /> 159 </IntentDialogProvider> 160 </GestureHandlerRootView> 161 - </TrendingConfigProvider> 162 </ProgressGuideProvider> 163 </MutedThreadsProvider> 164 </BackgroundNotificationPreferencesProvider>
··· 43 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 44 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' 45 import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' 46 + import {Provider as ServiceAccountManager} from '#/state/service-config' 47 import { 48 Provider as SessionProvider, 49 type SessionAccount, ··· 58 import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 59 import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 60 import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' 61 import {TestCtrls} from '#/view/com/testing/TestCtrls' 62 import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' 63 import * as Toast from '#/view/com/util/Toast' ··· 149 <BackgroundNotificationPreferencesProvider> 150 <MutedThreadsProvider> 151 <ProgressGuideProvider> 152 + <ServiceAccountManager> 153 <GestureHandlerRootView 154 style={s.h100pct}> 155 <IntentDialogProvider> ··· 158 <NuxDialogs /> 159 </IntentDialogProvider> 160 </GestureHandlerRootView> 161 + </ServiceAccountManager> 162 </ProgressGuideProvider> 163 </MutedThreadsProvider> 164 </BackgroundNotificationPreferencesProvider>
+3 -3
src/App.web.tsx
··· 33 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 34 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' 35 import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' 36 import { 37 Provider as SessionProvider, 38 type SessionAccount, ··· 47 import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 48 import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 49 import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' 50 - import {Provider as TrendingConfigProvider} from '#/state/trending-config' 51 import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' 52 import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' 53 import * as Toast from '#/view/com/util/Toast' ··· 130 <MutedThreadsProvider> 131 <SafeAreaProvider> 132 <ProgressGuideProvider> 133 - <TrendingConfigProvider> 134 <IntentDialogProvider> 135 <Shell /> 136 <NuxDialogs /> 137 </IntentDialogProvider> 138 - </TrendingConfigProvider> 139 </ProgressGuideProvider> 140 </SafeAreaProvider> 141 </MutedThreadsProvider>
··· 33 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' 34 import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts' 35 import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' 36 + import {Provider as ServiceConfigProvider} from '#/state/service-config' 37 import { 38 Provider as SessionProvider, 39 type SessionAccount, ··· 48 import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' 49 import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 50 import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' 51 import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' 52 import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' 53 import * as Toast from '#/view/com/util/Toast' ··· 130 <MutedThreadsProvider> 131 <SafeAreaProvider> 132 <ProgressGuideProvider> 133 + <ServiceConfigProvider> 134 <IntentDialogProvider> 135 <Shell /> 136 <NuxDialogs /> 137 </IntentDialogProvider> 138 + </ServiceConfigProvider> 139 </ProgressGuideProvider> 140 </SafeAreaProvider> 141 </MutedThreadsProvider>
+1 -1
src/components/interstitials/Trending.tsx
··· 9 useTrendingSettingsApi, 10 } from '#/state/preferences/trending' 11 import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' 12 - import {useTrendingConfig} from '#/state/trending-config' 13 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 14 import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' 15 import {atoms as a, useGutters, useTheme} from '#/alf'
··· 9 useTrendingSettingsApi, 10 } from '#/state/preferences/trending' 11 import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' 12 + import {useTrendingConfig} from '#/state/service-config' 13 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 14 import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' 15 import {atoms as a, useGutters, useTheme} from '#/alf'
+18 -3
src/components/live/GoLiveDialog.tsx
··· 10 import {toNiceDomain} from '#/lib/strings/url-helpers' 11 import {definitelyUrl} from '#/lib/strings/url-helpers' 12 import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 - import {useAgent} from '#/state/session' 14 import {useTickEveryMinute} from '#/state/shell' 15 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 16 import {atoms as a, ios, native, platform, useTheme, web} from '#/alf' ··· 58 const [duration, setDuration] = useState(60) 59 const moderationOpts = useModerationOpts() 60 const tick = useTickEveryMinute() 61 62 const time = useCallback( 63 (offset: number) => { ··· 79 80 const liveLinkUrl = definitelyUrl(liveLink) 81 const debouncedUrl = useDebouncedValue(liveLinkUrl, 500) 82 - const hasLink = !!debouncedUrl 83 84 const { 85 data: linkMeta, ··· 91 queryKey: ['link-meta', debouncedUrl], 92 queryFn: async () => { 93 if (!debouncedUrl) return null 94 return getLinkMeta(agent, debouncedUrl) 95 }, 96 }) ··· 100 isPending: isGoingLive, 101 error: goLiveError, 102 } = useUpsertLiveStatusMutation(duration, linkMeta) 103 104 return ( 105 <Dialog.ScrollableInner ··· 136 <TextField.LabelText> 137 <Trans>Live link</Trans> 138 </TextField.LabelText> 139 - <TextField.Root isInvalid={!!liveLinkError || !!linkMetaError}> 140 <TextField.Input 141 label={_(msg`Live link`)} 142 placeholder={_(msg`www.mylivestream.tv`)}
··· 10 import {toNiceDomain} from '#/lib/strings/url-helpers' 11 import {definitelyUrl} from '#/lib/strings/url-helpers' 12 import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 + import {useLiveNowConfig} from '#/state/service-config' 14 + import {useAgent, useSession} from '#/state/session' 15 import {useTickEveryMinute} from '#/state/shell' 16 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 17 import {atoms as a, ios, native, platform, useTheme, web} from '#/alf' ··· 59 const [duration, setDuration] = useState(60) 60 const moderationOpts = useModerationOpts() 61 const tick = useTickEveryMinute() 62 + const liveNowConfig = useLiveNowConfig() 63 + const {currentAccount} = useSession() 64 + 65 + const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did) 66 67 const time = useCallback( 68 (offset: number) => { ··· 84 85 const liveLinkUrl = definitelyUrl(liveLink) 86 const debouncedUrl = useDebouncedValue(liveLinkUrl, 500) 87 88 const { 89 data: linkMeta, ··· 95 queryKey: ['link-meta', debouncedUrl], 96 queryFn: async () => { 97 if (!debouncedUrl) return null 98 + if (!config) throw new Error(_(msg`You are not allowed to go live`)) 99 + 100 + const urlp = new URL(debouncedUrl) 101 + if (!config.domains.includes(urlp.hostname)) { 102 + throw new Error(_(msg`${urlp.hostname} is not a valid URL`)) 103 + } 104 + 105 return getLinkMeta(agent, debouncedUrl) 106 }, 107 }) ··· 111 isPending: isGoingLive, 112 error: goLiveError, 113 } = useUpsertLiveStatusMutation(duration, linkMeta) 114 + 115 + const isSourceInvalid = !!liveLinkError || !!linkMetaError 116 + 117 + const hasLink = !!debouncedUrl && !isSourceInvalid 118 119 return ( 120 <Dialog.ScrollableInner ··· 151 <TextField.LabelText> 152 <Trans>Live link</Trans> 153 </TextField.LabelText> 154 + <TextField.Root isInvalid={isSourceInvalid}> 155 <TextField.Input 156 label={_(msg`Live link`)} 157 placeholder={_(msg`www.mylivestream.tv`)}
-41
src/components/live/temp.ts
··· 1 - import {type AppBskyActorDefs, AppBskyEmbedExternal} from '@atproto/api' 2 - 3 - import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' 4 - import type * as bsky from '#/types/bsky' 5 - 6 - export const LIVE_DIDS: Record<string, true> = { 7 - 'did:plc:7sfnardo5xxznxc6esxc5ooe': true, // nba.com 8 - 'did:plc:gx6fyi3jcfxd7ammq2t7mzp2': true, // rtgame.bsky.social 9 - } 10 - 11 - export const LIVE_SOURCES: Record<string, true> = { 12 - 'nba.com': true, 13 - 'twitch.tv': true, 14 - } 15 - 16 - // TEMP: dumb gating 17 - export function temp__canBeLive(profile: bsky.profile.AnyProfileView) { 18 - if (__DEV__) 19 - return !!DISCOVER_DEBUG_DIDS[profile.did] || !!LIVE_DIDS[profile.did] 20 - return !!LIVE_DIDS[profile.did] 21 - } 22 - 23 - export function temp__canGoLive(profile: bsky.profile.AnyProfileView) { 24 - if (__DEV__) return true 25 - return !!LIVE_DIDS[profile.did] 26 - } 27 - 28 - // status must have a embed, and the embed must be an approved host for the status to be valid 29 - export function temp__isStatusValid(status: AppBskyActorDefs.StatusView) { 30 - if (status.status !== 'app.bsky.actor.status#live') return false 31 - try { 32 - if (AppBskyEmbedExternal.isView(status.embed)) { 33 - const url = new URL(status.embed.external.uri) 34 - return !!LIVE_SOURCES[url.hostname] 35 - } else { 36 - return false 37 - } 38 - } catch { 39 - return false 40 - } 41 - }
···
+28 -5
src/lib/actor-status.ts
··· 2 import { 3 type $Typed, 4 type AppBskyActorDefs, 5 - type AppBskyEmbedExternal, 6 } from '@atproto/api' 7 import {isAfter, parseISO} from 'date-fns' 8 9 import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' 10 import {useTickEveryMinute} from '#/state/shell' 11 - import {temp__canBeLive, temp__isStatusValid} from '#/components/live/temp' 12 import type * as bsky from '#/types/bsky' 13 14 export function useActorStatus(actor?: bsky.profile.AnyProfileView) { 15 const shadowed = useMaybeProfileShadow(actor) 16 const tick = useTickEveryMinute() 17 return useMemo(() => { 18 tick! // revalidate every minute 19 20 if ( 21 shadowed && 22 - temp__canBeLive(shadowed) && 23 'status' in shadowed && 24 shadowed.status && 25 - temp__isStatusValid(shadowed.status) && 26 isStatusStillActive(shadowed.status.expiresAt) 27 ) { 28 return { ··· 39 record: {}, 40 } satisfies AppBskyActorDefs.StatusView 41 } 42 - }, [shadowed, tick]) 43 } 44 45 export function isStatusStillActive(timeStr: string | undefined) { ··· 49 50 return isAfter(expiry, now) 51 }
··· 2 import { 3 type $Typed, 4 type AppBskyActorDefs, 5 + AppBskyEmbedExternal, 6 } from '@atproto/api' 7 import {isAfter, parseISO} from 'date-fns' 8 9 import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' 10 + import {useLiveNowConfig} from '#/state/service-config' 11 import {useTickEveryMinute} from '#/state/shell' 12 import type * as bsky from '#/types/bsky' 13 14 export function useActorStatus(actor?: bsky.profile.AnyProfileView) { 15 const shadowed = useMaybeProfileShadow(actor) 16 const tick = useTickEveryMinute() 17 + const config = useLiveNowConfig() 18 + 19 return useMemo(() => { 20 tick! // revalidate every minute 21 22 if ( 23 shadowed && 24 'status' in shadowed && 25 shadowed.status && 26 + validateStatus(shadowed.did, shadowed.status, config) && 27 isStatusStillActive(shadowed.status.expiresAt) 28 ) { 29 return { ··· 40 record: {}, 41 } satisfies AppBskyActorDefs.StatusView 42 } 43 + }, [shadowed, config, tick]) 44 } 45 46 export function isStatusStillActive(timeStr: string | undefined) { ··· 50 51 return isAfter(expiry, now) 52 } 53 + 54 + export function validateStatus( 55 + did: string, 56 + status: AppBskyActorDefs.StatusView, 57 + config: {did: string; domains: string[]}[], 58 + ) { 59 + if (status.status !== 'app.bsky.actor.status#live') return false 60 + const sources = config.find(cfg => cfg.did === did) 61 + if (!sources) { 62 + return false 63 + } 64 + try { 65 + if (AppBskyEmbedExternal.isView(status.embed)) { 66 + const url = new URL(status.embed.external.uri) 67 + return sources.domains.includes(url.hostname) 68 + } else { 69 + return false 70 + } 71 + } catch { 72 + return false 73 + } 74 + }
+1 -1
src/screens/Search/modules/ExploreRecommendations.tsx
··· 8 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT, 9 useTrendingTopics, 10 } from '#/state/queries/trending/useTrendingTopics' 11 - import {useTrendingConfig} from '#/state/trending-config' 12 import {atoms as a, useGutters, useTheme} from '#/alf' 13 import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag' 14 import {
··· 8 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT, 9 useTrendingTopics, 10 } from '#/state/queries/trending/useTrendingTopics' 11 + import {useTrendingConfig} from '#/state/service-config' 12 import {atoms as a, useGutters, useTheme} from '#/alf' 13 import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag' 14 import {
+1 -1
src/screens/Search/modules/ExploreTrendingTopics.tsx
··· 8 import {useModerationOpts} from '#/state/preferences/moderation-opts' 9 import {useTrendingSettings} from '#/state/preferences/trending' 10 import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' 11 - import {useTrendingConfig} from '#/state/trending-config' 12 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 13 import {formatCount} from '#/view/com/util/numeric/format' 14 import {atoms as a, useGutters, useTheme, type ViewStyleProp, web} from '#/alf'
··· 8 import {useModerationOpts} from '#/state/preferences/moderation-opts' 9 import {useTrendingSettings} from '#/state/preferences/trending' 10 import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' 11 + import {useTrendingConfig} from '#/state/service-config' 12 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 13 import {formatCount} from '#/view/com/util/numeric/format' 14 import {atoms as a, useGutters, useTheme, type ViewStyleProp, web} from '#/alf'
+1 -1
src/screens/Settings/ContentAndMediaSettings.tsx
··· 14 useTrendingSettings, 15 useTrendingSettingsApi, 16 } from '#/state/preferences/trending' 17 - import {useTrendingConfig} from '#/state/trending-config' 18 import * as SettingsList from '#/screens/Settings/components/SettingsList' 19 import * as Toggle from '#/components/forms/Toggle' 20 import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
··· 14 useTrendingSettings, 15 useTrendingSettingsApi, 16 } from '#/state/preferences/trending' 17 + import {useTrendingConfig} from '#/state/service-config' 18 import * as SettingsList from '#/screens/Settings/components/SettingsList' 19 import * as Toggle from '#/components/forms/Toggle' 20 import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
+6
src/state/queries/service-config.ts
··· 6 type ServiceConfig = { 7 checkEmailConfirmed: boolean 8 topicsEnabled: boolean 9 } 10 11 export function useServiceConfigQuery() { ··· 21 checkEmailConfirmed: Boolean(data.checkEmailConfirmed), 22 // @ts-expect-error not included in types atm 23 topicsEnabled: Boolean(data.topicsEnabled), 24 } 25 } catch (e) { 26 return { 27 checkEmailConfirmed: false, 28 topicsEnabled: false, 29 } 30 } 31 },
··· 6 type ServiceConfig = { 7 checkEmailConfirmed: boolean 8 topicsEnabled: boolean 9 + liveNow: { 10 + did: string 11 + domains: string[] 12 + }[] 13 } 14 15 export function useServiceConfigQuery() { ··· 25 checkEmailConfirmed: Boolean(data.checkEmailConfirmed), 26 // @ts-expect-error not included in types atm 27 topicsEnabled: Boolean(data.topicsEnabled), 28 + liveNow: data.liveNow ?? [], 29 } 30 } catch (e) { 31 return { 32 checkEmailConfirmed: false, 33 topicsEnabled: false, 34 + liveNow: [], 35 } 36 } 37 },
+88
src/state/service-config.tsx
···
··· 1 + import {createContext, useContext, useMemo} from 'react' 2 + 3 + import {useLanguagePrefs} from '#/state/preferences/languages' 4 + import {useServiceConfigQuery} from '#/state/queries/service-config' 5 + import {device} from '#/storage' 6 + 7 + type TrendingContext = { 8 + enabled: boolean 9 + } 10 + 11 + type LiveNowContext = { 12 + did: string 13 + domains: string[] 14 + }[] 15 + 16 + const TrendingContext = createContext<TrendingContext>({ 17 + enabled: false, 18 + }) 19 + 20 + const LiveNowContext = createContext<LiveNowContext | null>(null) 21 + 22 + export function Provider({children}: {children: React.ReactNode}) { 23 + const langPrefs = useLanguagePrefs() 24 + const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery() 25 + const trending = useMemo<TrendingContext>(() => { 26 + if (__DEV__) { 27 + return {enabled: true} 28 + } 29 + 30 + /* 31 + * Only English during beta period 32 + */ 33 + if ( 34 + !!langPrefs.contentLanguages.length && 35 + !langPrefs.contentLanguages.includes('en') 36 + ) { 37 + return {enabled: false} 38 + } 39 + 40 + /* 41 + * While loading, use cached value 42 + */ 43 + const cachedEnabled = device.get(['trendingBetaEnabled']) 44 + if (isInitialLoad) { 45 + return {enabled: Boolean(cachedEnabled)} 46 + } 47 + 48 + /* 49 + * Doing an extra check here to reduce hits to statsig. If it's disabled on 50 + * the server, we can exit early. 51 + */ 52 + const enabled = Boolean(config?.topicsEnabled) 53 + 54 + // update cache 55 + device.set(['trendingBetaEnabled'], enabled) 56 + 57 + return {enabled} 58 + }, [isInitialLoad, config, langPrefs.contentLanguages]) 59 + 60 + const liveNow = useMemo<LiveNowContext>(() => config?.liveNow ?? [], [config]) 61 + 62 + return ( 63 + <TrendingContext.Provider value={trending}> 64 + <LiveNowContext.Provider value={liveNow}> 65 + {children} 66 + </LiveNowContext.Provider> 67 + </TrendingContext.Provider> 68 + ) 69 + } 70 + 71 + export function useTrendingConfig() { 72 + return useContext(TrendingContext) 73 + } 74 + 75 + export function useLiveNowConfig() { 76 + const ctx = useContext(LiveNowContext) 77 + if (!ctx) { 78 + throw new Error( 79 + 'useLiveNowConfig must be used within a LiveNowConfigProvider', 80 + ) 81 + } 82 + return ctx 83 + } 84 + 85 + export function useCanGoLive(did?: string) { 86 + const config = useLiveNowConfig() 87 + return !!config.find(cfg => cfg.did === did) 88 + }
-57
src/state/trending-config.tsx
··· 1 - import React from 'react' 2 - 3 - import {useLanguagePrefs} from '#/state/preferences/languages' 4 - import {useServiceConfigQuery} from '#/state/queries/service-config' 5 - import {device} from '#/storage' 6 - 7 - type Context = { 8 - enabled: boolean 9 - } 10 - 11 - const Context = React.createContext<Context>({ 12 - enabled: false, 13 - }) 14 - 15 - export function Provider({children}: React.PropsWithChildren<{}>) { 16 - const langPrefs = useLanguagePrefs() 17 - const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery() 18 - const ctx = React.useMemo<Context>(() => { 19 - if (__DEV__) { 20 - return {enabled: true} 21 - } 22 - 23 - /* 24 - * Only English during beta period 25 - */ 26 - if ( 27 - !!langPrefs.contentLanguages.length && 28 - !langPrefs.contentLanguages.includes('en') 29 - ) { 30 - return {enabled: false} 31 - } 32 - 33 - /* 34 - * While loading, use cached value 35 - */ 36 - const cachedEnabled = device.get(['trendingBetaEnabled']) 37 - if (isInitialLoad) { 38 - return {enabled: Boolean(cachedEnabled)} 39 - } 40 - 41 - /* 42 - * Doing an extra check here to reduce hits to statsig. If it's disabled on 43 - * the server, we can exit early. 44 - */ 45 - const enabled = Boolean(config?.topicsEnabled) 46 - 47 - // update cache 48 - device.set(['trendingBetaEnabled'], enabled) 49 - 50 - return {enabled} 51 - }, [isInitialLoad, config, langPrefs.contentLanguages]) 52 - return <Context.Provider value={ctx}>{children}</Context.Provider> 53 - } 54 - 55 - export function useTrendingConfig() { 56 - return React.useContext(Context) 57 - }
···
+7 -5
src/view/com/posts/PostFeed.tsx
··· 19 import {useLingui} from '@lingui/react' 20 import {useQueryClient} from '@tanstack/react-query' 21 22 - import {isStatusStillActive} from '#/lib/actor-status' 23 import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' 24 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 25 import {logEvent} from '#/lib/statsig/statsig' ··· 39 RQKEY, 40 usePostFeedQuery, 41 } from '#/state/queries/post-feed' 42 import {useSession} from '#/state/session' 43 import {useProgressGuide} from '#/state/shell/progress-guide' 44 import {List, type ListRef} from '#/view/com/util/List' ··· 53 } from '#/components/feeds/PostFeedVideoGridRow' 54 import {TrendingInterstitial} from '#/components/interstitials/Trending' 55 import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' 56 - import {temp__canBeLive, temp__isStatusValid} from '#/components/live/temp' 57 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' 58 import {FeedShutdownMsg} from './FeedShutdownMsg' 59 import {PostFeedErrorMessage} from './PostFeedErrorMessage' ··· 777 ) 778 }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) 779 780 const seenActorWithStatusRef = useRef<Set<string>>(new Set()) 781 const onItemSeen = useCallback( 782 (item: FeedRow) => { 783 feedFeedback.onItemSeen(item) 784 if (item.type === 'sliceItem') { 785 const actor = item.slice.items[item.indexInSlice].post.author 786 if ( 787 actor.status && 788 - temp__canBeLive(actor) && 789 - temp__isStatusValid(actor.status) && 790 isStatusStillActive(actor.status.expiresAt) 791 ) { 792 if (!seenActorWithStatusRef.current.has(actor.did)) { ··· 799 } 800 } 801 }, 802 - [feedFeedback, feed], 803 ) 804 805 return (
··· 19 import {useLingui} from '@lingui/react' 20 import {useQueryClient} from '@tanstack/react-query' 21 22 + import {isStatusStillActive, validateStatus} from '#/lib/actor-status' 23 import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' 24 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 25 import {logEvent} from '#/lib/statsig/statsig' ··· 39 RQKEY, 40 usePostFeedQuery, 41 } from '#/state/queries/post-feed' 42 + import {useLiveNowConfig} from '#/state/service-config' 43 import {useSession} from '#/state/session' 44 import {useProgressGuide} from '#/state/shell/progress-guide' 45 import {List, type ListRef} from '#/view/com/util/List' ··· 54 } from '#/components/feeds/PostFeedVideoGridRow' 55 import {TrendingInterstitial} from '#/components/interstitials/Trending' 56 import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' 57 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' 58 import {FeedShutdownMsg} from './FeedShutdownMsg' 59 import {PostFeedErrorMessage} from './PostFeedErrorMessage' ··· 777 ) 778 }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset]) 779 780 + const liveNowConfig = useLiveNowConfig() 781 + 782 const seenActorWithStatusRef = useRef<Set<string>>(new Set()) 783 const onItemSeen = useCallback( 784 (item: FeedRow) => { 785 feedFeedback.onItemSeen(item) 786 if (item.type === 'sliceItem') { 787 const actor = item.slice.items[item.indexInSlice].post.author 788 + 789 if ( 790 actor.status && 791 + validateStatus(actor.did, actor.status, liveNowConfig) && 792 isStatusStillActive(actor.status.expiresAt) 793 ) { 794 if (!seenActorWithStatusRef.current.has(actor.did)) { ··· 801 } 802 } 803 }, 804 + [feedFeedback, feed, liveNowConfig], 805 ) 806 807 return (
+3 -2
src/view/com/profile/ProfileMenu.tsx
··· 20 useProfileFollowMutationQueue, 21 useProfileMuteMutationQueue, 22 } from '#/state/queries/profile' 23 import {useSession} from '#/state/session' 24 import {EventStopper} from '#/view/com/util/EventStopper' 25 import * as Toast from '#/view/com/util/Toast' ··· 43 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 44 import {EditLiveDialog} from '#/components/live/EditLiveDialog' 45 import {GoLiveDialog} from '#/components/live/GoLiveDialog' 46 - import {temp__canGoLive} from '#/components/live/temp' 47 import * as Menu from '#/components/Menu' 48 import { 49 ReportDialog, ··· 73 const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked 74 const [devModeEnabled] = useDevMode() 75 const verification = useFullVerificationState({profile}) 76 77 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 78 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) ··· 299 </Menu.ItemText> 300 <Menu.ItemIcon icon={List} /> 301 </Menu.Item> 302 - {isSelf && temp__canGoLive(profile) && ( 303 <Menu.Item 304 testID="profileHeaderDropdownListAddRemoveBtn" 305 label={
··· 20 useProfileFollowMutationQueue, 21 useProfileMuteMutationQueue, 22 } from '#/state/queries/profile' 23 + import {useCanGoLive} from '#/state/service-config' 24 import {useSession} from '#/state/session' 25 import {EventStopper} from '#/view/com/util/EventStopper' 26 import * as Toast from '#/view/com/util/Toast' ··· 44 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 45 import {EditLiveDialog} from '#/components/live/EditLiveDialog' 46 import {GoLiveDialog} from '#/components/live/GoLiveDialog' 47 import * as Menu from '#/components/Menu' 48 import { 49 ReportDialog, ··· 73 const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked 74 const [devModeEnabled] = useDevMode() 75 const verification = useFullVerificationState({profile}) 76 + const canGoLive = useCanGoLive(currentAccount?.did) 77 78 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 79 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) ··· 300 </Menu.ItemText> 301 <Menu.ItemIcon icon={List} /> 302 </Menu.Item> 303 + {isSelf && canGoLive && ( 304 <Menu.Item 305 testID="profileHeaderDropdownListAddRemoveBtn" 306 label={
+1 -1
src/view/shell/desktop/SidebarTrendingTopics.tsx
··· 9 useTrendingSettingsApi, 10 } from '#/state/preferences/trending' 11 import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' 12 - import {useTrendingConfig} from '#/state/trending-config' 13 import {atoms as a, useTheme} from '#/alf' 14 import {Button, ButtonIcon} from '#/components/Button' 15 import {Divider} from '#/components/Divider'
··· 9 useTrendingSettingsApi, 10 } from '#/state/preferences/trending' 11 import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' 12 + import {useTrendingConfig} from '#/state/service-config' 13 import {atoms as a, useTheme} from '#/alf' 14 import {Button, ButtonIcon} from '#/components/Button' 15 import {Divider} from '#/components/Divider'
+27 -27
yarn.lock
··· 64 "@atproto/xrpc" "^0.7.0" 65 "@atproto/xrpc-server" "^0.7.18" 66 67 - "@atproto/api@^0.15.6": 68 - version "0.15.6" 69 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.6.tgz#3832f16641d89c687794cea14b4aba05ba5993c8" 70 - integrity sha512-hKwrBf60LcI4BqArWyrhWJWIpjwAWUJpW3PVvNzUB1q2W/ByC0JAuwq/F8tZpCEiiVBzHjHVRx4QNA2TA1cG3g== 71 dependencies: 72 "@atproto/common-web" "^0.4.2" 73 "@atproto/lexicon" "^0.4.11" ··· 95 multiformats "^9.9.0" 96 uint8arrays "3.0.0" 97 98 - "@atproto/bsky@^0.0.148": 99 - version "0.0.148" 100 - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.148.tgz#f864631e5a9726d3a40c15b0311f730bc16d6bd9" 101 - integrity sha512-09Lzjz9kCK7kPOlJcVj6KbATtoPQwNeeU5s0J2apZYCQmA7wN2xRb5KMf9wr+wa1KO7FwbXKSunwer96dB6zrQ== 102 dependencies: 103 "@atproto-labs/fetch-node" "0.1.8" 104 "@atproto-labs/xrpc-utils" "0.0.14" 105 - "@atproto/api" "^0.15.6" 106 "@atproto/common" "^0.4.11" 107 "@atproto/crypto" "^0.4.4" 108 "@atproto/did" "^0.1.5" ··· 219 "@noble/hashes" "^1.6.1" 220 uint8arrays "3.0.0" 221 222 - "@atproto/dev-env@^0.3.129": 223 - version "0.3.130" 224 - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.130.tgz#444ad315c00bdcf8bdae036d1e6a56a1808b98c6" 225 - integrity sha512-xRQb+b09lpdG1vGdvMk8Yf/AnO4SDQTjKLyPO+LYYeHuOrKKjJWiBorFC8Lp/rnraoM3AcwMKmW48wdd7cOL9g== 226 dependencies: 227 - "@atproto/api" "^0.15.6" 228 - "@atproto/bsky" "^0.0.148" 229 "@atproto/bsync" "^0.0.19" 230 "@atproto/common-web" "^0.4.2" 231 "@atproto/crypto" "^0.4.4" 232 "@atproto/identity" "^0.4.8" 233 "@atproto/lexicon" "^0.4.11" 234 - "@atproto/ozone" "^0.1.109" 235 - "@atproto/pds" "^0.4.136" 236 "@atproto/sync" "^0.1.23" 237 "@atproto/syntax" "^0.4.0" 238 "@atproto/xrpc-server" "^0.7.18" ··· 348 "@atproto/jwk" "0.1.5" 349 zod "^3.23.8" 350 351 - "@atproto/ozone@^0.1.109": 352 - version "0.1.109" 353 - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.109.tgz#538de28cb21c10afa3fbce0140cd695ef7948e09" 354 - integrity sha512-KokZtu5mhYJdNmYqkI2JZ2hiehxXpi8bbULyWE3f0RKbQRBUBGDVBSF8WkuJUuLzaquyYJVtg3MZFp9ELBcg0g== 355 dependencies: 356 - "@atproto/api" "^0.15.6" 357 "@atproto/common" "^0.4.11" 358 "@atproto/crypto" "^0.4.4" 359 "@atproto/identity" "^0.4.8" ··· 378 undici "^6.14.1" 379 ws "^8.12.0" 380 381 - "@atproto/pds@^0.4.136": 382 - version "0.4.136" 383 - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.136.tgz#53989ff7784c4d1e68d745d69721e71ba82a111d" 384 - integrity sha512-sao4iq/CRWwdM0gljw7XGg/ef4OTWFc6RU2g0nNgJLvxfPO3uMG8Ze1S6tfhr9wvhIKZWVCzzPruTglrlWMEYw== 385 dependencies: 386 "@atproto-labs/fetch-node" "0.1.8" 387 "@atproto-labs/xrpc-utils" "0.0.14" 388 - "@atproto/api" "^0.15.6" 389 "@atproto/aws" "^0.2.21" 390 "@atproto/common" "^0.4.11" 391 "@atproto/crypto" "^0.4.4"
··· 64 "@atproto/xrpc" "^0.7.0" 65 "@atproto/xrpc-server" "^0.7.18" 66 67 + "@atproto/api@^0.15.7": 68 + version "0.15.7" 69 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.7.tgz#8436162d9fa5dac627bdd5c0f5c9598309ec1383" 70 + integrity sha512-YRETLcOwDCYfGs7Sl9ObqPwhOlVWrPkw4f1AYGIrXLQS58WHe/vz1lZbqOqMsC6gvCnyZnOuKlhsRHZ14rBLzg== 71 dependencies: 72 "@atproto/common-web" "^0.4.2" 73 "@atproto/lexicon" "^0.4.11" ··· 95 multiformats "^9.9.0" 96 uint8arrays "3.0.0" 97 98 + "@atproto/bsky@^0.0.149": 99 + version "0.0.149" 100 + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.149.tgz#3e9cfb999b9958e9a61776eddb72d424905ec3be" 101 + integrity sha512-7j2KgWHm1nOTQDmtEcNwtldTArS9WwZS3M+aw7OmGH8wCa8vEljNxP6HETjtktDMNTrSipHmmyqh25+Rc5+Ziw== 102 dependencies: 103 "@atproto-labs/fetch-node" "0.1.8" 104 "@atproto-labs/xrpc-utils" "0.0.14" 105 + "@atproto/api" "^0.15.7" 106 "@atproto/common" "^0.4.11" 107 "@atproto/crypto" "^0.4.4" 108 "@atproto/did" "^0.1.5" ··· 219 "@noble/hashes" "^1.6.1" 220 uint8arrays "3.0.0" 221 222 + "@atproto/dev-env@^0.3.131": 223 + version "0.3.131" 224 + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.131.tgz#b3b4cee5f367766d542515b1713523423ecb5a71" 225 + integrity sha512-Tijqc/vq7qKGTpgoKm1BwyvP2QfoOQRjNm9Ro5CDAMXsKqHfXxPiytxYqxj6QR/PptC27aDUqgmexluZN6XbWg== 226 dependencies: 227 + "@atproto/api" "^0.15.7" 228 + "@atproto/bsky" "^0.0.149" 229 "@atproto/bsync" "^0.0.19" 230 "@atproto/common-web" "^0.4.2" 231 "@atproto/crypto" "^0.4.4" 232 "@atproto/identity" "^0.4.8" 233 "@atproto/lexicon" "^0.4.11" 234 + "@atproto/ozone" "^0.1.110" 235 + "@atproto/pds" "^0.4.137" 236 "@atproto/sync" "^0.1.23" 237 "@atproto/syntax" "^0.4.0" 238 "@atproto/xrpc-server" "^0.7.18" ··· 348 "@atproto/jwk" "0.1.5" 349 zod "^3.23.8" 350 351 + "@atproto/ozone@^0.1.110": 352 + version "0.1.110" 353 + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.110.tgz#78ad57961b4699c8aa3e6f7d5b6f215d7760a723" 354 + integrity sha512-X7VU7QAkwJrwpgmAuAHqvVDX9CEW0Ts5R4ovATgEt2lbxyxtJtYIm1dG346fAlOfC9f3RGN+HI8vBMWrrrLKAQ== 355 dependencies: 356 + "@atproto/api" "^0.15.7" 357 "@atproto/common" "^0.4.11" 358 "@atproto/crypto" "^0.4.4" 359 "@atproto/identity" "^0.4.8" ··· 378 undici "^6.14.1" 379 ws "^8.12.0" 380 381 + "@atproto/pds@^0.4.137": 382 + version "0.4.137" 383 + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.137.tgz#87468703b02bf42681ddd50049ee906331655731" 384 + integrity sha512-DRUck9CgOdK0cP6B6/1Cku2gb5t31Vhh9su2TcqF9eymZP1dNSI6nfTIEp+cuwpW/VpDeu7AfHCSgYfnJeZ5yg== 385 dependencies: 386 "@atproto-labs/fetch-node" "0.1.8" 387 "@atproto-labs/xrpc-utils" "0.0.14" 388 + "@atproto/api" "^0.15.7" 389 "@atproto/aws" "^0.2.21" 390 "@atproto/common" "^0.4.11" 391 "@atproto/crypto" "^0.4.4"