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
This commit is contained in:
+2
-2
@@ -69,7 +69,7 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.15.6",
|
"@atproto/api": "^0.15.7",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@atproto/dev-env": "^0.3.129",
|
"@atproto/dev-env": "^0.3.131",
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.0",
|
||||||
"@babel/runtime": "^7.26.0",
|
"@babel/runtime": "^7.26.0",
|
||||||
|
|||||||
+3
-3
@@ -43,6 +43,7 @@ import {Provider as PrefsStateProvider} from '#/state/preferences'
|
|||||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||||
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
|
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
|
||||||
import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread'
|
import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread'
|
||||||
|
import {Provider as ServiceAccountManager} from '#/state/service-config'
|
||||||
import {
|
import {
|
||||||
Provider as SessionProvider,
|
Provider as SessionProvider,
|
||||||
type SessionAccount,
|
type SessionAccount,
|
||||||
@@ -57,7 +58,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
|||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
|
|
||||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -149,7 +149,7 @@ function InnerApp() {
|
|||||||
<BackgroundNotificationPreferencesProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
<ProgressGuideProvider>
|
<ProgressGuideProvider>
|
||||||
<TrendingConfigProvider>
|
<ServiceAccountManager>
|
||||||
<GestureHandlerRootView
|
<GestureHandlerRootView
|
||||||
style={s.h100pct}>
|
style={s.h100pct}>
|
||||||
<IntentDialogProvider>
|
<IntentDialogProvider>
|
||||||
@@ -158,7 +158,7 @@ function InnerApp() {
|
|||||||
<NuxDialogs />
|
<NuxDialogs />
|
||||||
</IntentDialogProvider>
|
</IntentDialogProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
</TrendingConfigProvider>
|
</ServiceAccountManager>
|
||||||
</ProgressGuideProvider>
|
</ProgressGuideProvider>
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
|
|||||||
+3
-3
@@ -33,6 +33,7 @@ import {Provider as PrefsStateProvider} from '#/state/preferences'
|
|||||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||||
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
|
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
|
||||||
import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread'
|
import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread'
|
||||||
|
import {Provider as ServiceConfigProvider} from '#/state/service-config'
|
||||||
import {
|
import {
|
||||||
Provider as SessionProvider,
|
Provider as SessionProvider,
|
||||||
type SessionAccount,
|
type SessionAccount,
|
||||||
@@ -47,7 +48,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
|||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
|
|
||||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -130,12 +130,12 @@ function InnerApp() {
|
|||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<ProgressGuideProvider>
|
<ProgressGuideProvider>
|
||||||
<TrendingConfigProvider>
|
<ServiceConfigProvider>
|
||||||
<IntentDialogProvider>
|
<IntentDialogProvider>
|
||||||
<Shell />
|
<Shell />
|
||||||
<NuxDialogs />
|
<NuxDialogs />
|
||||||
</IntentDialogProvider>
|
</IntentDialogProvider>
|
||||||
</TrendingConfigProvider>
|
</ServiceConfigProvider>
|
||||||
</ProgressGuideProvider>
|
</ProgressGuideProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useTrendingSettingsApi,
|
useTrendingSettingsApi,
|
||||||
} from '#/state/preferences/trending'
|
} from '#/state/preferences/trending'
|
||||||
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
||||||
import {useTrendingConfig} from '#/state/trending-config'
|
import {useTrendingConfig} from '#/state/service-config'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {cleanError} from '#/lib/strings/errors'
|
|||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useAgent} from '#/state/session'
|
import {useLiveNowConfig} from '#/state/service-config'
|
||||||
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
|
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
|
||||||
@@ -58,6 +59,10 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
const [duration, setDuration] = useState(60)
|
const [duration, setDuration] = useState(60)
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const tick = useTickEveryMinute()
|
const tick = useTickEveryMinute()
|
||||||
|
const liveNowConfig = useLiveNowConfig()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
const config = liveNowConfig.find(cfg => cfg.did === currentAccount?.did)
|
||||||
|
|
||||||
const time = useCallback(
|
const time = useCallback(
|
||||||
(offset: number) => {
|
(offset: number) => {
|
||||||
@@ -79,7 +84,6 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
|
|
||||||
const liveLinkUrl = definitelyUrl(liveLink)
|
const liveLinkUrl = definitelyUrl(liveLink)
|
||||||
const debouncedUrl = useDebouncedValue(liveLinkUrl, 500)
|
const debouncedUrl = useDebouncedValue(liveLinkUrl, 500)
|
||||||
const hasLink = !!debouncedUrl
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: linkMeta,
|
data: linkMeta,
|
||||||
@@ -91,6 +95,13 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
queryKey: ['link-meta', debouncedUrl],
|
queryKey: ['link-meta', debouncedUrl],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!debouncedUrl) return null
|
if (!debouncedUrl) return null
|
||||||
|
if (!config) throw new Error(_(msg`You are not allowed to go live`))
|
||||||
|
|
||||||
|
const urlp = new URL(debouncedUrl)
|
||||||
|
if (!config.domains.includes(urlp.hostname)) {
|
||||||
|
throw new Error(_(msg`${urlp.hostname} is not a valid URL`))
|
||||||
|
}
|
||||||
|
|
||||||
return getLinkMeta(agent, debouncedUrl)
|
return getLinkMeta(agent, debouncedUrl)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -101,6 +112,10 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
error: goLiveError,
|
error: goLiveError,
|
||||||
} = useUpsertLiveStatusMutation(duration, linkMeta)
|
} = useUpsertLiveStatusMutation(duration, linkMeta)
|
||||||
|
|
||||||
|
const isSourceInvalid = !!liveLinkError || !!linkMetaError
|
||||||
|
|
||||||
|
const hasLink = !!debouncedUrl && !isSourceInvalid
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Go Live`)}
|
label={_(msg`Go Live`)}
|
||||||
@@ -136,7 +151,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
|||||||
<TextField.LabelText>
|
<TextField.LabelText>
|
||||||
<Trans>Live link</Trans>
|
<Trans>Live link</Trans>
|
||||||
</TextField.LabelText>
|
</TextField.LabelText>
|
||||||
<TextField.Root isInvalid={!!liveLinkError || !!linkMetaError}>
|
<TextField.Root isInvalid={isSourceInvalid}>
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
label={_(msg`Live link`)}
|
label={_(msg`Live link`)}
|
||||||
placeholder={_(msg`www.mylivestream.tv`)}
|
placeholder={_(msg`www.mylivestream.tv`)}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import {type AppBskyActorDefs, AppBskyEmbedExternal} from '@atproto/api'
|
|
||||||
|
|
||||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
|
||||||
import type * as bsky from '#/types/bsky'
|
|
||||||
|
|
||||||
export const LIVE_DIDS: Record<string, true> = {
|
|
||||||
'did:plc:7sfnardo5xxznxc6esxc5ooe': true, // nba.com
|
|
||||||
'did:plc:gx6fyi3jcfxd7ammq2t7mzp2': true, // rtgame.bsky.social
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LIVE_SOURCES: Record<string, true> = {
|
|
||||||
'nba.com': true,
|
|
||||||
'twitch.tv': true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TEMP: dumb gating
|
|
||||||
export function temp__canBeLive(profile: bsky.profile.AnyProfileView) {
|
|
||||||
if (__DEV__)
|
|
||||||
return !!DISCOVER_DEBUG_DIDS[profile.did] || !!LIVE_DIDS[profile.did]
|
|
||||||
return !!LIVE_DIDS[profile.did]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function temp__canGoLive(profile: bsky.profile.AnyProfileView) {
|
|
||||||
if (__DEV__) return true
|
|
||||||
return !!LIVE_DIDS[profile.did]
|
|
||||||
}
|
|
||||||
|
|
||||||
// status must have a embed, and the embed must be an approved host for the status to be valid
|
|
||||||
export function temp__isStatusValid(status: AppBskyActorDefs.StatusView) {
|
|
||||||
if (status.status !== 'app.bsky.actor.status#live') return false
|
|
||||||
try {
|
|
||||||
if (AppBskyEmbedExternal.isView(status.embed)) {
|
|
||||||
const url = new URL(status.embed.external.uri)
|
|
||||||
return !!LIVE_SOURCES[url.hostname]
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+28
-5
@@ -2,27 +2,28 @@ import {useMemo} from 'react'
|
|||||||
import {
|
import {
|
||||||
type $Typed,
|
type $Typed,
|
||||||
type AppBskyActorDefs,
|
type AppBskyActorDefs,
|
||||||
type AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {isAfter, parseISO} from 'date-fns'
|
import {isAfter, parseISO} from 'date-fns'
|
||||||
|
|
||||||
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {useLiveNowConfig} from '#/state/service-config'
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import {temp__canBeLive, temp__isStatusValid} from '#/components/live/temp'
|
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
|
export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
|
||||||
const shadowed = useMaybeProfileShadow(actor)
|
const shadowed = useMaybeProfileShadow(actor)
|
||||||
const tick = useTickEveryMinute()
|
const tick = useTickEveryMinute()
|
||||||
|
const config = useLiveNowConfig()
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
tick! // revalidate every minute
|
tick! // revalidate every minute
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shadowed &&
|
shadowed &&
|
||||||
temp__canBeLive(shadowed) &&
|
|
||||||
'status' in shadowed &&
|
'status' in shadowed &&
|
||||||
shadowed.status &&
|
shadowed.status &&
|
||||||
temp__isStatusValid(shadowed.status) &&
|
validateStatus(shadowed.did, shadowed.status, config) &&
|
||||||
isStatusStillActive(shadowed.status.expiresAt)
|
isStatusStillActive(shadowed.status.expiresAt)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@@ -39,7 +40,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
|
|||||||
record: {},
|
record: {},
|
||||||
} satisfies AppBskyActorDefs.StatusView
|
} satisfies AppBskyActorDefs.StatusView
|
||||||
}
|
}
|
||||||
}, [shadowed, tick])
|
}, [shadowed, config, tick])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isStatusStillActive(timeStr: string | undefined) {
|
export function isStatusStillActive(timeStr: string | undefined) {
|
||||||
@@ -49,3 +50,25 @@ export function isStatusStillActive(timeStr: string | undefined) {
|
|||||||
|
|
||||||
return isAfter(expiry, now)
|
return isAfter(expiry, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateStatus(
|
||||||
|
did: string,
|
||||||
|
status: AppBskyActorDefs.StatusView,
|
||||||
|
config: {did: string; domains: string[]}[],
|
||||||
|
) {
|
||||||
|
if (status.status !== 'app.bsky.actor.status#live') return false
|
||||||
|
const sources = config.find(cfg => cfg.did === did)
|
||||||
|
if (!sources) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (AppBskyEmbedExternal.isView(status.embed)) {
|
||||||
|
const url = new URL(status.embed.external.uri)
|
||||||
|
return sources.domains.includes(url.hostname)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
|
DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
|
||||||
useTrendingTopics,
|
useTrendingTopics,
|
||||||
} from '#/state/queries/trending/useTrendingTopics'
|
} from '#/state/queries/trending/useTrendingTopics'
|
||||||
import {useTrendingConfig} from '#/state/trending-config'
|
import {useTrendingConfig} from '#/state/service-config'
|
||||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||||
import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag'
|
import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {logger} from '#/logger'
|
|||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useTrendingSettings} from '#/state/preferences/trending'
|
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||||
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
|
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
|
||||||
import {useTrendingConfig} from '#/state/trending-config'
|
import {useTrendingConfig} from '#/state/service-config'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {formatCount} from '#/view/com/util/numeric/format'
|
import {formatCount} from '#/view/com/util/numeric/format'
|
||||||
import {atoms as a, useGutters, useTheme, type ViewStyleProp, web} from '#/alf'
|
import {atoms as a, useGutters, useTheme, type ViewStyleProp, web} from '#/alf'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
useTrendingSettings,
|
useTrendingSettings,
|
||||||
useTrendingSettingsApi,
|
useTrendingSettingsApi,
|
||||||
} from '#/state/preferences/trending'
|
} from '#/state/preferences/trending'
|
||||||
import {useTrendingConfig} from '#/state/trending-config'
|
import {useTrendingConfig} from '#/state/service-config'
|
||||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {useAgent} from '#/state/session'
|
|||||||
type ServiceConfig = {
|
type ServiceConfig = {
|
||||||
checkEmailConfirmed: boolean
|
checkEmailConfirmed: boolean
|
||||||
topicsEnabled: boolean
|
topicsEnabled: boolean
|
||||||
|
liveNow: {
|
||||||
|
did: string
|
||||||
|
domains: string[]
|
||||||
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useServiceConfigQuery() {
|
export function useServiceConfigQuery() {
|
||||||
@@ -21,11 +25,13 @@ export function useServiceConfigQuery() {
|
|||||||
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
|
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
|
||||||
// @ts-expect-error not included in types atm
|
// @ts-expect-error not included in types atm
|
||||||
topicsEnabled: Boolean(data.topicsEnabled),
|
topicsEnabled: Boolean(data.topicsEnabled),
|
||||||
|
liveNow: data.liveNow ?? [],
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
checkEmailConfirmed: false,
|
checkEmailConfirmed: false,
|
||||||
topicsEnabled: false,
|
topicsEnabled: false,
|
||||||
|
liveNow: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
import React from 'react'
|
import {createContext, useContext, useMemo} from 'react'
|
||||||
|
|
||||||
import {useLanguagePrefs} from '#/state/preferences/languages'
|
import {useLanguagePrefs} from '#/state/preferences/languages'
|
||||||
import {useServiceConfigQuery} from '#/state/queries/service-config'
|
import {useServiceConfigQuery} from '#/state/queries/service-config'
|
||||||
import {device} from '#/storage'
|
import {device} from '#/storage'
|
||||||
|
|
||||||
type Context = {
|
type TrendingContext = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Context = React.createContext<Context>({
|
type LiveNowContext = {
|
||||||
|
did: string
|
||||||
|
domains: string[]
|
||||||
|
}[]
|
||||||
|
|
||||||
|
const TrendingContext = createContext<TrendingContext>({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
const LiveNowContext = createContext<LiveNowContext | null>(null)
|
||||||
|
|
||||||
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery()
|
const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery()
|
||||||
const ctx = React.useMemo<Context>(() => {
|
const trending = useMemo<TrendingContext>(() => {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
return {enabled: true}
|
return {enabled: true}
|
||||||
}
|
}
|
||||||
@@ -49,9 +56,33 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
return {enabled}
|
return {enabled}
|
||||||
}, [isInitialLoad, config, langPrefs.contentLanguages])
|
}, [isInitialLoad, config, langPrefs.contentLanguages])
|
||||||
return <Context.Provider value={ctx}>{children}</Context.Provider>
|
|
||||||
|
const liveNow = useMemo<LiveNowContext>(() => config?.liveNow ?? [], [config])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TrendingContext.Provider value={trending}>
|
||||||
|
<LiveNowContext.Provider value={liveNow}>
|
||||||
|
{children}
|
||||||
|
</LiveNowContext.Provider>
|
||||||
|
</TrendingContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTrendingConfig() {
|
export function useTrendingConfig() {
|
||||||
return React.useContext(Context)
|
return useContext(TrendingContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLiveNowConfig() {
|
||||||
|
const ctx = useContext(LiveNowContext)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error(
|
||||||
|
'useLiveNowConfig must be used within a LiveNowConfigProvider',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCanGoLive(did?: string) {
|
||||||
|
const config = useLiveNowConfig()
|
||||||
|
return !!config.find(cfg => cfg.did === did)
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {isStatusStillActive} from '#/lib/actor-status'
|
import {isStatusStillActive, validateStatus} from '#/lib/actor-status'
|
||||||
import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants'
|
import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
RQKEY,
|
RQKEY,
|
||||||
usePostFeedQuery,
|
usePostFeedQuery,
|
||||||
} from '#/state/queries/post-feed'
|
} from '#/state/queries/post-feed'
|
||||||
|
import {useLiveNowConfig} from '#/state/service-config'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
import {List, type ListRef} from '#/view/com/util/List'
|
import {List, type ListRef} from '#/view/com/util/List'
|
||||||
@@ -53,7 +54,6 @@ import {
|
|||||||
} from '#/components/feeds/PostFeedVideoGridRow'
|
} from '#/components/feeds/PostFeedVideoGridRow'
|
||||||
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
import {TrendingInterstitial} from '#/components/interstitials/Trending'
|
||||||
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
|
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
|
||||||
import {temp__canBeLive, temp__isStatusValid} from '#/components/live/temp'
|
|
||||||
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||||
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
||||||
import {PostFeedErrorMessage} from './PostFeedErrorMessage'
|
import {PostFeedErrorMessage} from './PostFeedErrorMessage'
|
||||||
@@ -777,16 +777,18 @@ let PostFeed = ({
|
|||||||
)
|
)
|
||||||
}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
|
}, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
|
||||||
|
|
||||||
|
const liveNowConfig = useLiveNowConfig()
|
||||||
|
|
||||||
const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
||||||
const onItemSeen = useCallback(
|
const onItemSeen = useCallback(
|
||||||
(item: FeedRow) => {
|
(item: FeedRow) => {
|
||||||
feedFeedback.onItemSeen(item)
|
feedFeedback.onItemSeen(item)
|
||||||
if (item.type === 'sliceItem') {
|
if (item.type === 'sliceItem') {
|
||||||
const actor = item.slice.items[item.indexInSlice].post.author
|
const actor = item.slice.items[item.indexInSlice].post.author
|
||||||
|
|
||||||
if (
|
if (
|
||||||
actor.status &&
|
actor.status &&
|
||||||
temp__canBeLive(actor) &&
|
validateStatus(actor.did, actor.status, liveNowConfig) &&
|
||||||
temp__isStatusValid(actor.status) &&
|
|
||||||
isStatusStillActive(actor.status.expiresAt)
|
isStatusStillActive(actor.status.expiresAt)
|
||||||
) {
|
) {
|
||||||
if (!seenActorWithStatusRef.current.has(actor.did)) {
|
if (!seenActorWithStatusRef.current.has(actor.did)) {
|
||||||
@@ -799,7 +801,7 @@ let PostFeed = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[feedFeedback, feed],
|
[feedFeedback, feed, liveNowConfig],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
useProfileFollowMutationQueue,
|
useProfileFollowMutationQueue,
|
||||||
useProfileMuteMutationQueue,
|
useProfileMuteMutationQueue,
|
||||||
} from '#/state/queries/profile'
|
} from '#/state/queries/profile'
|
||||||
|
import {useCanGoLive} from '#/state/service-config'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -43,7 +44,6 @@ import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
|
|||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
||||||
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
|
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
|
||||||
import {temp__canGoLive} from '#/components/live/temp'
|
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {
|
import {
|
||||||
ReportDialog,
|
ReportDialog,
|
||||||
@@ -73,6 +73,7 @@ let ProfileMenu = ({
|
|||||||
const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
|
const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
|
||||||
const [devModeEnabled] = useDevMode()
|
const [devModeEnabled] = useDevMode()
|
||||||
const verification = useFullVerificationState({profile})
|
const verification = useFullVerificationState({profile})
|
||||||
|
const canGoLive = useCanGoLive(currentAccount?.did)
|
||||||
|
|
||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
@@ -299,7 +300,7 @@ let ProfileMenu = ({
|
|||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={List} />
|
<Menu.ItemIcon icon={List} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{isSelf && temp__canGoLive(profile) && (
|
{isSelf && canGoLive && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="profileHeaderDropdownListAddRemoveBtn"
|
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||||
label={
|
label={
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useTrendingSettingsApi,
|
useTrendingSettingsApi,
|
||||||
} from '#/state/preferences/trending'
|
} from '#/state/preferences/trending'
|
||||||
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
||||||
import {useTrendingConfig} from '#/state/trending-config'
|
import {useTrendingConfig} from '#/state/service-config'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
|
|||||||
@@ -64,10 +64,10 @@
|
|||||||
"@atproto/xrpc" "^0.7.0"
|
"@atproto/xrpc" "^0.7.0"
|
||||||
"@atproto/xrpc-server" "^0.7.18"
|
"@atproto/xrpc-server" "^0.7.18"
|
||||||
|
|
||||||
"@atproto/api@^0.15.6":
|
"@atproto/api@^0.15.7":
|
||||||
version "0.15.6"
|
version "0.15.7"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.6.tgz#3832f16641d89c687794cea14b4aba05ba5993c8"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.7.tgz#8436162d9fa5dac627bdd5c0f5c9598309ec1383"
|
||||||
integrity sha512-hKwrBf60LcI4BqArWyrhWJWIpjwAWUJpW3PVvNzUB1q2W/ByC0JAuwq/F8tZpCEiiVBzHjHVRx4QNA2TA1cG3g==
|
integrity sha512-YRETLcOwDCYfGs7Sl9ObqPwhOlVWrPkw4f1AYGIrXLQS58WHe/vz1lZbqOqMsC6gvCnyZnOuKlhsRHZ14rBLzg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "^0.4.2"
|
"@atproto/common-web" "^0.4.2"
|
||||||
"@atproto/lexicon" "^0.4.11"
|
"@atproto/lexicon" "^0.4.11"
|
||||||
@@ -95,14 +95,14 @@
|
|||||||
multiformats "^9.9.0"
|
multiformats "^9.9.0"
|
||||||
uint8arrays "3.0.0"
|
uint8arrays "3.0.0"
|
||||||
|
|
||||||
"@atproto/bsky@^0.0.148":
|
"@atproto/bsky@^0.0.149":
|
||||||
version "0.0.148"
|
version "0.0.149"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.148.tgz#f864631e5a9726d3a40c15b0311f730bc16d6bd9"
|
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.149.tgz#3e9cfb999b9958e9a61776eddb72d424905ec3be"
|
||||||
integrity sha512-09Lzjz9kCK7kPOlJcVj6KbATtoPQwNeeU5s0J2apZYCQmA7wN2xRb5KMf9wr+wa1KO7FwbXKSunwer96dB6zrQ==
|
integrity sha512-7j2KgWHm1nOTQDmtEcNwtldTArS9WwZS3M+aw7OmGH8wCa8vEljNxP6HETjtktDMNTrSipHmmyqh25+Rc5+Ziw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto-labs/fetch-node" "0.1.8"
|
"@atproto-labs/fetch-node" "0.1.8"
|
||||||
"@atproto-labs/xrpc-utils" "0.0.14"
|
"@atproto-labs/xrpc-utils" "0.0.14"
|
||||||
"@atproto/api" "^0.15.6"
|
"@atproto/api" "^0.15.7"
|
||||||
"@atproto/common" "^0.4.11"
|
"@atproto/common" "^0.4.11"
|
||||||
"@atproto/crypto" "^0.4.4"
|
"@atproto/crypto" "^0.4.4"
|
||||||
"@atproto/did" "^0.1.5"
|
"@atproto/did" "^0.1.5"
|
||||||
@@ -219,20 +219,20 @@
|
|||||||
"@noble/hashes" "^1.6.1"
|
"@noble/hashes" "^1.6.1"
|
||||||
uint8arrays "3.0.0"
|
uint8arrays "3.0.0"
|
||||||
|
|
||||||
"@atproto/dev-env@^0.3.129":
|
"@atproto/dev-env@^0.3.131":
|
||||||
version "0.3.130"
|
version "0.3.131"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.130.tgz#444ad315c00bdcf8bdae036d1e6a56a1808b98c6"
|
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.131.tgz#b3b4cee5f367766d542515b1713523423ecb5a71"
|
||||||
integrity sha512-xRQb+b09lpdG1vGdvMk8Yf/AnO4SDQTjKLyPO+LYYeHuOrKKjJWiBorFC8Lp/rnraoM3AcwMKmW48wdd7cOL9g==
|
integrity sha512-Tijqc/vq7qKGTpgoKm1BwyvP2QfoOQRjNm9Ro5CDAMXsKqHfXxPiytxYqxj6QR/PptC27aDUqgmexluZN6XbWg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/api" "^0.15.6"
|
"@atproto/api" "^0.15.7"
|
||||||
"@atproto/bsky" "^0.0.148"
|
"@atproto/bsky" "^0.0.149"
|
||||||
"@atproto/bsync" "^0.0.19"
|
"@atproto/bsync" "^0.0.19"
|
||||||
"@atproto/common-web" "^0.4.2"
|
"@atproto/common-web" "^0.4.2"
|
||||||
"@atproto/crypto" "^0.4.4"
|
"@atproto/crypto" "^0.4.4"
|
||||||
"@atproto/identity" "^0.4.8"
|
"@atproto/identity" "^0.4.8"
|
||||||
"@atproto/lexicon" "^0.4.11"
|
"@atproto/lexicon" "^0.4.11"
|
||||||
"@atproto/ozone" "^0.1.109"
|
"@atproto/ozone" "^0.1.110"
|
||||||
"@atproto/pds" "^0.4.136"
|
"@atproto/pds" "^0.4.137"
|
||||||
"@atproto/sync" "^0.1.23"
|
"@atproto/sync" "^0.1.23"
|
||||||
"@atproto/syntax" "^0.4.0"
|
"@atproto/syntax" "^0.4.0"
|
||||||
"@atproto/xrpc-server" "^0.7.18"
|
"@atproto/xrpc-server" "^0.7.18"
|
||||||
@@ -348,12 +348,12 @@
|
|||||||
"@atproto/jwk" "0.1.5"
|
"@atproto/jwk" "0.1.5"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/ozone@^0.1.109":
|
"@atproto/ozone@^0.1.110":
|
||||||
version "0.1.109"
|
version "0.1.110"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.109.tgz#538de28cb21c10afa3fbce0140cd695ef7948e09"
|
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.110.tgz#78ad57961b4699c8aa3e6f7d5b6f215d7760a723"
|
||||||
integrity sha512-KokZtu5mhYJdNmYqkI2JZ2hiehxXpi8bbULyWE3f0RKbQRBUBGDVBSF8WkuJUuLzaquyYJVtg3MZFp9ELBcg0g==
|
integrity sha512-X7VU7QAkwJrwpgmAuAHqvVDX9CEW0Ts5R4ovATgEt2lbxyxtJtYIm1dG346fAlOfC9f3RGN+HI8vBMWrrrLKAQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/api" "^0.15.6"
|
"@atproto/api" "^0.15.7"
|
||||||
"@atproto/common" "^0.4.11"
|
"@atproto/common" "^0.4.11"
|
||||||
"@atproto/crypto" "^0.4.4"
|
"@atproto/crypto" "^0.4.4"
|
||||||
"@atproto/identity" "^0.4.8"
|
"@atproto/identity" "^0.4.8"
|
||||||
@@ -378,14 +378,14 @@
|
|||||||
undici "^6.14.1"
|
undici "^6.14.1"
|
||||||
ws "^8.12.0"
|
ws "^8.12.0"
|
||||||
|
|
||||||
"@atproto/pds@^0.4.136":
|
"@atproto/pds@^0.4.137":
|
||||||
version "0.4.136"
|
version "0.4.137"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.136.tgz#53989ff7784c4d1e68d745d69721e71ba82a111d"
|
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.137.tgz#87468703b02bf42681ddd50049ee906331655731"
|
||||||
integrity sha512-sao4iq/CRWwdM0gljw7XGg/ef4OTWFc6RU2g0nNgJLvxfPO3uMG8Ze1S6tfhr9wvhIKZWVCzzPruTglrlWMEYw==
|
integrity sha512-DRUck9CgOdK0cP6B6/1Cku2gb5t31Vhh9su2TcqF9eymZP1dNSI6nfTIEp+cuwpW/VpDeu7AfHCSgYfnJeZ5yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto-labs/fetch-node" "0.1.8"
|
"@atproto-labs/fetch-node" "0.1.8"
|
||||||
"@atproto-labs/xrpc-utils" "0.0.14"
|
"@atproto-labs/xrpc-utils" "0.0.14"
|
||||||
"@atproto/api" "^0.15.6"
|
"@atproto/api" "^0.15.7"
|
||||||
"@atproto/aws" "^0.2.21"
|
"@atproto/aws" "^0.2.21"
|
||||||
"@atproto/common" "^0.4.11"
|
"@atproto/common" "^0.4.11"
|
||||||
"@atproto/crypto" "^0.4.4"
|
"@atproto/crypto" "^0.4.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user