[APP-1776] Refactor Live Now config, reorg files (#9871)
This commit is contained in:
@@ -46,3 +46,6 @@ GEOLOCATION_DEV_URL=
|
|||||||
|
|
||||||
# live-events web worker URL
|
# live-events web worker URL
|
||||||
LIVE_EVENTS_DEV_URL=
|
LIVE_EVENTS_DEV_URL=
|
||||||
|
|
||||||
|
# app-config web worker URL
|
||||||
|
APP_CONFIG_DEV_URL=
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ import {ThemeProvider} from '#/lib/ThemeContext'
|
|||||||
import I18nProvider from '#/locale/i18nProvider'
|
import I18nProvider from '#/locale/i18nProvider'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {Provider as A11yProvider} from '#/state/a11y'
|
import {Provider as A11yProvider} from '#/state/a11y'
|
||||||
|
import {
|
||||||
|
prefetchAppConfig,
|
||||||
|
Provider as AppConfigProvider,
|
||||||
|
} from '#/state/appConfig'
|
||||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||||
@@ -103,6 +107,7 @@ if (IS_ANDROID) {
|
|||||||
Geo.resolve()
|
Geo.resolve()
|
||||||
prefetchAgeAssuranceConfig()
|
prefetchAgeAssuranceConfig()
|
||||||
prefetchLiveEvents()
|
prefetchLiveEvents()
|
||||||
|
prefetchAppConfig()
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const [isReady, setIsReady] = React.useState(false)
|
const [isReady, setIsReady] = React.useState(false)
|
||||||
@@ -228,6 +233,7 @@ function App() {
|
|||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<Geo.Provider>
|
<Geo.Provider>
|
||||||
|
<AppConfigProvider>
|
||||||
<A11yProvider>
|
<A11yProvider>
|
||||||
<KeyboardControllerProvider>
|
<KeyboardControllerProvider>
|
||||||
<OnboardingProvider>
|
<OnboardingProvider>
|
||||||
@@ -260,6 +266,7 @@ function App() {
|
|||||||
</OnboardingProvider>
|
</OnboardingProvider>
|
||||||
</KeyboardControllerProvider>
|
</KeyboardControllerProvider>
|
||||||
</A11yProvider>
|
</A11yProvider>
|
||||||
|
</AppConfigProvider>
|
||||||
</Geo.Provider>
|
</Geo.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {ThemeProvider} from '#/lib/ThemeContext'
|
|||||||
import I18nProvider from '#/locale/i18nProvider'
|
import I18nProvider from '#/locale/i18nProvider'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {Provider as A11yProvider} from '#/state/a11y'
|
import {Provider as A11yProvider} from '#/state/a11y'
|
||||||
|
import {
|
||||||
|
prefetchAppConfig,
|
||||||
|
Provider as AppConfigProvider,
|
||||||
|
} from '#/state/appConfig'
|
||||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||||
@@ -79,6 +83,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
|
|||||||
Geo.resolve()
|
Geo.resolve()
|
||||||
prefetchAgeAssuranceConfig()
|
prefetchAgeAssuranceConfig()
|
||||||
prefetchLiveEvents()
|
prefetchLiveEvents()
|
||||||
|
prefetchAppConfig()
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const [isReady, setIsReady] = useState(false)
|
const [isReady, setIsReady] = useState(false)
|
||||||
@@ -202,6 +207,7 @@ function App() {
|
|||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<Geo.Provider>
|
<Geo.Provider>
|
||||||
|
<AppConfigProvider>
|
||||||
<A11yProvider>
|
<A11yProvider>
|
||||||
<OnboardingProvider>
|
<OnboardingProvider>
|
||||||
<AnalyticsContext>
|
<AnalyticsContext>
|
||||||
@@ -227,6 +233,7 @@ function App() {
|
|||||||
</AnalyticsContext>
|
</AnalyticsContext>
|
||||||
</OnboardingProvider>
|
</OnboardingProvider>
|
||||||
</A11yProvider>
|
</A11yProvider>
|
||||||
|
</AppConfigProvider>
|
||||||
</Geo.Provider>
|
</Geo.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {type AppBskyActorDefs} from '@atproto/api'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {isJwtExpired} from '#/lib/jwt'
|
import {isJwtExpired} from '#/lib/jwt'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
@@ -19,6 +18,7 @@ import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
|
|
||||||
export function AccountList({
|
export function AccountList({
|
||||||
onSelectAccount,
|
onSelectAccount,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {getModerationCauseKey} from '#/lib/moderation'
|
import {getModerationCauseKey} from '#/lib/moderation'
|
||||||
import {forceLTR} from '#/lib/strings/bidi'
|
import {forceLTR} from '#/lib/strings/bidi'
|
||||||
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
||||||
@@ -47,6 +46,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {type Metrics} from '#/analytics'
|
import {type Metrics} from '#/analytics'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function Default({
|
export function Default({
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {msg, plural} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {getModerationCauseKey} from '#/lib/moderation'
|
import {getModerationCauseKey} from '#/lib/moderation'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
@@ -34,7 +33,6 @@ import {
|
|||||||
shouldShowKnownFollowers,
|
shouldShowKnownFollowers,
|
||||||
} from '#/components/KnownFollowers'
|
} from '#/components/KnownFollowers'
|
||||||
import {InlineLinkText, Link} from '#/components/Link'
|
import {InlineLinkText, Link} from '#/components/Link'
|
||||||
import {LiveStatus} from '#/components/live/LiveStatusDialog'
|
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Pills from '#/components/Pills'
|
import * as Pills from '#/components/Pills'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
@@ -43,6 +41,8 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {IS_WEB_TOUCH_DEVICE} from '#/env'
|
import {IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
|
import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog'
|
||||||
import {type ProfileHoverCardProps} from './types'
|
import {type ProfileHoverCardProps} from './types'
|
||||||
|
|
||||||
const floatingMiddlewares = [
|
const floatingMiddlewares = [
|
||||||
|
|||||||
Vendored
+10
@@ -141,3 +141,13 @@ export const LIVE_EVENTS_PROD_URL = `https://live-events.workers.bsky.app`
|
|||||||
export const LIVE_EVENTS_URL = IS_DEV
|
export const LIVE_EVENTS_URL = IS_DEV
|
||||||
? (LIVE_EVENTS_DEV_URL ?? LIVE_EVENTS_PROD_URL)
|
? (LIVE_EVENTS_DEV_URL ?? LIVE_EVENTS_PROD_URL)
|
||||||
: LIVE_EVENTS_PROD_URL
|
: LIVE_EVENTS_PROD_URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URLs for the app-config web worker. Can be a
|
||||||
|
* locally running server, see `env.example` for more.
|
||||||
|
*/
|
||||||
|
export const APP_CONFIG_DEV_URL = process.env.APP_CONFIG_DEV_URL
|
||||||
|
export const APP_CONFIG_PROD_URL = `https://app-config.workers.bsky.app`
|
||||||
|
export const APP_CONFIG_URL = IS_DEV
|
||||||
|
? (APP_CONFIG_DEV_URL ?? APP_CONFIG_PROD_URL)
|
||||||
|
: APP_CONFIG_PROD_URL
|
||||||
|
|||||||
+4
-3
@@ -9,6 +9,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {differenceInMinutes} from 'date-fns'
|
import {differenceInMinutes} from 'date-fns'
|
||||||
|
|
||||||
|
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
import {definitelyUrl} from '#/lib/strings/url-helpers'
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
@@ -20,13 +21,13 @@ import * as TextField from '#/components/forms/TextField'
|
|||||||
import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock'
|
import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {LinkPreview} from './LinkPreview'
|
|
||||||
import {
|
import {
|
||||||
|
displayDuration,
|
||||||
useLiveLinkMetaQuery,
|
useLiveLinkMetaQuery,
|
||||||
useRemoveLiveStatusMutation,
|
useRemoveLiveStatusMutation,
|
||||||
useUpsertLiveStatusMutation,
|
useUpsertLiveStatusMutation,
|
||||||
} from './queries'
|
} from '#/features/liveNow'
|
||||||
import {displayDuration, useDebouncedValue} from './utils'
|
import {LinkPreview} from '#/features/liveNow/components/LinkPreview'
|
||||||
|
|
||||||
export function EditLiveDialog({
|
export function EditLiveDialog({
|
||||||
control,
|
control,
|
||||||
+8
-7
@@ -3,28 +3,29 @@ import {View} from 'react-native'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
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 {useLiveNowConfig} from '#/state/service-config'
|
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
|
import {atoms as a, ios, native, platform, useTheme, web} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {
|
|
||||||
displayDuration,
|
|
||||||
getLiveServiceNames,
|
|
||||||
useDebouncedValue,
|
|
||||||
} from '#/components/live/utils'
|
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import * as Select from '#/components/Select'
|
import * as Select from '#/components/Select'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {
|
||||||
|
displayDuration,
|
||||||
|
getLiveServiceNames,
|
||||||
|
useLiveLinkMetaQuery,
|
||||||
|
useLiveNowConfig,
|
||||||
|
useUpsertLiveStatusMutation,
|
||||||
|
} from '#/features/liveNow'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {LinkPreview} from './LinkPreview'
|
import {LinkPreview} from './LinkPreview'
|
||||||
import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries'
|
|
||||||
|
|
||||||
export function GoLiveDialog({
|
export function GoLiveDialog({
|
||||||
control,
|
control,
|
||||||
+3
-3
@@ -17,15 +17,15 @@ import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
|||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
||||||
|
import {Globe_Stroke2_Corner0_Rounded} from '#/components/icons/Globe'
|
||||||
|
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '#/components/icons/SquareArrowTopRight'
|
||||||
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
|
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
|
||||||
import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
|
import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {Globe_Stroke2_Corner0_Rounded} from '../icons/Globe'
|
|
||||||
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '../icons/SquareArrowTopRight'
|
|
||||||
import {LiveIndicator} from './LiveIndicator'
|
|
||||||
|
|
||||||
export function LiveStatusDialog({
|
export function LiveStatusDialog({
|
||||||
control,
|
control,
|
||||||
@@ -1,24 +1,166 @@
|
|||||||
|
import {useMemo} from 'react'
|
||||||
import {
|
import {
|
||||||
type $Typed,
|
type $Typed,
|
||||||
|
type AppBskyActorDefs,
|
||||||
type AppBskyActorStatus,
|
type AppBskyActorStatus,
|
||||||
type AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
|
AtUri,
|
||||||
ComAtprotoRepoPutRecord,
|
ComAtprotoRepoPutRecord,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {retry} from '@atproto/common-web'
|
import {retry} from '@atproto/common-web'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
import {isAfter, parseISO} from 'date-fns'
|
||||||
|
|
||||||
import {uploadBlob} from '#/lib/api'
|
import {uploadBlob} from '#/lib/api'
|
||||||
import {imageToThumb} from '#/lib/api/resolve'
|
import {imageToThumb} from '#/lib/api/resolve'
|
||||||
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
|
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
|
||||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
import {useAppConfig} from '#/state/appConfig'
|
||||||
import {useLiveNowConfig} from '#/state/service-config'
|
import {
|
||||||
|
updateProfileShadow,
|
||||||
|
useMaybeProfileShadow,
|
||||||
|
} from '#/state/cache/profile-shadow'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useDialogContext} from '#/components/Dialog'
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
import {getLiveServiceNames} from '#/components/live/utils'
|
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {getLiveNowHost, getLiveServiceNames} from '#/features/liveNow/utils'
|
||||||
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
|
export * from '#/features/liveNow/utils'
|
||||||
|
|
||||||
|
export const DEFAULT_ALLOWED_DOMAINS = [
|
||||||
|
'twitch.tv',
|
||||||
|
'stream.place',
|
||||||
|
'bluecast.app',
|
||||||
|
]
|
||||||
|
|
||||||
|
export type LiveNowConfig = {
|
||||||
|
canGoLive: boolean
|
||||||
|
currentAccountAllowedHosts: Set<string>
|
||||||
|
defaultAllowedHosts: Set<string>
|
||||||
|
allowedHostsExceptionsByDid: Map<string, Set<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLiveNowConfig(): LiveNowConfig {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const {liveNow} = useAppConfig()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const disabled = ax.features.enabled(ax.features.LiveNowBetaDisable)
|
||||||
|
|
||||||
|
const defaultAllowedHosts = new Set(
|
||||||
|
DEFAULT_ALLOWED_DOMAINS.concat(liveNow.allow),
|
||||||
|
)
|
||||||
|
const allowedHostsExceptionsByDid = new Map<string, Set<string>>()
|
||||||
|
for (const ex of liveNow.exceptions) {
|
||||||
|
allowedHostsExceptionsByDid.set(
|
||||||
|
ex.did,
|
||||||
|
new Set(DEFAULT_ALLOWED_DOMAINS.concat(ex.allow)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentAccount?.did || disabled) {
|
||||||
|
return {
|
||||||
|
canGoLive: false,
|
||||||
|
currentAccountAllowedHosts: new Set(),
|
||||||
|
defaultAllowedHosts,
|
||||||
|
allowedHostsExceptionsByDid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canGoLive: true,
|
||||||
|
currentAccountAllowedHosts:
|
||||||
|
allowedHostsExceptionsByDid.get(currentAccount.did) ??
|
||||||
|
defaultAllowedHosts,
|
||||||
|
defaultAllowedHosts,
|
||||||
|
allowedHostsExceptionsByDid,
|
||||||
|
}
|
||||||
|
}, [ax, liveNow, currentAccount])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
|
||||||
|
const shadowed = useMaybeProfileShadow(actor)
|
||||||
|
const tick = useTickEveryMinute()
|
||||||
|
const config = useLiveNowConfig()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
void tick // revalidate every minute
|
||||||
|
|
||||||
|
if (shadowed && 'status' in shadowed && shadowed.status) {
|
||||||
|
const isValid = isStatusValidForViewers(shadowed.status, config)
|
||||||
|
const isDisabled = shadowed.status.isDisabled || false
|
||||||
|
const isActive = isStatusStillActive(shadowed.status.expiresAt)
|
||||||
|
if (isValid && !isDisabled && isActive) {
|
||||||
|
return {
|
||||||
|
uri: shadowed.status.uri,
|
||||||
|
cid: shadowed.status.cid,
|
||||||
|
isDisabled: false,
|
||||||
|
isActive: true,
|
||||||
|
status: 'app.bsky.actor.status#live',
|
||||||
|
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
|
||||||
|
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
|
||||||
|
record: shadowed.status.record,
|
||||||
|
} satisfies AppBskyActorDefs.StatusView
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uri: shadowed.status.uri,
|
||||||
|
cid: shadowed.status.cid,
|
||||||
|
isDisabled,
|
||||||
|
isActive: false,
|
||||||
|
status: 'app.bsky.actor.status#live',
|
||||||
|
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
|
||||||
|
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
|
||||||
|
record: shadowed.status.record,
|
||||||
|
} satisfies AppBskyActorDefs.StatusView
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: '',
|
||||||
|
isDisabled: false,
|
||||||
|
isActive: false,
|
||||||
|
record: {},
|
||||||
|
} satisfies AppBskyActorDefs.StatusView
|
||||||
|
}
|
||||||
|
}, [shadowed, config, tick])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStatusStillActive(timeStr: string | undefined) {
|
||||||
|
if (!timeStr) return false
|
||||||
|
const now = new Date()
|
||||||
|
const expiry = parseISO(timeStr)
|
||||||
|
|
||||||
|
return isAfter(expiry, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether the live status is valid for display in the app. Does NOT
|
||||||
|
* validate if the status is valid for the acting user e.g. as they go live.
|
||||||
|
*/
|
||||||
|
export function isStatusValidForViewers(
|
||||||
|
status: AppBskyActorDefs.StatusView,
|
||||||
|
config: LiveNowConfig,
|
||||||
|
) {
|
||||||
|
if (status.status !== 'app.bsky.actor.status#live') return false
|
||||||
|
if (!status.uri) return false // should not happen, just backwards compat
|
||||||
|
try {
|
||||||
|
const {host: liveDid} = new AtUri(status.uri)
|
||||||
|
if (AppBskyEmbedExternal.isView(status.embed)) {
|
||||||
|
const host = getLiveNowHost(status.embed.external.uri)
|
||||||
|
const exception = config.allowedHostsExceptionsByDid.get(liveDid)
|
||||||
|
const isValidException = exception ? exception.has(host) : false
|
||||||
|
const isValidForAnyone = config.defaultAllowedHosts.has(host)
|
||||||
|
return isValidException || isValidForAnyone
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useLiveLinkMetaQuery(url: string | null) {
|
export function useLiveLinkMetaQuery(url: string | null) {
|
||||||
const liveNowConfig = useLiveNowConfig()
|
const liveNowConfig = useLiveNowConfig()
|
||||||
@@ -30,8 +172,8 @@ export function useLiveLinkMetaQuery(url: string | null) {
|
|||||||
queryKey: ['link-meta', url],
|
queryKey: ['link-meta', url],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!url) return undefined
|
if (!url) return undefined
|
||||||
const urlp = new URL(url)
|
const host = getLiveNowHost(url)
|
||||||
if (!liveNowConfig.currentAccountAllowedHosts.has(urlp.hostname)) {
|
if (!liveNowConfig.currentAccountAllowedHosts.has(host)) {
|
||||||
const {formatted} = getLiveServiceNames(
|
const {formatted} = getLiveServiceNames(
|
||||||
liveNowConfig.currentAccountAllowedHosts,
|
liveNowConfig.currentAccountAllowedHosts,
|
||||||
)
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {useEffect, useState} from 'react'
|
|
||||||
import {type I18n} from '@lingui/core'
|
import {type I18n} from '@lingui/core'
|
||||||
import {plural} from '@lingui/macro'
|
import {plural} from '@lingui/macro'
|
||||||
|
import psl from 'psl'
|
||||||
|
|
||||||
export function displayDuration(i18n: I18n, durationInMinutes: number) {
|
export function displayDuration(i18n: I18n, durationInMinutes: number) {
|
||||||
const roundedDurationInMinutes = Math.round(durationInMinutes)
|
const roundedDurationInMinutes = Math.round(durationInMinutes)
|
||||||
@@ -24,41 +24,49 @@ export function displayDuration(i18n: I18n, durationInMinutes: number) {
|
|||||||
: minutesString
|
: minutesString
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trailing debounce
|
|
||||||
export function useDebouncedValue<T>(val: T, delayMs: number): T {
|
|
||||||
const [prev, setPrev] = useState(val)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setTimeout(() => setPrev(val), delayMs)
|
|
||||||
return () => clearTimeout(timeout)
|
|
||||||
}, [val, delayMs])
|
|
||||||
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceUrlToNameMap: Record<string, string> = {
|
const serviceUrlToNameMap: Record<string, string> = {
|
||||||
'twitch.tv': 'Twitch',
|
'twitch.tv': 'Twitch',
|
||||||
'www.twitch.tv': 'Twitch',
|
|
||||||
'youtube.com': 'YouTube',
|
'youtube.com': 'YouTube',
|
||||||
'www.youtube.com': 'YouTube',
|
|
||||||
'youtu.be': 'YouTube',
|
|
||||||
'nba.com': 'NBA',
|
'nba.com': 'NBA',
|
||||||
'www.nba.com': 'NBA',
|
|
||||||
'nba.smart.link': 'nba.smart.link',
|
'nba.smart.link': 'nba.smart.link',
|
||||||
'espn.com': 'ESPN',
|
'espn.com': 'ESPN',
|
||||||
'www.espn.com': 'ESPN',
|
|
||||||
'stream.place': 'Streamplace',
|
'stream.place': 'Streamplace',
|
||||||
'skylight.social': 'Skylight',
|
'skylight.social': 'Skylight',
|
||||||
'bluecast.app': 'Bluecast',
|
'bluecast.app': 'Bluecast',
|
||||||
'www.bluecast.app': 'Bluecast',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLiveServiceNames(domains: Set<string>) {
|
export function getLiveServiceNames(domains: Set<string>) {
|
||||||
const names = Array.from(
|
const names = Array.from(
|
||||||
new Set(Array.from(domains.values()).map(d => serviceUrlToNameMap[d] || d)),
|
new Set(
|
||||||
|
Array.from(domains.values())
|
||||||
|
.map(d => sanitizeLiveNowHost(d))
|
||||||
|
.map(d => serviceUrlToNameMap[d] || d),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
names,
|
names,
|
||||||
formatted: names.join(', '),
|
formatted: names.join(', '),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeLiveNowHost(hostname: string) {
|
||||||
|
// special case this one
|
||||||
|
if (hostname === 'nba.smart.link') {
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
|
const parsed = psl.parse(hostname)
|
||||||
|
if (parsed.error || !parsed.listed || !parsed.domain) {
|
||||||
|
// fall back to dumb version
|
||||||
|
return hostname.replace(/^www\./, '')
|
||||||
|
}
|
||||||
|
return parsed.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the apex domain from a given URL, for use when matching allowed
|
||||||
|
* Live Now hosts.
|
||||||
|
*/
|
||||||
|
export function getLiveNowHost(url: string) {
|
||||||
|
const {hostname} = new URL(url)
|
||||||
|
return sanitizeLiveNowHost(hostname)
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import {useMemo} from 'react'
|
|
||||||
import {
|
|
||||||
type $Typed,
|
|
||||||
type AppBskyActorDefs,
|
|
||||||
AppBskyEmbedExternal,
|
|
||||||
AtUri,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {isAfter, parseISO} from 'date-fns'
|
|
||||||
|
|
||||||
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
|
||||||
import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config'
|
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
|
||||||
import type * as bsky from '#/types/bsky'
|
|
||||||
|
|
||||||
export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
|
|
||||||
const shadowed = useMaybeProfileShadow(actor)
|
|
||||||
const tick = useTickEveryMinute()
|
|
||||||
const config = useLiveNowConfig()
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
void tick // revalidate every minute
|
|
||||||
|
|
||||||
if (shadowed && 'status' in shadowed && shadowed.status) {
|
|
||||||
const isValid = isStatusValidForViewers(shadowed.status, config)
|
|
||||||
const isDisabled = shadowed.status.isDisabled || false
|
|
||||||
const isActive = isStatusStillActive(shadowed.status.expiresAt)
|
|
||||||
if (isValid && !isDisabled && isActive) {
|
|
||||||
return {
|
|
||||||
uri: shadowed.status.uri,
|
|
||||||
cid: shadowed.status.cid,
|
|
||||||
isDisabled: false,
|
|
||||||
isActive: true,
|
|
||||||
status: 'app.bsky.actor.status#live',
|
|
||||||
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
|
|
||||||
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
|
|
||||||
record: shadowed.status.record,
|
|
||||||
} satisfies AppBskyActorDefs.StatusView
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
uri: shadowed.status.uri,
|
|
||||||
cid: shadowed.status.cid,
|
|
||||||
isDisabled,
|
|
||||||
isActive: false,
|
|
||||||
status: 'app.bsky.actor.status#live',
|
|
||||||
embed: shadowed.status.embed as $Typed<AppBskyEmbedExternal.View>, // temp_isStatusValid asserts this
|
|
||||||
expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this
|
|
||||||
record: shadowed.status.record,
|
|
||||||
} satisfies AppBskyActorDefs.StatusView
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
status: '',
|
|
||||||
isDisabled: false,
|
|
||||||
isActive: false,
|
|
||||||
record: {},
|
|
||||||
} satisfies AppBskyActorDefs.StatusView
|
|
||||||
}
|
|
||||||
}, [shadowed, config, tick])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isStatusStillActive(timeStr: string | undefined) {
|
|
||||||
if (!timeStr) return false
|
|
||||||
const now = new Date()
|
|
||||||
const expiry = parseISO(timeStr)
|
|
||||||
|
|
||||||
return isAfter(expiry, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates whether the live status is valid for display in the app. Does NOT
|
|
||||||
* validate if the status is valid for the acting user e.g. as they go live.
|
|
||||||
*/
|
|
||||||
export function isStatusValidForViewers(
|
|
||||||
status: AppBskyActorDefs.StatusView,
|
|
||||||
config: LiveNowConfig,
|
|
||||||
) {
|
|
||||||
if (status.status !== 'app.bsky.actor.status#live') return false
|
|
||||||
if (!status.uri) return false // should not happen, just backwards compat
|
|
||||||
try {
|
|
||||||
const {host: liveDid} = new AtUri(status.uri)
|
|
||||||
if (AppBskyEmbedExternal.isView(status.embed)) {
|
|
||||||
const url = new URL(status.embed.external.uri)
|
|
||||||
const exception = config.allowedHostsExceptionsByDid.get(liveDid)
|
|
||||||
const isValidException = exception ? exception.has(url.hostname) : false
|
|
||||||
const isValidForAnyone = config.defaultAllowedHosts.has(url.hostname)
|
|
||||||
return isValidException || isValidForAnyone
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,7 @@ export async function retry<P>(
|
|||||||
export async function networkRetry<P>(
|
export async function networkRetry<P>(
|
||||||
retries: number,
|
retries: number,
|
||||||
fn: () => Promise<P>,
|
fn: () => Promise<P>,
|
||||||
|
delay?: number,
|
||||||
): Promise<P> {
|
): Promise<P> {
|
||||||
return retry(retries, isNetworkError, fn)
|
return retry(retries, isNetworkError, fn, delay)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a debounced version of the input value that only updates after the
|
||||||
|
* specified delay has passed without any changes to the input value.
|
||||||
|
*/
|
||||||
|
export function useDebouncedValue<T>(val: T, delayMs: number): T {
|
||||||
|
const [prev, setPrev] = useState(val)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => setPrev(val), delayMs)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [val, delayMs])
|
||||||
|
|
||||||
|
return prev
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import {msg, Plural, Trans} from '@lingui/macro'
|
import {msg, Plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {useTranslate} from '#/lib/hooks/useTranslate'
|
import {useTranslate} from '#/lib/hooks/useTranslate'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
@@ -60,6 +59,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||||
import {WhoCanReply} from '#/components/WhoCanReply'
|
import {WhoCanReply} from '#/components/WhoCanReply'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function ThreadItemAnchor({
|
export function ThreadItemAnchor({
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {MAX_POST_LINES} from '#/lib/constants'
|
import {MAX_POST_LINES} from '#/lib/constants'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
@@ -44,6 +43,7 @@ import {RichText} from '#/components/RichText'
|
|||||||
import * as Skele from '#/components/Skeleton'
|
import * as Skele from '#/components/Skeleton'
|
||||||
import {SubtleHover} from '#/components/SubtleHover'
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
|
|
||||||
export type ThreadItemPostProps = {
|
export type ThreadItemPostProps = {
|
||||||
item: Extract<ThreadItem, {type: 'threadPost'}>
|
item: Extract<ThreadItem, {type: 'threadPost'}>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
@@ -39,6 +38,7 @@ import * as Toast from '#/components/Toast'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {GermButton} from '../components/GermButton'
|
import {GermButton} from '../components/GermButton'
|
||||||
import {EditProfileDialog} from './EditProfileDialog'
|
import {EditProfileDialog} from './EditProfileDialog'
|
||||||
import {ProfileHeaderHandle} from './Handle'
|
import {ProfileHeaderHandle} from './Handle'
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {BACK_HITSLOP} from '#/lib/constants'
|
import {BACK_HITSLOP} from '#/lib/constants'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
@@ -28,13 +27,14 @@ import {atoms as a, platform, useTheme} from '#/alf'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
||||||
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
|
||||||
import {LiveIndicator} from '#/components/live/LiveIndicator'
|
|
||||||
import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
|
|
||||||
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
||||||
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
|
import {EditLiveDialog} from '#/features/liveNow/components/EditLiveDialog'
|
||||||
|
import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator'
|
||||||
|
import {LiveStatusDialog} from '#/features/liveNow/components/LiveStatusDialog'
|
||||||
import {GrowableAvatar} from './GrowableAvatar'
|
import {GrowableAvatar} from './GrowableAvatar'
|
||||||
import {GrowableBanner} from './GrowableBanner'
|
import {GrowableBanner} from './GrowableBanner'
|
||||||
import {StatusBarShadow} from './StatusBarShadow'
|
import {StatusBarShadow} from './StatusBarShadow'
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {HELP_DESK_URL} from '#/lib/constants'
|
import {HELP_DESK_URL} from '#/lib/constants'
|
||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
|
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
|
||||||
@@ -70,6 +69,7 @@ import {
|
|||||||
} from '#/components/verification/VerificationCheckButton'
|
} from '#/components/verification/VerificationCheckButton'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env'
|
import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {device, useStorage} from '#/storage'
|
import {device, useStorage} from '#/storage'
|
||||||
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
|
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import {createContext, useContext} from 'react'
|
||||||
|
import {QueryClient, useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {APP_CONFIG_URL} from '#/env'
|
||||||
|
|
||||||
|
const qc = new QueryClient()
|
||||||
|
const appConfigQueryKey = ['app-config']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the types defined in our `app-config` worker
|
||||||
|
*/
|
||||||
|
type AppConfigResponse = {
|
||||||
|
liveNow: {
|
||||||
|
allow: string[]
|
||||||
|
exceptions: {
|
||||||
|
did: string
|
||||||
|
allow: string[]
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_APP_CONFIG_RESPONSE: AppConfigResponse = {
|
||||||
|
liveNow: {
|
||||||
|
allow: [],
|
||||||
|
exceptions: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchAppConfigPromise: Promise<AppConfigResponse> | undefined
|
||||||
|
|
||||||
|
async function fetchAppConfig(): Promise<AppConfigResponse | null> {
|
||||||
|
try {
|
||||||
|
if (!fetchAppConfigPromise) {
|
||||||
|
fetchAppConfigPromise = (async () => {
|
||||||
|
const r = await fetch(`${APP_CONFIG_URL}/config`)
|
||||||
|
if (!r.ok) throw new Error(await r.text())
|
||||||
|
const data = await r.json()
|
||||||
|
return data
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
return await fetchAppConfigPromise
|
||||||
|
} catch (e) {
|
||||||
|
fetchAppConfigPromise = undefined
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = createContext<AppConfigResponse>(DEFAULT_APP_CONFIG_RESPONSE)
|
||||||
|
|
||||||
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const {data} = useQuery<AppConfigResponse | null>(
|
||||||
|
{
|
||||||
|
staleTime: Infinity,
|
||||||
|
queryKey: appConfigQueryKey,
|
||||||
|
refetchInterval: query => {
|
||||||
|
// refetch regularly if fetch failed, otherwise never refetch
|
||||||
|
return query.state.status === 'error' ? 60e3 : Infinity
|
||||||
|
},
|
||||||
|
async queryFn() {
|
||||||
|
return fetchAppConfig()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
qc,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Context.Provider value={data ?? DEFAULT_APP_CONFIG_RESPONSE}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prefetchAppConfig() {
|
||||||
|
try {
|
||||||
|
const data = await fetchAppConfig()
|
||||||
|
if (data) {
|
||||||
|
qc.setQueryData(appConfigQueryKey, data)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppConfig() {
|
||||||
|
const ctx = useContext(Context)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('useAppConfig must be used within a Provider')
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
BSKY_SERVICE_DID,
|
BSKY_SERVICE_DID,
|
||||||
PUBLIC_BSKY_SERVICE,
|
PUBLIC_BSKY_SERVICE,
|
||||||
} from '#/lib/constants'
|
} from '#/lib/constants'
|
||||||
|
import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue'
|
||||||
import {createFullHandle} from '#/lib/strings/handles'
|
import {createFullHandle} from '#/lib/strings/handles'
|
||||||
import {useDebouncedValue} from '#/components/live/utils'
|
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
import {Agent} from '../session/agent'
|
import {Agent} from '../session/agent'
|
||||||
|
|||||||
@@ -2,28 +2,17 @@ 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 {useSession} from '#/state/session'
|
|
||||||
import {useAnalytics} from '#/analytics'
|
|
||||||
import {IS_DEV} from '#/env'
|
|
||||||
import {device} from '#/storage'
|
import {device} from '#/storage'
|
||||||
|
|
||||||
type TrendingContext = {
|
type TrendingContext = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type LiveNowContext = {
|
|
||||||
did: string
|
|
||||||
domains: string[]
|
|
||||||
}[]
|
|
||||||
|
|
||||||
const TrendingContext = createContext<TrendingContext>({
|
const TrendingContext = createContext<TrendingContext>({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
})
|
})
|
||||||
TrendingContext.displayName = 'TrendingContext'
|
TrendingContext.displayName = 'TrendingContext'
|
||||||
|
|
||||||
const LiveNowContext = createContext<LiveNowContext>([])
|
|
||||||
LiveNowContext.displayName = 'LiveNowContext'
|
|
||||||
|
|
||||||
const CheckEmailConfirmedContext = createContext<boolean | null>(null)
|
const CheckEmailConfirmedContext = createContext<boolean | null>(null)
|
||||||
|
|
||||||
export function Provider({children}: {children: React.ReactNode}) {
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
@@ -60,19 +49,15 @@ export function Provider({children}: {children: React.ReactNode}) {
|
|||||||
return {enabled}
|
return {enabled}
|
||||||
}, [isInitialLoad, config, langPrefs.contentLanguages])
|
}, [isInitialLoad, config, langPrefs.contentLanguages])
|
||||||
|
|
||||||
const liveNow = useMemo<LiveNowContext>(() => config?.liveNow ?? [], [config])
|
|
||||||
|
|
||||||
// probably true, so default to true when loading
|
// probably true, so default to true when loading
|
||||||
// if the call fails, the query will set it to false for us
|
// if the call fails, the query will set it to false for us
|
||||||
const checkEmailConfirmed = config?.checkEmailConfirmed ?? true
|
const checkEmailConfirmed = config?.checkEmailConfirmed ?? true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrendingContext.Provider value={trending}>
|
<TrendingContext.Provider value={trending}>
|
||||||
<LiveNowContext.Provider value={liveNow}>
|
|
||||||
<CheckEmailConfirmedContext.Provider value={checkEmailConfirmed}>
|
<CheckEmailConfirmedContext.Provider value={checkEmailConfirmed}>
|
||||||
{children}
|
{children}
|
||||||
</CheckEmailConfirmedContext.Provider>
|
</CheckEmailConfirmedContext.Provider>
|
||||||
</LiveNowContext.Provider>
|
|
||||||
</TrendingContext.Provider>
|
</TrendingContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -81,55 +66,6 @@ export function useTrendingConfig() {
|
|||||||
return useContext(TrendingContext)
|
return useContext(TrendingContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_LIVE_ALLOWED_DOMAINS = [
|
|
||||||
'twitch.tv',
|
|
||||||
'www.twitch.tv',
|
|
||||||
'stream.place',
|
|
||||||
'bluecast.app',
|
|
||||||
'www.bluecast.app',
|
|
||||||
]
|
|
||||||
export type LiveNowConfig = {
|
|
||||||
currentAccountAllowedHosts: Set<string>
|
|
||||||
defaultAllowedHosts: Set<string>
|
|
||||||
allowedHostsExceptionsByDid: Map<string, Set<string>>
|
|
||||||
}
|
|
||||||
export function useLiveNowConfig(): LiveNowConfig {
|
|
||||||
const ctx = useContext(LiveNowContext)
|
|
||||||
const canGoLive = useCanGoLive()
|
|
||||||
const {currentAccount} = useSession()
|
|
||||||
return useMemo(() => {
|
|
||||||
const defaultAllowedHosts = new Set(DEFAULT_LIVE_ALLOWED_DOMAINS)
|
|
||||||
const allowedHostsExceptionsByDid = new Map<string, Set<string>>()
|
|
||||||
for (const live of ctx) {
|
|
||||||
allowedHostsExceptionsByDid.set(
|
|
||||||
live.did,
|
|
||||||
new Set(DEFAULT_LIVE_ALLOWED_DOMAINS.concat(live.domains)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!currentAccount?.did || !canGoLive)
|
|
||||||
return {
|
|
||||||
currentAccountAllowedHosts: new Set(),
|
|
||||||
defaultAllowedHosts,
|
|
||||||
allowedHostsExceptionsByDid,
|
|
||||||
}
|
|
||||||
const vip = ctx.find(live => live.did === currentAccount.did)
|
|
||||||
return {
|
|
||||||
currentAccountAllowedHosts: new Set(
|
|
||||||
DEFAULT_LIVE_ALLOWED_DOMAINS.concat(vip ? vip.domains : []),
|
|
||||||
),
|
|
||||||
defaultAllowedHosts,
|
|
||||||
allowedHostsExceptionsByDid,
|
|
||||||
}
|
|
||||||
}, [ctx, currentAccount, canGoLive])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCanGoLive() {
|
|
||||||
const ax = useAnalytics()
|
|
||||||
const {hasSession} = useSession()
|
|
||||||
if (!hasSession) return false
|
|
||||||
return IS_DEV ? true : !ax.features.enabled(ax.features.LiveNowBetaDisable)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCheckEmailConfirmed() {
|
export function useCheckEmailConfirmed() {
|
||||||
const ctx = useContext(CheckEmailConfirmedContext)
|
const ctx = useContext(CheckEmailConfirmedContext)
|
||||||
if (ctx === null) {
|
if (ctx === null) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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, isStatusValidForViewers} 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 {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
@@ -40,7 +39,6 @@ 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 {useSelectedFeed} from '#/state/shell/selected-feed'
|
import {useSelectedFeed} from '#/state/shell/selected-feed'
|
||||||
@@ -63,6 +61,11 @@ import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interst
|
|||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner'
|
import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner'
|
||||||
|
import {
|
||||||
|
isStatusStillActive,
|
||||||
|
isStatusValidForViewers,
|
||||||
|
useLiveNowConfig,
|
||||||
|
} from '#/features/liveNow'
|
||||||
import {ComposerPrompt} from '../feeds/ComposerPrompt'
|
import {ComposerPrompt} from '../feeds/ComposerPrompt'
|
||||||
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||||
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {type ReasonFeedSource} from '#/lib/api/feed/types'
|
import {type ReasonFeedSource} from '#/lib/api/feed/types'
|
||||||
import {MAX_POST_LINES} from '#/lib/constants'
|
import {MAX_POST_LINES} from '#/lib/constants'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
@@ -48,6 +47,7 @@ import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
|||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {SubtleHover} from '#/components/SubtleHover'
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
import {PostFeedReason} from './PostFeedReason'
|
import {PostFeedReason} from './PostFeedReason'
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
@@ -20,7 +19,6 @@ 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'
|
||||||
@@ -47,9 +45,6 @@ import {
|
|||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
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 {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
|
|
||||||
import {GoLiveDialog} from '#/components/live/GoLiveDialog'
|
|
||||||
import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog'
|
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {
|
import {
|
||||||
ReportDialog,
|
ReportDialog,
|
||||||
@@ -61,6 +56,10 @@ import {VerificationCreatePrompt} from '#/components/verification/VerificationCr
|
|||||||
import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
|
import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
|
import {useActorStatus, useLiveNowConfig} from '#/features/liveNow'
|
||||||
|
import {EditLiveDialog} from '#/features/liveNow/components/EditLiveDialog'
|
||||||
|
import {GoLiveDialog} from '#/features/liveNow/components/GoLiveDialog'
|
||||||
|
import {GoLiveDisabledDialog} from '#/features/liveNow/components/GoLiveDisabledDialog'
|
||||||
import {Dot} from '#/features/nuxs/components/Dot'
|
import {Dot} from '#/features/nuxs/components/Dot'
|
||||||
import {Gradient} from '#/features/nuxs/components/Gradient'
|
import {Gradient} from '#/features/nuxs/components/Gradient'
|
||||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||||
@@ -85,7 +84,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()
|
const {canGoLive} = useLiveNowConfig()
|
||||||
const status = useActorStatus(profile)
|
const status = useActorStatus(profile)
|
||||||
const statusNudge = useNux(Nux.LiveNowBetaNudge)
|
const statusNudge = useNux(Nux.LiveNowBetaNudge)
|
||||||
const statusNudgeActive =
|
const statusNudgeActive =
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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 {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {forceLTR} from '#/lib/strings/bidi'
|
import {forceLTR} from '#/lib/strings/bidi'
|
||||||
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
||||||
@@ -21,6 +20,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {IS_ANDROID} from '#/env'
|
import {IS_ANDROID} from '#/env'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {TimeElapsed} from './TimeElapsed'
|
import {TimeElapsed} from './TimeElapsed'
|
||||||
import {PreviewableUserAvatar} from './UserAvatar'
|
import {PreviewableUserAvatar} from './UserAvatar'
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {msg, Trans} 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 {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {
|
import {
|
||||||
useCameraPermission,
|
useCameraPermission,
|
||||||
@@ -47,13 +46,14 @@ import {
|
|||||||
import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive'
|
import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {LiveIndicator} from '#/components/live/LiveIndicator'
|
|
||||||
import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
|
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_ANDROID, IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
import {IS_ANDROID, IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
|
import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator'
|
||||||
|
import {LiveStatusDialog} from '#/features/liveNow/components/LiveStatusDialog'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
|
export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {msg, Plural, plural, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
|
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
|
||||||
import {type PressableScale} from '#/lib/custom-animations/PressableScale'
|
import {type PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||||
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
|
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
|
||||||
@@ -57,6 +56,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
|
|
||||||
const iconWidth = 26
|
const iconWidth = 26
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {type BottomTabBarProps} from '@react-navigation/bottom-tabs'
|
import {type BottomTabBarProps} from '@react-navigation/bottom-tabs'
|
||||||
import {StackActions} from '@react-navigation/native'
|
import {StackActions} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||||
import {BOTTOM_BAR_AVI} from '#/lib/demo'
|
import {BOTTOM_BAR_AVI} from '#/lib/demo'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
@@ -49,6 +48,7 @@ import {
|
|||||||
Message_Stroke2_Corner0_Rounded_Filled as MessageFilled,
|
Message_Stroke2_Corner0_Rounded_Filled as MessageFilled,
|
||||||
} from '#/components/icons/Message'
|
} from '#/components/icons/Message'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {useDemoMode} from '#/storage/hooks/demo-mode'
|
import {useDemoMode} from '#/storage/hooks/demo-mode'
|
||||||
import {styles} from './BottomBarStyles'
|
import {styles} from './BottomBarStyles'
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {msg, plural, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useActorStatus} from '#/lib/actor-status'
|
|
||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
@@ -74,6 +73,7 @@ import {CENTER_COLUMN_OFFSET} from '#/components/Layout'
|
|||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
||||||
import {router} from '../../../routes'
|
import {router} from '../../../routes'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user