Merge remote-tracking branch 'origin/main' into APP-2833
* origin/main: (94 commits) Create moderation inbox menu item and feature gate (#11552) Add OP thread numbering to feed posts (#11472) [APP-2864] Use hydrated known likers (#11541) Enable React Native strict TypeScript API (#11459) Nightly source-language update Bump the actions group with 2 updates (#11537) Update tech stack versions and fix markdown in CLAUDE.md (#11530) Send an integer exp for video upload service auth tokens (#11529) Fix iOS message input height after sending (#11533) Revert "Send beta user state with AppView requests (#11474)" (#11510) Overfetch trending topics before filtering (#11477) [APP-2922] Fix cached starter pack navigation (#11531) Nightly source-language update Explain trending topics in options menu (#11521) Nightly source-language update Adds a small padding for labeller avatars in group chat 'Add people' selector (#11422) [AA] Set `maxAge: Infinity` on age assurance cache (#11526) Reland "Bump React Compiler to stable 1.0.0" (#11525) Fix hairline divider not visible (#11518) Back to v1.132.0 (#11522) ...
This commit is contained in:
+9
-3
@@ -87,6 +87,7 @@ import {MessagesJoinRequestsScreen} from '#/screens/Messages/JoinRequests'
|
||||
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
|
||||
import {ModerationInboxScreen} from '#/screens/ModerationInbox'
|
||||
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
||||
import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'
|
||||
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
||||
@@ -178,6 +179,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
getComponent={() => ModerationScreen}
|
||||
options={{title: title(msg`Moderation`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ModerationInbox"
|
||||
getComponent={() => ModerationInboxScreen}
|
||||
options={{title: title(msg`Moderation inbox`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ModerationModlists"
|
||||
getComponent={() => ModerationModlistsScreen}
|
||||
@@ -919,7 +925,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
// chat-removed-from-group, chat-join-request-rejected: the convo is
|
||||
// no longer accessible to the recipient, so just open the list.
|
||||
// @ts-expect-error nested navigators aren't typed -sfn
|
||||
navigate('MessagesTab', {screen: 'Messages'})
|
||||
void navigate('MessagesTab', {screen: 'Messages'})
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1006,7 +1012,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
ax.metric('init', {
|
||||
initMs: Math.round(
|
||||
// @ts-ignore Emitted by Metro in the bundle prelude
|
||||
// @ts-expect-error Emitted by Metro in the bundle prelude
|
||||
performance.now() - global.__BUNDLE_START_TIME__,
|
||||
),
|
||||
})
|
||||
@@ -1081,7 +1087,7 @@ function navigate<K extends keyof AllNavigatorParams>(
|
||||
}
|
||||
navigationRef.addListener('state', handler)
|
||||
|
||||
// @ts-ignore I don't know what would make typescript happy but I have a life -prf
|
||||
// @ts-expect-error I don't know what would make typescript happy but I have a life -prf
|
||||
navigationRef.navigate(name, params)
|
||||
}),
|
||||
timeout(1e3),
|
||||
|
||||
+15
-13
@@ -2,7 +2,6 @@ import {forwardRef, useCallback, useEffect, useState} from 'react'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
Image as RNImage,
|
||||
StyleSheet,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from 'react-native'
|
||||
@@ -20,14 +19,16 @@ import {Image} from 'expo-image'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
|
||||
import {Logotype} from '#/view/icons/Logotype'
|
||||
// @ts-ignore
|
||||
import {atoms as a} from '#/alf'
|
||||
// @ts-expect-error
|
||||
import splashImagePointer from '../assets/splash/splash.png'
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
import darkSplashImagePointer from '../assets/splash/splash-dark.png'
|
||||
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
||||
|
||||
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer)!.uri
|
||||
const darkSplashImageUri = RNImage.resolveAssetSource(
|
||||
darkSplashImagePointer,
|
||||
).uri
|
||||
)!.uri
|
||||
|
||||
export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
|
||||
const width = 1000
|
||||
@@ -35,7 +36,7 @@ export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
// @ts-ignore it's fiiiiine
|
||||
// @ts-expect-error it's fiiiiine
|
||||
ref={ref}
|
||||
viewBox="0 0 64 66"
|
||||
style={[{width, height}, props.style]}>
|
||||
@@ -124,14 +125,14 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
if (isReady) {
|
||||
SplashScreen.hideAsync()
|
||||
.then(() => {
|
||||
intro.set(() =>
|
||||
intro.set(
|
||||
withTiming(
|
||||
1,
|
||||
{duration: 400, easing: Easing.out(Easing.cubic)},
|
||||
() => {
|
||||
'worklet'
|
||||
// set these values to check animation at specific point
|
||||
outroLogo.set(() =>
|
||||
outroLogo.set(
|
||||
withTiming(
|
||||
1,
|
||||
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
||||
@@ -140,13 +141,13 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
},
|
||||
),
|
||||
)
|
||||
outroApp.set(() =>
|
||||
outroApp.set(
|
||||
withTiming(1, {
|
||||
duration: 1200,
|
||||
easing: Easing.inOut(Easing.cubic),
|
||||
}),
|
||||
)
|
||||
outroAppOpacity.set(() =>
|
||||
outroAppOpacity.set(
|
||||
withTiming(1, {
|
||||
duration: 1200,
|
||||
easing: Easing.in(Easing.cubic),
|
||||
@@ -170,12 +171,12 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<View style={{flex: 1}} onLayout={onLayout}>
|
||||
{!isAnimationComplete && (
|
||||
<View style={StyleSheet.absoluteFillObject}>
|
||||
<View style={[a.absolute, a.inset_0]}>
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
onLoadEnd={onLoadEnd}
|
||||
source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
@@ -205,7 +206,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
{!isAnimationComplete && (
|
||||
<Animated.View
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
logoAnimation,
|
||||
{
|
||||
flex: 1,
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import Svg, {Path} from 'react-native-svg'
|
||||
|
||||
import {atoms as a, flatten} from '#/alf'
|
||||
import {atoms as a, flattenToCSS} from '#/alf'
|
||||
|
||||
const size = 100
|
||||
const ratio = 57 / 64
|
||||
@@ -72,7 +72,7 @@ export function Splash({
|
||||
{!isAnimationComplete && (
|
||||
<div
|
||||
ref={splashRef}
|
||||
style={flatten([
|
||||
style={flattenToCSS([
|
||||
a.fixed,
|
||||
a.inset_0,
|
||||
a.flex,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {Trans} from '@lingui/react/macro'
|
||||
import {retry} from '#/lib/async/retry'
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useAppviewClient, useSession} from '#/state/session'
|
||||
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
|
||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -149,7 +149,6 @@ export function RedirectOverlay() {
|
||||
t.atoms.bg,
|
||||
gtMobile ? a.p_2xl : a.p_xl,
|
||||
a.align_center,
|
||||
// @ts-ignore
|
||||
platform({
|
||||
web: {
|
||||
paddingTop: '35vh',
|
||||
@@ -176,7 +175,7 @@ function Inner() {
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const appviewClient = useAppviewClient()
|
||||
const polling = useRef(false)
|
||||
const unmounted = useRef(false)
|
||||
const [error, setError] = useState(false)
|
||||
@@ -196,10 +195,10 @@ function Inner() {
|
||||
5,
|
||||
() => true,
|
||||
async () => {
|
||||
if (!agent.session) return
|
||||
if (!appviewClient.did) return
|
||||
if (unmounted.current) return
|
||||
|
||||
const data = await refetchAgeAssuranceServerState({agent})
|
||||
const data = await refetchAgeAssuranceServerState({appviewClient})
|
||||
|
||||
if (data?.state.status !== 'assured') {
|
||||
throw new Error(
|
||||
@@ -214,7 +213,7 @@ function Inner() {
|
||||
)
|
||||
.then(async data => {
|
||||
if (!data) return
|
||||
if (!agent.session) return
|
||||
if (!appviewClient.did) return
|
||||
if (unmounted.current) return
|
||||
|
||||
setSuccess(true)
|
||||
@@ -230,7 +229,7 @@ function Inner() {
|
||||
return () => {
|
||||
unmounted.current = true
|
||||
}
|
||||
}, [ax, agent])
|
||||
}, [ax, appviewClient])
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
ageAssuranceRuleIDs as ids,
|
||||
type AppBskyAgeassuranceDefs,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {AgeAssuranceAccess} from '#/ageAssurance/types'
|
||||
import {
|
||||
ANDROID_API_LEVEL,
|
||||
@@ -11,6 +6,7 @@ import {
|
||||
IS_IOS,
|
||||
IS_WEB,
|
||||
} from '#/env'
|
||||
import {app} from '#/lexicons'
|
||||
|
||||
/**
|
||||
* Minimum age required to access the app at all.
|
||||
@@ -44,19 +40,17 @@ export const AGE_ASSURANCE_PLATFORM: 'web' | 'ios' | 'android' = IS_WEB
|
||||
export const DEVICE_SIGNALS_SUPPORTED: boolean =
|
||||
(IS_IOS && IOS_MAJOR_VERSION >= 26) || (IS_ANDROID && ANDROID_API_LEVEL >= 23)
|
||||
|
||||
export const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = {
|
||||
export const FALLBACK_REGION_CONFIG: app.bsky.ageassurance.defs.ConfigRegion = {
|
||||
countryCode: '*',
|
||||
regionCode: undefined,
|
||||
minAccessAge: MIN_ACCESS_AGE,
|
||||
rules: [
|
||||
{
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: MIN_ACCESS_AGE,
|
||||
access: AgeAssuranceAccess.Full,
|
||||
},
|
||||
{
|
||||
$type: ids.Default,
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: AgeAssuranceAccess.None,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
+95
-70
@@ -1,19 +1,13 @@
|
||||
import {createContext, useCallback, useContext, useEffect, useMemo} from 'react'
|
||||
import * as AgeRange from 'expo-age-range'
|
||||
import {
|
||||
type AppBskyAgeassuranceDefs,
|
||||
type AppBskyAgeassuranceGetConfig,
|
||||
type AppBskyAgeassuranceGetState,
|
||||
AtpAgent,
|
||||
type ChatBskyActorDeclaration,
|
||||
} from '@atproto/api'
|
||||
import {type Client} from '@atproto/lex'
|
||||
import {getPreferences} from '@bsky/sdk'
|
||||
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
|
||||
import {focusManager, QueryClient, useQuery} from '@tanstack/react-query'
|
||||
import {persistQueryClient} from '@tanstack/react-query-persist-client'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
|
||||
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {
|
||||
@@ -21,7 +15,8 @@ import {
|
||||
snoozeBirthdateUpdateAllowedForDid,
|
||||
} from '#/state/birthdate'
|
||||
import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declaration'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
|
||||
import {getPublicAppviewClient} from '#/state/session/clients'
|
||||
import {DEVICE_SIGNALS_SUPPORTED} from '#/ageAssurance/const'
|
||||
import * as debug from '#/ageAssurance/debug'
|
||||
import {logger} from '#/ageAssurance/logger'
|
||||
@@ -37,6 +32,7 @@ import {
|
||||
} from '#/ageAssurance/util'
|
||||
import {IS_DEV} from '#/env'
|
||||
import {useGeolocation} from '#/geolocation'
|
||||
import {app, type chat} from '#/lexicons'
|
||||
import {device} from '#/storage'
|
||||
|
||||
/**
|
||||
@@ -61,14 +57,15 @@ const persister = createAsyncStoragePersister({
|
||||
const [, cacheHydrationPromise] = persistQueryClient({
|
||||
queryClient: qc,
|
||||
persister,
|
||||
/*
|
||||
* Device signals are local-only and cannot be recovered without prompting
|
||||
* the user again, so they must survive the persister's default 24-hour
|
||||
* expiration. Server-backed queries still use their own stale times and
|
||||
* refetch normally after hydration.
|
||||
*/
|
||||
maxAge: Infinity,
|
||||
})
|
||||
|
||||
export function getDidFromAgentSession(agent: AtpAgent) {
|
||||
const sessionManager = agent.sessionManager
|
||||
if (!sessionManager || !sessionManager.did) return
|
||||
return sessionManager.did
|
||||
}
|
||||
|
||||
/*
|
||||
* Optimistic data
|
||||
*/
|
||||
@@ -101,16 +98,16 @@ export function setBirthdateForDid({
|
||||
export const configQueryKey = ['config']
|
||||
export async function getConfig() {
|
||||
if (debug.enabled) return debug.resolve(debug.config)
|
||||
const agent = new AtpAgent({
|
||||
service: PUBLIC_BSKY_SERVICE,
|
||||
})
|
||||
const res = await agent.app.bsky.ageassurance.getConfig()
|
||||
return res.data
|
||||
/*
|
||||
* An unauthenticated read against the public appview: the config is fetched
|
||||
* before there is any session (and while logged out), so it goes through the
|
||||
* process-wide public client rather than a bundle one.
|
||||
*/
|
||||
return await getPublicAppviewClient().call(app.bsky.ageassurance.getConfig)
|
||||
}
|
||||
export function getConfigFromCache():
|
||||
| AppBskyAgeassuranceGetConfig.OutputSchema
|
||||
| undefined {
|
||||
return qc.getQueryData<AppBskyAgeassuranceGetConfig.OutputSchema>(
|
||||
app.bsky.ageassurance.getConfig.$OutputBody | undefined {
|
||||
return qc.getQueryData<app.bsky.ageassurance.getConfig.$OutputBody>(
|
||||
configQueryKey,
|
||||
)
|
||||
}
|
||||
@@ -131,7 +128,7 @@ export function prefetchConfig() {
|
||||
try {
|
||||
logger.debug(`prefetchAgeAssuranceConfig: resolving...`)
|
||||
const res = await networkRetry(3, () => getConfig())
|
||||
qc.setQueryData<AppBskyAgeassuranceGetConfig.OutputSchema>(
|
||||
qc.setQueryData<app.bsky.ageassurance.getConfig.$OutputBody>(
|
||||
configQueryKey,
|
||||
res,
|
||||
)
|
||||
@@ -147,7 +144,7 @@ export function prefetchConfig() {
|
||||
export async function refetchConfig() {
|
||||
logger.debug(`refetchConfig: fetching...`)
|
||||
const res = await getConfig()
|
||||
qc.setQueryData<AppBskyAgeassuranceGetConfig.OutputSchema>(
|
||||
qc.setQueryData<app.bsky.ageassurance.getConfig.$OutputBody>(
|
||||
configQueryKey,
|
||||
res,
|
||||
)
|
||||
@@ -187,7 +184,7 @@ export function useConfigQuery() {
|
||||
export function createServerStateQueryKey({did}: {did: string}) {
|
||||
return ['serverState', did]
|
||||
}
|
||||
export async function getServerState({agent}: {agent: AtpAgent}) {
|
||||
export async function getServerState({appviewClient}: {appviewClient: Client}) {
|
||||
if (debug.enabled && debug.serverState)
|
||||
return debug.resolve(debug.serverState)
|
||||
const geolocation = device.get(['mergedGeolocation'])
|
||||
@@ -195,17 +192,21 @@ export async function getServerState({agent}: {agent: AtpAgent}) {
|
||||
logger.error(`getServerState: missing geolocation countryCode`)
|
||||
return null
|
||||
}
|
||||
const {data} = await agent.app.bsky.ageassurance.getState({
|
||||
const data = await appviewClient.call(app.bsky.ageassurance.getState, {
|
||||
countryCode: geolocation.countryCode,
|
||||
regionCode: geolocation.regionCode,
|
||||
})
|
||||
const did = getDidFromAgentSession(agent)
|
||||
const did = appviewClient.did
|
||||
if (data && did && createdAtCache.has(did)) {
|
||||
/*
|
||||
* If account was just created, just use the local cache if available. On
|
||||
* subsequent reloads, the server should have the correct value.
|
||||
* subsequent reloads, the server should have the correct value. The cache
|
||||
* holds ISO datetime strings (written from `new Date().toISOString()`), so
|
||||
* assert the branded DatetimeString at this boundary.
|
||||
*/
|
||||
data.metadata.accountCreatedAt = createdAtCache.get(did)
|
||||
data.metadata.accountCreatedAt = createdAtCache.get(
|
||||
did,
|
||||
) as typeof data.metadata.accountCreatedAt
|
||||
}
|
||||
return data ?? null
|
||||
}
|
||||
@@ -213,13 +214,17 @@ export function getServerStateFromCache({
|
||||
did,
|
||||
}: {
|
||||
did: string
|
||||
}): AppBskyAgeassuranceGetState.OutputSchema | undefined {
|
||||
return qc.getQueryData<AppBskyAgeassuranceGetState.OutputSchema>(
|
||||
}): app.bsky.ageassurance.getState.$OutputBody | undefined {
|
||||
return qc.getQueryData<app.bsky.ageassurance.getState.$OutputBody>(
|
||||
createServerStateQueryKey({did}),
|
||||
)
|
||||
}
|
||||
export async function prefetchServerState({agent}: {agent: AtpAgent}) {
|
||||
const did = getDidFromAgentSession(agent)
|
||||
export async function prefetchServerState({
|
||||
appviewClient,
|
||||
}: {
|
||||
appviewClient: Client
|
||||
}) {
|
||||
const did = appviewClient.did
|
||||
|
||||
if (!did) return
|
||||
|
||||
@@ -234,9 +239,9 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) {
|
||||
|
||||
try {
|
||||
logger.debug(`prefetchServerState: resolving...`)
|
||||
const res = await networkRetry(3, () => getServerState({agent}))
|
||||
const res = await networkRetry(3, () => getServerState({appviewClient}))
|
||||
if (res) {
|
||||
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(qk, res)
|
||||
qc.setQueryData<app.bsky.ageassurance.getState.$OutputBody>(qk, res)
|
||||
}
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
@@ -245,13 +250,17 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) {
|
||||
})
|
||||
}
|
||||
}
|
||||
export async function refetchServerState({agent}: {agent: AtpAgent}) {
|
||||
const did = getDidFromAgentSession(agent)
|
||||
export async function refetchServerState({
|
||||
appviewClient,
|
||||
}: {
|
||||
appviewClient: Client
|
||||
}) {
|
||||
const did = appviewClient.did
|
||||
if (!did) return
|
||||
logger.debug(`refetchServerState: fetching...`)
|
||||
const res = await networkRetry(3, () => getServerState({agent}))
|
||||
const res = await networkRetry(3, () => getServerState({appviewClient}))
|
||||
if (res) {
|
||||
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(
|
||||
qc.setQueryData<app.bsky.ageassurance.getState.$OutputBody>(
|
||||
createServerStateQueryKey({did}),
|
||||
res,
|
||||
)
|
||||
@@ -261,16 +270,16 @@ export async function refetchServerState({agent}: {agent: AtpAgent}) {
|
||||
export function usePatchServerState() {
|
||||
const {currentAccount} = useSession()
|
||||
return useCallback(
|
||||
(next: AppBskyAgeassuranceDefs.State) => {
|
||||
(next: app.bsky.ageassurance.defs.State) => {
|
||||
if (!currentAccount) return
|
||||
const did = currentAccount.did
|
||||
const prev = getServerStateFromCache({did})
|
||||
const merged: AppBskyAgeassuranceGetState.OutputSchema = {
|
||||
const merged: app.bsky.ageassurance.getState.$OutputBody = {
|
||||
metadata: {},
|
||||
...(prev || {}),
|
||||
state: next,
|
||||
}
|
||||
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(
|
||||
qc.setQueryData<app.bsky.ageassurance.getState.$OutputBody>(
|
||||
createServerStateQueryKey({did}),
|
||||
merged,
|
||||
)
|
||||
@@ -279,8 +288,8 @@ export function usePatchServerState() {
|
||||
)
|
||||
}
|
||||
export function useServerStateQuery() {
|
||||
const agent = useAgent()
|
||||
const did = getDidFromAgentSession(agent)
|
||||
const appviewClient = useAppviewClient()
|
||||
const did = appviewClient.did
|
||||
const query = useQuery(
|
||||
{
|
||||
enabled: !!did,
|
||||
@@ -290,7 +299,7 @@ export function useServerStateQuery() {
|
||||
},
|
||||
queryKey: createServerStateQueryKey({did: did!}),
|
||||
async queryFn() {
|
||||
return getServerState({agent})
|
||||
return getServerState({appviewClient})
|
||||
},
|
||||
},
|
||||
qc,
|
||||
@@ -336,21 +345,21 @@ export function useServerStateQuery() {
|
||||
|
||||
export type OtherRequiredData = {
|
||||
birthdate: string | undefined
|
||||
actorDeclaration?: ChatBskyActorDeclaration.Main
|
||||
actorDeclaration?: chat.bsky.actor.declaration.Main
|
||||
}
|
||||
export function createOtherRequiredDataQueryKey({did}: {did: string}) {
|
||||
return ['otherRequiredData', did]
|
||||
}
|
||||
async function getOtherRequiredData({
|
||||
agent,
|
||||
accountClient,
|
||||
}: {
|
||||
agent: AtpAgent
|
||||
accountClient: Client
|
||||
}): Promise<OtherRequiredData> {
|
||||
if (debug.enabled) return debug.resolve(debug.otherRequiredData)
|
||||
const did = getDidFromAgentSession(agent)
|
||||
const did = accountClient.did
|
||||
const [prefs, actorDeclaration] = await Promise.all([
|
||||
agent.getPreferences(),
|
||||
fetchActorDeclarationRecord({did, agent}),
|
||||
accountClient.call(getPreferences),
|
||||
fetchActorDeclarationRecord({did, client: accountClient}),
|
||||
])
|
||||
const data: OtherRequiredData = {
|
||||
birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined,
|
||||
@@ -411,7 +420,7 @@ export function setOtherRequiredDataActorDeclarationCache({
|
||||
actorDeclaration,
|
||||
}: {
|
||||
did: string
|
||||
actorDeclaration: ChatBskyActorDeclaration.Main
|
||||
actorDeclaration: chat.bsky.actor.declaration.Main
|
||||
}) {
|
||||
const prev = getOtherRequiredDataFromCache({did})
|
||||
const next: OtherRequiredData = {
|
||||
@@ -426,8 +435,12 @@ export function setOtherRequiredDataActorDeclarationCache({
|
||||
next,
|
||||
)
|
||||
}
|
||||
export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) {
|
||||
const did = getDidFromAgentSession(agent)
|
||||
export async function prefetchOtherRequiredData({
|
||||
accountClient,
|
||||
}: {
|
||||
accountClient: Client
|
||||
}) {
|
||||
const did = accountClient.did
|
||||
|
||||
if (!did) return
|
||||
|
||||
@@ -442,7 +455,9 @@ export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) {
|
||||
|
||||
try {
|
||||
logger.debug(`prefetchOtherRequiredData: resolving...`)
|
||||
const res = await networkRetry(3, () => getOtherRequiredData({agent}))
|
||||
const res = await networkRetry(3, () =>
|
||||
getOtherRequiredData({accountClient}),
|
||||
)
|
||||
qc.setQueryData<OtherRequiredData>(qk, res)
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
@@ -471,8 +486,8 @@ export function usePatchOtherRequiredData() {
|
||||
)
|
||||
}
|
||||
export function useOtherRequiredDataQuery() {
|
||||
const agent = useAgent()
|
||||
const did = getDidFromAgentSession(agent)
|
||||
const accountClient = usePdsClient()
|
||||
const did = accountClient.did
|
||||
return useQuery(
|
||||
{
|
||||
enabled: !!did,
|
||||
@@ -482,7 +497,7 @@ export function useOtherRequiredDataQuery() {
|
||||
},
|
||||
queryKey: createOtherRequiredDataQueryKey({did: did!}),
|
||||
async queryFn() {
|
||||
return getOtherRequiredData({agent})
|
||||
return getOtherRequiredData({accountClient})
|
||||
},
|
||||
},
|
||||
qc,
|
||||
@@ -547,7 +562,7 @@ export function getDeviceSignalsFromCacheForRegion({
|
||||
region,
|
||||
}: {
|
||||
did: string
|
||||
region: AppBskyAgeassuranceDefs.ConfigRegion
|
||||
region: app.bsky.ageassurance.defs.ConfigRegion
|
||||
}): AgeRange.AgeRangeResponse | undefined {
|
||||
const regionKey = createRegionKey(region)
|
||||
return getDeviceSignalsMapFromCache({did})?.[regionKey]
|
||||
@@ -577,8 +592,12 @@ export function setDeviceSignalsForRegion({
|
||||
prev => ({...prev, [regionKey]: signals}),
|
||||
)
|
||||
}
|
||||
export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) {
|
||||
const did = getDidFromAgentSession(agent)
|
||||
export async function prefetchDeviceSignals({
|
||||
appviewClient,
|
||||
}: {
|
||||
appviewClient: Client
|
||||
}) {
|
||||
const did = appviewClient.did
|
||||
if (!did) return
|
||||
|
||||
/**
|
||||
@@ -613,8 +632,8 @@ export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) {
|
||||
*/
|
||||
}
|
||||
export function useDeviceSignalsQuery() {
|
||||
const agent = useAgent()
|
||||
const did = getDidFromAgentSession(agent)
|
||||
const appviewClient = useAppviewClient()
|
||||
const did = appviewClient.did
|
||||
const {data: config} = useConfigQuery()
|
||||
const geolocation = useGeolocation()
|
||||
/*
|
||||
@@ -659,13 +678,19 @@ export function useDeviceSignalsQuery() {
|
||||
/**
|
||||
* Helper to prefetch all age assurance data from the server.
|
||||
*/
|
||||
export function prefetchAgeAssuranceServerData({agent}: {agent: AtpAgent}) {
|
||||
export function prefetchAgeAssuranceServerData({
|
||||
appviewClient,
|
||||
accountClient,
|
||||
}: {
|
||||
appviewClient: Client
|
||||
accountClient: Client
|
||||
}) {
|
||||
return Promise.allSettled([
|
||||
// config fetch initiated at the top of the App.platform.tsx files, awaited here
|
||||
configPrefetchPromise,
|
||||
prefetchServerState({agent}),
|
||||
prefetchOtherRequiredData({agent}),
|
||||
prefetchDeviceSignals({agent}),
|
||||
prefetchServerState({appviewClient}),
|
||||
prefetchOtherRequiredData({accountClient}),
|
||||
prefetchDeviceSignals({appviewClient}),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -691,11 +716,11 @@ export type AgeAssuranceServerData = {
|
||||
/**
|
||||
* The raw config from the appview.
|
||||
*/
|
||||
config: AppBskyAgeassuranceDefs.Config | undefined
|
||||
config: app.bsky.ageassurance.defs.Config | undefined
|
||||
/**
|
||||
* The raw state from the appview. Must be further processed before being useful.
|
||||
*/
|
||||
state: AppBskyAgeassuranceDefs.State | undefined
|
||||
state: app.bsky.ageassurance.defs.State | undefined
|
||||
metadata: AgeAssuranceMetadata | undefined
|
||||
/**
|
||||
* The native on-device age signals for the region the user is currently in,
|
||||
|
||||
+80
-115
@@ -1,13 +1,10 @@
|
||||
import type * as AgeRange from 'expo-age-range'
|
||||
import {
|
||||
ageAssuranceRuleIDs as ids,
|
||||
type AppBskyAgeassuranceDefs,
|
||||
type AppBskyAgeassuranceGetState,
|
||||
} from '@atproto/api'
|
||||
import {toDatetimeString} from '@atproto/syntax'
|
||||
|
||||
import {type OtherRequiredData} from '#/ageAssurance/data'
|
||||
import {IS_DEV, IS_E2E} from '#/env'
|
||||
import {type Geolocation} from '#/geolocation'
|
||||
import {app} from '#/lexicons'
|
||||
|
||||
export const enabled = (IS_DEV && false) || IS_E2E
|
||||
|
||||
@@ -31,31 +28,30 @@ export const otherRequiredData: OtherRequiredData = {
|
||||
}
|
||||
|
||||
const serverStateEnabled = false || IS_E2E
|
||||
export const serverState: AppBskyAgeassuranceGetState.OutputSchema | undefined =
|
||||
serverStateEnabled
|
||||
? {
|
||||
state: {
|
||||
lastInitiatedAt: undefined, // new Date(2025, 1, 1).toISOString(),
|
||||
status: 'unknown',
|
||||
access: 'unknown',
|
||||
},
|
||||
metadata: {
|
||||
accountCreatedAt: new Date(2023, 1, 1).toISOString(),
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
export const serverState:
|
||||
app.bsky.ageassurance.getState.$OutputBody | undefined = serverStateEnabled
|
||||
? {
|
||||
state: {
|
||||
lastInitiatedAt: undefined, // new Date(2025, 1, 1).toISOString(),
|
||||
status: 'unknown',
|
||||
access: 'unknown',
|
||||
},
|
||||
metadata: {
|
||||
accountCreatedAt: toDatetimeString(new Date(2023, 1, 1)),
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
|
||||
export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
export const config: app.bsky.ageassurance.defs.Config = {
|
||||
regions: [
|
||||
{
|
||||
countryCode: 'AA',
|
||||
regionCode: undefined,
|
||||
minAccessAge: 13,
|
||||
rules: [
|
||||
{
|
||||
$type: ids.Default,
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'full',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -68,65 +64,55 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
minAccessAge: 18,
|
||||
additionalVerificationMethods: ['device'],
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
countryCode: 'GB',
|
||||
minAccessAge: 13,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 13,
|
||||
access: 'safe',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
countryCode: 'AU',
|
||||
minAccessAge: 16,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAccountNewerThan.build({
|
||||
date: '2025-12-10T00:00:00Z',
|
||||
access: 'none',
|
||||
$type: ids.IfAccountNewerThan,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 16,
|
||||
access: 'safe',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 16,
|
||||
access: 'safe',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -134,20 +120,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
regionCode: 'SD',
|
||||
minAccessAge: 13,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 13,
|
||||
access: 'safe',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -155,20 +138,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
regionCode: 'WY',
|
||||
minAccessAge: 13,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 13,
|
||||
access: 'safe',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -176,20 +156,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
regionCode: 'OH',
|
||||
minAccessAge: 13,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 13,
|
||||
access: 'safe',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -197,15 +174,13 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
regionCode: 'MS',
|
||||
minAccessAge: 18,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -213,20 +188,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
regionCode: 'VA',
|
||||
minAccessAge: 16,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 16,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 16,
|
||||
access: 'full',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -234,45 +206,38 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
regionCode: 'TN',
|
||||
minAccessAge: 18,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
countryCode: 'BR',
|
||||
minAccessAge: 13,
|
||||
rules: [
|
||||
{
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfAssuredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 18,
|
||||
access: 'full',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
|
||||
age: 13,
|
||||
access: 'safe',
|
||||
$type: ids.IfDeclaredOverAge,
|
||||
},
|
||||
{
|
||||
}),
|
||||
app.bsky.ageassurance.defs.configRegionRuleDefault.build({
|
||||
access: 'none',
|
||||
$type: ids.Default,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import type * as AgeRange from 'expo-age-range'
|
||||
import {
|
||||
type AppBskyAgeassuranceDefs,
|
||||
computeAgeAssuranceRegionAccess,
|
||||
} from '@atproto/api'
|
||||
import {computeAgeAssuranceRegionAccess} from '@bsky/sdk/utils'
|
||||
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -30,6 +27,7 @@ import {
|
||||
getAgeAssuranceRegionConfigWithFallback,
|
||||
} from '#/ageAssurance/util'
|
||||
import {type Geolocation, useGeolocation} from '#/geolocation'
|
||||
import {type app} from '#/lexicons'
|
||||
import {device} from '#/storage'
|
||||
|
||||
/**
|
||||
@@ -47,8 +45,8 @@ function computeAgeAssuranceState({
|
||||
}: {
|
||||
hasSession: boolean
|
||||
geolocation: Geolocation
|
||||
config?: AppBskyAgeassuranceDefs.Config
|
||||
state?: AppBskyAgeassuranceDefs.State
|
||||
config?: app.bsky.ageassurance.defs.Config
|
||||
state?: app.bsky.ageassurance.defs.State
|
||||
metadata?: AgeAssuranceMetadata
|
||||
deviceSignals?: AgeRange.AgeRangeResponse
|
||||
}) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type * as AgeRange from 'expo-age-range'
|
||||
import {type computeAgeAssuranceRegionAccess} from '@atproto/api'
|
||||
import {type computeAgeAssuranceRegionAccess} from '@bsky/sdk/utils'
|
||||
|
||||
import {logger} from '#/ageAssurance/logger'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {type AppBskyAgeassuranceBegin, AtpAgent} from '@atproto/api'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
@@ -9,26 +8,28 @@ import {
|
||||
PUBLIC_APPVIEW_DID,
|
||||
} from '#/lib/constants'
|
||||
import {isNetworkError} from '#/lib/hooks/useCleanError'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {createLexClient} from '#/lib/lexClient'
|
||||
import {usePdsClient} from '#/state/session'
|
||||
import {usePatchAgeAssuranceServerState} from '#/ageAssurance'
|
||||
import {logger} from '#/ageAssurance/logger'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {BLUESKY_PROXY_DID} from '#/env'
|
||||
import {useGeolocation} from '#/geolocation'
|
||||
import {app, com} from '#/lexicons'
|
||||
|
||||
const IS_DEV_ENV = BLUESKY_PROXY_DID !== PUBLIC_APPVIEW_DID
|
||||
const APPVIEW = IS_DEV_ENV ? DEV_ENV_APPVIEW : PUBLIC_APPVIEW
|
||||
|
||||
export function useBeginAgeAssurance() {
|
||||
const ax = useAnalytics()
|
||||
const agent = useAgent()
|
||||
const pdsClient = usePdsClient()
|
||||
const geolocation = useGeolocation()
|
||||
const patchAgeAssuranceStateResponse = usePatchAgeAssuranceServerState()
|
||||
|
||||
return useMutation({
|
||||
async mutationFn(
|
||||
props: Omit<
|
||||
AppBskyAgeassuranceBegin.InputSchema,
|
||||
app.bsky.ageassurance.begin.$InputBody,
|
||||
'countryCode' | 'regionCode'
|
||||
>,
|
||||
) {
|
||||
@@ -38,17 +39,22 @@ export function useBeginAgeAssurance() {
|
||||
throw new Error(`Geolocation not available, cannot init age assurance.`)
|
||||
}
|
||||
|
||||
const {
|
||||
data: {token},
|
||||
} = await agent.com.atproto.server.getServiceAuth({
|
||||
const {token} = await pdsClient.call(com.atproto.server.getServiceAuth, {
|
||||
aud: BLUESKY_PROXY_DID,
|
||||
lxm: `app.bsky.ageassurance.begin`,
|
||||
})
|
||||
|
||||
const appView = new AtpAgent({service: APPVIEW})
|
||||
appView.sessionManager.session = {...agent.session!}
|
||||
appView.sessionManager.session.accessJwt = token
|
||||
appView.sessionManager.session.refreshJwt = ''
|
||||
/*
|
||||
* A single-use client scoped to the service-auth token: it has no session,
|
||||
* so nothing can refresh it, and the request goes straight to the appview
|
||||
* with the token as a static `authorization` header. A raw client is
|
||||
* allowed to preset that header where a session-backed one is not, which
|
||||
* also makes the old `refreshJwt = ''` clone unnecessary.
|
||||
*/
|
||||
const scopedClient = createLexClient({
|
||||
service: APPVIEW,
|
||||
headers: {authorization: `Bearer ${token}`},
|
||||
})
|
||||
|
||||
ax.metric('ageAssurance:api:begin', {
|
||||
platform: Platform.OS,
|
||||
@@ -60,9 +66,9 @@ export function useBeginAgeAssurance() {
|
||||
* 2s wait is good actually. Email sending takes a hot sec and this helps
|
||||
* ensure the email is ready for the user once they open their inbox.
|
||||
*/
|
||||
const {data} = await wait(
|
||||
const data = await wait(
|
||||
2e3,
|
||||
appView.app.bsky.ageassurance.begin({
|
||||
scopedClient.call(app.bsky.ageassurance.begin, {
|
||||
...props,
|
||||
countryCode,
|
||||
regionCode,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useCallback} from 'react'
|
||||
import {computeAgeAssuranceRegionAccess} from '@atproto/api'
|
||||
import {computeAgeAssuranceRegionAccess} from '@bsky/sdk/utils'
|
||||
|
||||
import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data'
|
||||
import {logger} from '#/ageAssurance/logger'
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
import {getAgeAssuranceRegionConfig} from '@atproto/api'
|
||||
|
||||
import {getAgeAssuranceRegionConfigForGeolocation} from '#/ageAssurance/util'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
jest.mock('#/ageAssurance/data')
|
||||
jest.mock('@atproto/api', () => ({
|
||||
...jest.requireActual('@atproto/api'),
|
||||
getAgeAssuranceRegionConfig: jest.fn(),
|
||||
}))
|
||||
|
||||
/*
|
||||
* Platform-based region filtering itself is implemented and tested in
|
||||
* `@atproto/api` (see `getAgeAssuranceRegionConfig`). What we own - and test
|
||||
* here - is that region resolution passes the current platform through. The
|
||||
* jest preset is `jest-expo/ios`, so `AGE_ASSURANCE_PLATFORM` resolves to
|
||||
* `ios` in these tests.
|
||||
* Platform scoping is applied locally in `util.ts` (the SDK region matcher
|
||||
* takes no platform filter). The jest preset is `jest-expo/ios`, so
|
||||
* `AGE_ASSURANCE_PLATFORM` resolves to `ios` in these tests.
|
||||
*/
|
||||
describe('getAgeAssuranceRegionConfigForGeolocation', () => {
|
||||
it('passes the current platform to the SDK region matcher', () => {
|
||||
const config = {regions: []}
|
||||
getAgeAssuranceRegionConfigForGeolocation(config, {
|
||||
countryCode: 'US',
|
||||
regionCode: 'TX',
|
||||
})
|
||||
expect(getAgeAssuranceRegionConfig).toHaveBeenCalledWith(config, {
|
||||
countryCode: 'US',
|
||||
regionCode: 'TX',
|
||||
platform: 'ios',
|
||||
})
|
||||
const region = (
|
||||
countryCode: string,
|
||||
regionCode?: string,
|
||||
platforms?: app.bsky.ageassurance.defs.ConfigRegion['platforms'],
|
||||
): app.bsky.ageassurance.defs.ConfigRegion => ({
|
||||
countryCode,
|
||||
regionCode,
|
||||
platforms,
|
||||
minAccessAge: 13,
|
||||
rules: [],
|
||||
})
|
||||
|
||||
it('skips regions for other platforms and continues matching', () => {
|
||||
const web = region('US', undefined, ['web'])
|
||||
const ios = region('US', undefined, ['ios'])
|
||||
|
||||
expect(
|
||||
getAgeAssuranceRegionConfigForGeolocation(
|
||||
{regions: [web, ios]},
|
||||
{countryCode: 'US', regionCode: undefined},
|
||||
),
|
||||
).toBe(ios)
|
||||
})
|
||||
|
||||
it('matches a region-specific config before a later country config', () => {
|
||||
const texas = region('US', 'TX')
|
||||
const us = region('US')
|
||||
|
||||
expect(
|
||||
getAgeAssuranceRegionConfigForGeolocation(
|
||||
{regions: [texas, us]},
|
||||
{countryCode: 'US', regionCode: 'TX'},
|
||||
),
|
||||
).toBe(texas)
|
||||
})
|
||||
})
|
||||
|
||||
+22
-15
@@ -1,11 +1,10 @@
|
||||
import {useMemo} from 'react'
|
||||
import type * as AgeRange from 'expo-age-range'
|
||||
import {type ModerationPrefs} from '@bsky/sdk/moderation'
|
||||
import {
|
||||
AppBskyAgeassuranceDefs,
|
||||
computeAgeAssuranceRegionAccess,
|
||||
getAgeAssuranceRegionConfig,
|
||||
type ModerationPrefs,
|
||||
} from '@atproto/api'
|
||||
} from '@bsky/sdk/utils'
|
||||
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {regionName} from '#/locale/helpers'
|
||||
@@ -25,6 +24,8 @@ import {
|
||||
} from '#/ageAssurance/types'
|
||||
import {type Geolocation, useGeolocation} from '#/geolocation'
|
||||
import {USRegionNameToRegionCode} from '#/geolocation/util'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
/**
|
||||
* Resolves a geolocation to its matched age assurance region config, or
|
||||
@@ -41,9 +42,9 @@ import {USRegionNameToRegionCode} from '#/geolocation/util'
|
||||
* risk desyncing the write and read keys and silently losing grants.
|
||||
*/
|
||||
export function getAgeAssuranceRegionConfigForGeolocation(
|
||||
config: AppBskyAgeassuranceDefs.Config,
|
||||
config: app.bsky.ageassurance.defs.Config,
|
||||
geolocation: Geolocation,
|
||||
): AppBskyAgeassuranceDefs.ConfigRegion | undefined {
|
||||
): app.bsky.ageassurance.defs.ConfigRegion | undefined {
|
||||
return getAgeAssuranceRegionConfig(config, {
|
||||
countryCode: geolocation.countryCode ?? '',
|
||||
regionCode: geolocation.regionCode,
|
||||
@@ -59,9 +60,9 @@ export function getAgeAssuranceRegionConfigForGeolocation(
|
||||
* which can return undefined if the geolocation does not match any AA region.
|
||||
*/
|
||||
export function getAgeAssuranceRegionConfigWithFallback(
|
||||
config: AppBskyAgeassuranceDefs.Config,
|
||||
config: app.bsky.ageassurance.defs.Config,
|
||||
geolocation: Geolocation,
|
||||
): AppBskyAgeassuranceDefs.ConfigRegion {
|
||||
): app.bsky.ageassurance.defs.ConfigRegion {
|
||||
return (
|
||||
getAgeAssuranceRegionConfigForGeolocation(config, geolocation) ||
|
||||
FALLBACK_REGION_CONFIG
|
||||
@@ -74,9 +75,9 @@ export function getAgeAssuranceRegionConfigWithFallback(
|
||||
* historical KWS-only behavior).
|
||||
*/
|
||||
export function getRegionAdditionalVerificationMethods(
|
||||
region: AppBskyAgeassuranceDefs.ConfigRegion,
|
||||
region: app.bsky.ageassurance.defs.ConfigRegion,
|
||||
): NonNullable<
|
||||
AppBskyAgeassuranceDefs.ConfigRegion['additionalVerificationMethods']
|
||||
app.bsky.ageassurance.defs.ConfigRegion['additionalVerificationMethods']
|
||||
> {
|
||||
return region.additionalVerificationMethods ?? []
|
||||
}
|
||||
@@ -86,7 +87,7 @@ export function getRegionAdditionalVerificationMethods(
|
||||
* age APIs (Apple Declared Age Range / Google Play Age Signals).
|
||||
*/
|
||||
export function regionAllowsDeviceVerification(
|
||||
region: AppBskyAgeassuranceDefs.ConfigRegion,
|
||||
region: app.bsky.ageassurance.defs.ConfigRegion,
|
||||
): boolean {
|
||||
return getRegionAdditionalVerificationMethods(region).includes('device')
|
||||
}
|
||||
@@ -122,7 +123,7 @@ export function createRegionKey(region: {
|
||||
* usable data.
|
||||
*/
|
||||
export function getAgeAssuranceDataFromDeviceSignals(
|
||||
region: AppBskyAgeassuranceDefs.ConfigRegion,
|
||||
region: app.bsky.ageassurance.defs.ConfigRegion,
|
||||
deviceSignals: AgeRange.AgeRangeResponse | undefined,
|
||||
): {
|
||||
assuredAge?: number
|
||||
@@ -183,7 +184,7 @@ export function canBirthdateUpdateIncreaseAccess({
|
||||
metadata,
|
||||
deviceSignals,
|
||||
}: {
|
||||
region: AppBskyAgeassuranceDefs.ConfigRegion
|
||||
region: app.bsky.ageassurance.defs.ConfigRegion
|
||||
metadata?: AgeAssuranceMetadata
|
||||
deviceSignals?: AgeRange.AgeRangeResponse
|
||||
}): boolean {
|
||||
@@ -212,8 +213,14 @@ export function canBirthdateUpdateIncreaseAccess({
|
||||
const thresholds = new Set<number>([region.minAccessAge])
|
||||
for (const rule of region.rules) {
|
||||
if (
|
||||
AppBskyAgeassuranceDefs.isConfigRegionRuleIfDeclaredOverAge(rule) ||
|
||||
AppBskyAgeassuranceDefs.isConfigRegionRuleIfDeclaredUnderAge(rule)
|
||||
bsky.isType(
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge,
|
||||
rule,
|
||||
) ||
|
||||
bsky.isType(
|
||||
app.bsky.ageassurance.defs.configRegionRuleIfDeclaredUnderAge,
|
||||
rule,
|
||||
)
|
||||
) {
|
||||
thresholds.add(rule.age)
|
||||
}
|
||||
@@ -303,7 +310,7 @@ export function computeAgeAssuranceFlags({
|
||||
deviceSignals,
|
||||
}: {
|
||||
state: AgeAssuranceState
|
||||
regionConfig: AppBskyAgeassuranceDefs.ConfigRegion
|
||||
regionConfig: app.bsky.ageassurance.defs.ConfigRegion
|
||||
metadata?: AgeAssuranceMetadata
|
||||
deviceSignals?: AgeRange.AgeRangeResponse
|
||||
}): AgeAssuranceFlags {
|
||||
|
||||
+6
-1
@@ -3,6 +3,8 @@ import {type FontVariant, type TextStyle} from 'react-native'
|
||||
import {IS_ANDROID, IS_WEB} from '#/env'
|
||||
import {type Device, device} from '#/storage'
|
||||
|
||||
export type MutableTextStyle = {-readonly [K in keyof TextStyle]: TextStyle[K]}
|
||||
|
||||
const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
|
||||
|
||||
const factor = 0.0625 // 1 - (15/16)
|
||||
@@ -37,7 +39,10 @@ export function setFontFamily(fontFamily: Device['fontFamily']) {
|
||||
/*
|
||||
* Unused fonts are commented out, but the files are there if we need them.
|
||||
*/
|
||||
export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
|
||||
export function applyFonts(
|
||||
style: MutableTextStyle,
|
||||
fontFamily: 'system' | 'theme',
|
||||
) {
|
||||
if (fontFamily === 'theme') {
|
||||
if (IS_ANDROID) {
|
||||
style.fontFamily =
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
import {UITextView} from '@bsky.app/react-native-uitextview'
|
||||
import createEmojiRegex from 'emoji-regex'
|
||||
|
||||
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
|
||||
import {
|
||||
type Alf,
|
||||
applyFonts,
|
||||
atoms,
|
||||
flatten,
|
||||
type MutableTextStyle,
|
||||
} from '#/alf'
|
||||
import {IS_IOS, IS_NATIVE} from '#/env'
|
||||
|
||||
/**
|
||||
@@ -27,7 +33,7 @@ export function normalizeTextStyles(
|
||||
fontFamily: Alf['fonts']['family']
|
||||
} & Pick<Alf, 'flags'>,
|
||||
) {
|
||||
const s = flatten(styles) ?? {}
|
||||
const s: MutableTextStyle = {...flatten(styles)}
|
||||
|
||||
// should always be defined on these components
|
||||
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {Dimensions} from 'react-native'
|
||||
import {Dimensions, type DimensionsPayload} from 'react-native'
|
||||
|
||||
/**
|
||||
* Same as `useWindowDimensions().fontScale`, but avoids rerendering
|
||||
@@ -9,9 +9,12 @@ export function useNativeFontScale() {
|
||||
const [fontScale, setFontScale] = useState(Dimensions.get('window').fontScale)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = Dimensions.addEventListener('change', evt => {
|
||||
setFontScale(evt.window.fontScale)
|
||||
})
|
||||
const sub = Dimensions.addEventListener(
|
||||
'change',
|
||||
(evt: DimensionsPayload) => {
|
||||
if (evt.window) setFontScale(evt.window.fontScale)
|
||||
},
|
||||
)
|
||||
return () => sub.remove()
|
||||
}, [])
|
||||
|
||||
|
||||
+16
-3
@@ -1,6 +1,19 @@
|
||||
import {type DimensionValue, StyleSheet} from 'react-native'
|
||||
import {type DimensionValue, type StyleProp, StyleSheet} from 'react-native'
|
||||
|
||||
export const flatten = StyleSheet.flatten
|
||||
export function flatten<T>(
|
||||
style?: StyleProp<T>,
|
||||
): T extends (infer U)[] ? U : T {
|
||||
return (StyleSheet.flatten(
|
||||
style as unknown as Parameters<typeof StyleSheet.flatten>[0],
|
||||
) ?? {}) as T extends (infer U)[] ? U : T
|
||||
}
|
||||
|
||||
/** Flatten React Native styles passed directly to a web-only DOM component. */
|
||||
export function flattenToCSS(style: unknown): React.CSSProperties {
|
||||
return (StyleSheet.flatten(
|
||||
style as Parameters<typeof StyleSheet.flatten>[0],
|
||||
) ?? {}) as React.CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce a style value to a number. Padding values are typed as
|
||||
@@ -28,7 +41,7 @@ interface PaddingStyle {
|
||||
* non-numeric `DimensionValue` (e.g. percentages) is treated as 0.
|
||||
*/
|
||||
export function extractPadding(style: PaddingStyle | PaddingStyle[]) {
|
||||
const s = flatten(style as any) ?? {}
|
||||
const s = flatten(style)
|
||||
const base = num(s.padding)
|
||||
return {
|
||||
paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base,
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useThemeName(): ThemeName {
|
||||
}
|
||||
|
||||
function getThemeName(
|
||||
colorScheme: ColorSchemeName,
|
||||
colorScheme: ColorSchemeName | null | undefined,
|
||||
colorMode: 'system' | 'light' | 'dark',
|
||||
darkTheme?: ThemeName,
|
||||
) {
|
||||
@@ -39,11 +39,8 @@ function getThemeName(
|
||||
}
|
||||
|
||||
function updateDocument(theme: ThemeName) {
|
||||
// @ts-ignore web only
|
||||
if (IS_WEB && typeof window !== 'undefined') {
|
||||
// @ts-ignore web only
|
||||
const html = window.document.documentElement
|
||||
// @ts-ignore web only
|
||||
const meta = window.document.querySelector('meta[name="theme-color"]')
|
||||
|
||||
// remove any other color mode classes
|
||||
|
||||
@@ -17,7 +17,6 @@ export enum Features {
|
||||
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
||||
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
||||
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
||||
PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable',
|
||||
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
||||
VideoAllow10MinuteEnable = 'video:allow-10-minute:enable',
|
||||
VideoMultipartUploadEnable = 'video:multipart_upload:enable',
|
||||
@@ -25,6 +24,8 @@ export enum Features {
|
||||
FollowSortEnable = 'follow_sort:enable',
|
||||
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
|
||||
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
|
||||
ContentVisibilitySettingsEnable = 'content_visibility_settings:enable',
|
||||
ModerationInboxEnable = 'moderation_inbox:enable',
|
||||
|
||||
// values
|
||||
TrendingDiscoverValues = 'trending_discover:values',
|
||||
|
||||
@@ -69,6 +69,26 @@ export type Events = {
|
||||
'router:navigate': {
|
||||
from?: string
|
||||
}
|
||||
'web:list:size': {
|
||||
itemCount: number
|
||||
renderedRowCount: number
|
||||
contentHeight: number
|
||||
sessionAgeMs: number
|
||||
milestone: 100 | 250 | 500 | 1000
|
||||
heapUsedBytes?: number
|
||||
heapLimitBytes?: number
|
||||
}
|
||||
'web:list:longTasks': {
|
||||
itemCount: number
|
||||
renderedRowCount: number
|
||||
taskCount: number
|
||||
totalDurationMs: number
|
||||
maxDurationMs: number
|
||||
intervalMs: number
|
||||
sessionAgeMs: number
|
||||
heapUsedBytes?: number
|
||||
heapLimitBytes?: number
|
||||
}
|
||||
'nav:click': {
|
||||
item:
|
||||
| 'home'
|
||||
@@ -276,12 +296,7 @@ export type Events = {
|
||||
}
|
||||
'composer:open': {
|
||||
logContext:
|
||||
| 'Fab'
|
||||
| 'PostReply'
|
||||
| 'QuotePost'
|
||||
| 'ProfileFeed'
|
||||
| 'Deeplink'
|
||||
| 'Other'
|
||||
'Fab' | 'PostReply' | 'QuotePost' | 'ProfileFeed' | 'Deeplink' | 'Other'
|
||||
isReply: boolean
|
||||
hasQuote: boolean
|
||||
hasDraft: boolean
|
||||
@@ -596,10 +611,7 @@ export type Events = {
|
||||
}
|
||||
'chat:create': {
|
||||
logContext:
|
||||
| 'ProfileHeader'
|
||||
| 'NewChatDialog'
|
||||
| 'SendViaChatDialog'
|
||||
| 'ConvoSettings'
|
||||
'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' | 'ConvoSettings'
|
||||
}
|
||||
'chat:open': {
|
||||
logContext:
|
||||
@@ -1083,6 +1095,8 @@ export type Events = {
|
||||
'bot:label:toggle': {state: 'add' | 'remove'}
|
||||
'bot:badge:click': {}
|
||||
|
||||
'contentVisibility:algorithmicRecommendations:change': {hide: boolean}
|
||||
|
||||
'live:create': {duration: number}
|
||||
'live:edit': {}
|
||||
'live:remove': {}
|
||||
@@ -1340,10 +1354,7 @@ export type Events = {
|
||||
// invite friends dialog opened, with the surface that triggered it
|
||||
'invite:dialog:open': {
|
||||
logContext:
|
||||
| 'ProfileHeader'
|
||||
| 'Drawer'
|
||||
| 'FindContactsSettings'
|
||||
| 'NuxAnnouncement'
|
||||
'ProfileHeader' | 'Drawer' | 'FindContactsSettings' | 'NuxAnnouncement'
|
||||
}
|
||||
// user copied the invite link to clipboard
|
||||
'invite:action:copy': {}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export function useMeta(metadata?: MergeableMetadata) {
|
||||
const m = useMemo(() => metadata, [metadata])
|
||||
if (!m) return
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
m.__meta = true
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Fragment, useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -19,6 +18,7 @@ import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useActorStatus} from '#/features/liveNow'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export function AccountList({
|
||||
onSelectAccount,
|
||||
@@ -107,7 +107,7 @@ function AccountItem({
|
||||
isCurrentAccount,
|
||||
isPendingAccount,
|
||||
}: {
|
||||
profile?: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile?: app.bsky.actor.defs.ProfileViewDetailed
|
||||
account: SessionAccount
|
||||
onSelect: (account: SessionAccount) => void
|
||||
isCurrentAccount: boolean
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Pressable} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Pressable} from '#/components/Pressable'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
@@ -49,7 +49,7 @@ export function AltBadgeWithDialog({
|
||||
accessibilityHint=""
|
||||
hitSlop={HITSLOP_20}
|
||||
onPress={control.open}
|
||||
style={s => [
|
||||
style={({pressed, hovered}) => [
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
@@ -62,7 +62,7 @@ export function AltBadgeWithDialog({
|
||||
opacity: 0.8,
|
||||
},
|
||||
pos,
|
||||
s.hovered || s.pressed
|
||||
hovered || pressed
|
||||
? [
|
||||
{
|
||||
opacity: 1,
|
||||
|
||||
@@ -31,10 +31,7 @@ export type AutocompleteSearch = {
|
||||
}
|
||||
|
||||
export type AutocompleteItem =
|
||||
| AutocompleteProfile
|
||||
| AutocompleteTag
|
||||
| AutocompleteEmoji
|
||||
| AutocompleteSearch
|
||||
AutocompleteProfile | AutocompleteTag | AutocompleteEmoji | AutocompleteSearch
|
||||
|
||||
export type AutocompleteItemType = AutocompleteItem['type']
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||
import {moderateProfile, type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
import {keepPreviousData, useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useAppviewClient} from '#/state/session'
|
||||
import {
|
||||
type AutocompleteApi,
|
||||
type AutocompleteItem,
|
||||
type AutocompleteItemType,
|
||||
type AutocompleteProfile,
|
||||
} from '#/components/Autocomplete/types'
|
||||
import {app} from '#/lexicons'
|
||||
import {useEmojiSearch} from './useEmojiSearch'
|
||||
|
||||
const DEFAULT_MOD_OPTS = {
|
||||
@@ -31,7 +32,7 @@ export function useAutocomplete({
|
||||
limit?: number
|
||||
showSearchFallback?: boolean
|
||||
}): AutocompleteApi {
|
||||
const agent = useAgent()
|
||||
const client = useAppviewClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const emojiSearch = useEmojiSearch()
|
||||
|
||||
@@ -52,12 +53,12 @@ export function useAutocomplete({
|
||||
// Going from "foo" to "foo." should not clear matches.
|
||||
q = q.toLowerCase().trim().replace(/\.$/, '')
|
||||
|
||||
const res = await agent.searchActorsTypeahead({
|
||||
const data = await client.call(app.bsky.actor.searchActorsTypeahead, {
|
||||
q,
|
||||
limit: limit || 8,
|
||||
})
|
||||
|
||||
return (res?.data.actors || []).map(profile => ({
|
||||
return (data?.actors || []).map(profile => ({
|
||||
key: profile.did,
|
||||
type: 'profile' as const,
|
||||
value: '@' + profile.handle,
|
||||
|
||||
@@ -8,7 +8,7 @@ import Animated, {
|
||||
withDelay,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
import {moderateProfile} from '@bsky/sdk/moderation'
|
||||
|
||||
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
@@ -72,7 +72,7 @@ export function AvatarBubbles({
|
||||
if (!animate) return
|
||||
const animateBubble = (p: SharedValue<number>, i: number) => {
|
||||
p.set(0)
|
||||
p.set(() =>
|
||||
p.set(
|
||||
withDelay(
|
||||
500 + i * 100,
|
||||
withTiming(1, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {View} from 'react-native'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
import {moderateProfile} from '@bsky/sdk/moderation'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {useState} from 'react'
|
||||
import {type Insets, Pressable, View} from 'react-native'
|
||||
import {type Insets, View} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||
import {Pressable} from '#/components/Pressable'
|
||||
import * as Tooltip from '#/components/Tooltip'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {type Insets, View} from 'react-native'
|
||||
import {type ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -8,11 +7,12 @@ import {Button} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {Bot_Filled as RobotIcon} from '#/components/icons/Bot'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {type com} from '#/lexicons'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function isBotAccount(profile: {
|
||||
did: string
|
||||
labels?: ComAtprotoLabelDefs.Label[]
|
||||
labels?: com.atproto.label.defs.Label[]
|
||||
}): boolean {
|
||||
return (
|
||||
profile.labels?.some(l => l.val === 'bot' && l.src === profile.did) ?? false
|
||||
|
||||
@@ -86,8 +86,7 @@ export type ButtonState = {
|
||||
export type ButtonContext = VariantProps & ButtonState
|
||||
|
||||
type NonTextElements =
|
||||
| React.ReactElement
|
||||
| Iterable<React.ReactElement | null | undefined | boolean>
|
||||
React.ReactElement | Iterable<React.ReactElement | null | undefined | boolean>
|
||||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
@@ -102,6 +101,8 @@ export type ButtonProps = Pick<
|
||||
| 'onPressOut'
|
||||
| 'onFocus'
|
||||
| 'onBlur'
|
||||
| 'onAccessibilityAction'
|
||||
| 'onAccessibilityEscape'
|
||||
> &
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
@@ -132,7 +133,7 @@ export function useButtonContext() {
|
||||
return useContext(Context)
|
||||
}
|
||||
|
||||
export const Button = forwardRef<View, ButtonProps>(
|
||||
export const Button = forwardRef<React.ComponentRef<typeof View>, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
@@ -576,7 +577,6 @@ export const Button = forwardRef<View, ButtonProps>(
|
||||
role="button"
|
||||
accessibilityHint={undefined} // optional
|
||||
{...rest}
|
||||
// @ts-ignore - this will always be a pressable
|
||||
ref={ref}
|
||||
aria-label={label}
|
||||
aria-pressed={state.pressed}
|
||||
|
||||
@@ -39,6 +39,9 @@ import {
|
||||
import {Span, Text} from '#/components/Typography'
|
||||
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||
|
||||
type TextInputInstance = React.ComponentRef<typeof TextInput>
|
||||
type ViewInstance = React.ComponentRef<typeof View>
|
||||
|
||||
export type SubmitRequest =
|
||||
| {
|
||||
platform: 'web'
|
||||
@@ -60,7 +63,7 @@ export type ComposerInternalApi = {
|
||||
input?: ReturnType<typeof useTapper>['input']
|
||||
clear: () => void
|
||||
insert(text: string): void
|
||||
setAutocompleteAnchor: (node: View | null) => void
|
||||
setAutocompleteAnchor: (node: ViewInstance | null) => void
|
||||
}
|
||||
|
||||
export function useComposerInternalApiRef() {
|
||||
@@ -82,7 +85,7 @@ export type ComposerProps = Omit<
|
||||
| 'onSubmitEditing'
|
||||
> & {
|
||||
label: string
|
||||
ref?: React.RefObject<TextInput>
|
||||
ref?: React.RefObject<TextInputInstance>
|
||||
internalApiRef?: React.Ref<ComposerInternalApi>
|
||||
outerStyle?: ViewStyleProp['style']
|
||||
contentTextStyle?: TextStyleProp['style']
|
||||
@@ -139,6 +142,11 @@ export function Composer({
|
||||
placement: autocompletePlacement,
|
||||
dynamicWidth: IS_WEB,
|
||||
})
|
||||
const inputRef = mergeRefs<TextInputInstance>([
|
||||
ref,
|
||||
tapper.inputProps.ref as React.Ref<TextInputInstance>,
|
||||
sift.targetProps.ref as React.Ref<TextInputInstance>,
|
||||
])
|
||||
|
||||
/*
|
||||
* Active facet state for controlling the visibility of the Autocomplete.
|
||||
@@ -306,7 +314,7 @@ export function Composer({
|
||||
style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]}
|
||||
ref={node => {
|
||||
if (IS_WEB && node) {
|
||||
// @ts-ignore web only a11y
|
||||
// @ts-expect-error web only a11y
|
||||
node.setAttribute('inert', '')
|
||||
}
|
||||
}}>
|
||||
@@ -345,7 +353,7 @@ export function Composer({
|
||||
{...rest}
|
||||
{...tapper.inputProps}
|
||||
{...sift.targetProps}
|
||||
ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])}
|
||||
ref={inputRef}
|
||||
rawValue={tapper.state.text}
|
||||
onBlur={e => {
|
||||
rest.onBlur?.(e)
|
||||
@@ -359,11 +367,10 @@ export function Composer({
|
||||
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
|
||||
}
|
||||
}}
|
||||
// @ts-ignore web only
|
||||
// @ts-expect-error web only
|
||||
onCompositionStart={() => {
|
||||
isComposing.current = true
|
||||
}}
|
||||
// @ts-ignore web only
|
||||
onCompositionEnd={() => {
|
||||
isComposing.current = false
|
||||
}}
|
||||
|
||||
@@ -244,7 +244,7 @@ export function Trigger({
|
||||
const context = useContextMenuContext()
|
||||
const playHaptic = useHaptics()
|
||||
const insets = useSafeAreaInsets()
|
||||
const ref = useRef<View>(null)
|
||||
const ref = useRef<React.ComponentRef<typeof View>>(null)
|
||||
const isFocused = useIsFocused()
|
||||
const [image, setImage] = useState<string | null>(null)
|
||||
const [pendingMeasurement, setPendingMeasurement] = useState<{
|
||||
@@ -971,7 +971,10 @@ export function Divider() {
|
||||
)
|
||||
}
|
||||
|
||||
function measureView(view: View | null, insets: EdgeInsets) {
|
||||
function measureView(
|
||||
view: React.ComponentRef<typeof View> | null,
|
||||
insets: EdgeInsets,
|
||||
) {
|
||||
if (!view) return Promise.resolve(null)
|
||||
return new Promise<Measurement>(resolve => {
|
||||
view?.measureInWindow((x, y, width, height) =>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from 'react'
|
||||
import {
|
||||
Keyboard,
|
||||
type KeyboardEventListener,
|
||||
type KeyboardEvent,
|
||||
type LayoutChangeEvent,
|
||||
type NativeScrollEvent,
|
||||
type NativeSyntheticEvent,
|
||||
@@ -203,80 +203,85 @@ export function Inner(props: DialogInnerProps) {
|
||||
return <ScrollableInner {...props} />
|
||||
}
|
||||
|
||||
export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>(
|
||||
function ScrollableInner(
|
||||
{children, contentContainerStyle, header, footer, style, ...props},
|
||||
ref,
|
||||
) {
|
||||
const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} =
|
||||
useDialogContext()
|
||||
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
|
||||
const insets = useSafeAreaInsets()
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(() =>
|
||||
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
|
||||
)
|
||||
export function ScrollableInner({
|
||||
ref,
|
||||
children,
|
||||
contentContainerStyle,
|
||||
header,
|
||||
footer,
|
||||
style,
|
||||
...props
|
||||
}: DialogInnerProps & {
|
||||
ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
|
||||
}) {
|
||||
const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} =
|
||||
useDialogContext()
|
||||
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
|
||||
const insets = useSafeAreaInsets()
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(() =>
|
||||
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
|
||||
)
|
||||
|
||||
const keyboardEventHandler = useCallback<KeyboardEventListener>(e => {
|
||||
setKeyboardHeight(e.endCoordinates.height)
|
||||
}, [])
|
||||
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
|
||||
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
|
||||
const keyboardEventHandler = useCallback((e: KeyboardEvent) => {
|
||||
setKeyboardHeight(e.endCoordinates.height)
|
||||
}, [])
|
||||
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
|
||||
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
|
||||
|
||||
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (!IS_ANDROID) {
|
||||
return
|
||||
}
|
||||
const {contentOffset} = e.nativeEvent
|
||||
if (contentOffset.y > 0 && !disableDrag) {
|
||||
setDisableDrag(true)
|
||||
} else if (contentOffset.y <= 1 && disableDrag) {
|
||||
setDisableDrag(false)
|
||||
}
|
||||
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (!IS_ANDROID) {
|
||||
return
|
||||
}
|
||||
const {contentOffset} = e.nativeEvent
|
||||
if (contentOffset.y > 0 && !disableDrag) {
|
||||
setDisableDrag(true)
|
||||
} else if (contentOffset.y <= 1 && disableDrag) {
|
||||
setDisableDrag(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
style={[isHeightConstrained && a.flex_1, style]}
|
||||
contentContainerStyle={[
|
||||
a.pt_2xl,
|
||||
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
|
||||
platform({
|
||||
ios: a.pb_2xl,
|
||||
android: {
|
||||
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
|
||||
},
|
||||
}),
|
||||
contentContainerStyle,
|
||||
]}
|
||||
ref={ref}
|
||||
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
|
||||
contentInsetAdjustmentBehavior={
|
||||
isAtMaxSnapPoint ? 'automatic' : 'never'
|
||||
}
|
||||
automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint}
|
||||
{...props}
|
||||
bounces={isAtMaxSnapPoint}
|
||||
scrollEventThrottle={50}
|
||||
// set drag state based on scroll on android.
|
||||
// we want to detect if it's at the top or not, so watch
|
||||
// scrollEndDrag and momentumScrollEnd as well
|
||||
onScroll={android(onScroll)}
|
||||
onScrollEndDrag={android(onScroll)}
|
||||
onMomentumScrollEnd={android(onScroll)}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
||||
// on Android. fine to disable for now, because we don't have any
|
||||
// dialogs that use this that actually scroll -sfn
|
||||
stickyHeaderIndices={ios(header ? [0] : undefined)}>
|
||||
{header}
|
||||
{children}
|
||||
</ScrollView>
|
||||
{footer}
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
style={[isHeightConstrained && a.flex_1, style]}
|
||||
contentContainerStyle={[
|
||||
a.pt_2xl,
|
||||
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
|
||||
platform({
|
||||
ios: a.pb_2xl,
|
||||
android: {
|
||||
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
|
||||
},
|
||||
}),
|
||||
contentContainerStyle,
|
||||
]}
|
||||
ref={ref}
|
||||
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
|
||||
contentInsetAdjustmentBehavior={
|
||||
isAtMaxSnapPoint ? 'automatic' : 'never'
|
||||
}
|
||||
automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint}
|
||||
{...props}
|
||||
bounces={isAtMaxSnapPoint}
|
||||
scrollEventThrottle={50}
|
||||
// set drag state based on scroll on android.
|
||||
// we want to detect if it's at the top or not, so watch
|
||||
// scrollEndDrag and momentumScrollEnd as well
|
||||
onScroll={android(onScroll)}
|
||||
onScrollEndDrag={android(onScroll)}
|
||||
onMomentumScrollEnd={android(onScroll)}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
||||
// on Android. fine to disable for now, because we don't have any
|
||||
// dialogs that use this that actually scroll -sfn
|
||||
stickyHeaderIndices={ios(header ? [0] : undefined)}>
|
||||
{header}
|
||||
{children}
|
||||
</ScrollView>
|
||||
{footer}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const InnerFlatList = forwardRef<
|
||||
ListMethods,
|
||||
|
||||
@@ -193,7 +193,6 @@ export function Inner({
|
||||
aria-label={label}
|
||||
aria-labelledby={accessibilityLabelledBy}
|
||||
aria-describedby={accessibilityDescribedBy}
|
||||
// @ts-expect-error web only -prf
|
||||
onClick={stopPropagation}
|
||||
onStartShouldSetResponder={_ => true}
|
||||
onTouchEnd={stopPropagation}
|
||||
@@ -239,7 +238,9 @@ export function Inner({
|
||||
export function ScrollableInner({
|
||||
ref: _ref,
|
||||
...props
|
||||
}: DialogInnerProps & {ref?: React.Ref<ScrollView>}) {
|
||||
}: DialogInnerProps & {
|
||||
ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
|
||||
}) {
|
||||
return <Inner {...props} />
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {useCallback, useEffect, useMemo} from 'react'
|
||||
import {type GestureResponderEvent, View} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyGraphDefs,
|
||||
AtUri,
|
||||
RichText as RichTextApi,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {RichText as RichTextApi} from '@bsky/sdk/richtext'
|
||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -35,11 +31,12 @@ import {RichText, type RichTextProps} from '#/components/RichText'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useActiveLiveEventFeedUris} from '#/features/liveEvents/context'
|
||||
import {type app} from '#/lexicons'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from './icons/Trash'
|
||||
|
||||
type Props = {
|
||||
view: AppBskyFeedDefs.GeneratorView
|
||||
view: app.bsky.feed.defs.GeneratorView
|
||||
onPress?: () => void
|
||||
}
|
||||
|
||||
@@ -258,7 +255,7 @@ export function SaveButton({
|
||||
pin,
|
||||
...props
|
||||
}: {
|
||||
view: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView
|
||||
view: app.bsky.feed.defs.GeneratorView | app.bsky.graph.defs.ListView
|
||||
pin?: boolean
|
||||
text?: boolean
|
||||
} & Partial<ButtonProps>) {
|
||||
@@ -273,7 +270,7 @@ function SaveButtonInner({
|
||||
text = true,
|
||||
...buttonProps
|
||||
}: {
|
||||
view: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView
|
||||
view: app.bsky.feed.defs.GeneratorView | app.bsky.graph.defs.ListView
|
||||
pin?: boolean
|
||||
text?: boolean
|
||||
} & Partial<ButtonProps>) {
|
||||
@@ -383,7 +380,7 @@ function SaveButtonInner({
|
||||
export function createProfileFeedHref({
|
||||
feed,
|
||||
}: {
|
||||
feed: AppBskyFeedDefs.GeneratorView
|
||||
feed: app.bsky.feed.defs.GeneratorView
|
||||
}) {
|
||||
const urip = new AtUri(feed.uri)
|
||||
const handleOrDid = feed.creator.handle || feed.creator.did
|
||||
|
||||
@@ -7,7 +7,6 @@ import Animated, {
|
||||
LayoutAnimationConfig,
|
||||
LinearTransition,
|
||||
} from 'react-native-reanimated'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
@@ -40,6 +39,7 @@ import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type Metrics, useAnalytics} from '#/analytics'
|
||||
import {IS_IOS} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog'
|
||||
|
||||
@@ -217,7 +217,7 @@ export function ProfileGrid({
|
||||
|
||||
// Track seen profiles
|
||||
const seenProfilesRef = useRef<Set<string>>(new Set())
|
||||
const containerRef = useRef<View>(null)
|
||||
const containerRef = useRef<React.ComponentRef<typeof View>>(null)
|
||||
const hasTrackedRef = useRef(false)
|
||||
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
|
||||
? 'DiscoverInterstitial'
|
||||
@@ -276,7 +276,7 @@ export function ProfileGrid({
|
||||
},
|
||||
{threshold: 0.5},
|
||||
)
|
||||
// @ts-ignore - web only
|
||||
// @ts-expect-error - web only
|
||||
observer.observe(node)
|
||||
return () => observer.disconnect()
|
||||
} else {
|
||||
@@ -569,7 +569,7 @@ export function SuggestedFeeds() {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const feeds = useMemo(() => {
|
||||
const items: AppBskyFeedDefs.GeneratorView[] = []
|
||||
const items: app.bsky.feed.defs.GeneratorView[] = []
|
||||
|
||||
if (!data) return items
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export function FocusScope({children}: {children: React.ReactNode}) {
|
||||
*/
|
||||
function FocusTrap({children}: {children: React.ReactNode}) {
|
||||
const {_} = useLingui()
|
||||
const child = useRef<View>(null)
|
||||
const child = useRef<React.ComponentRef<typeof View>>(null)
|
||||
|
||||
/*
|
||||
* Here we add a ref to the first child of this component. This currently
|
||||
@@ -66,13 +66,16 @@ function FocusTrap({children}: {children: React.ReactNode}) {
|
||||
})
|
||||
}, [children])
|
||||
|
||||
const focusNode = useCallback((ref: View | null) => {
|
||||
if (!ref) return
|
||||
const node = findNodeHandle(ref)
|
||||
if (node) {
|
||||
AccessibilityInfo.setAccessibilityFocus(node)
|
||||
}
|
||||
}, [])
|
||||
const focusNode = useCallback(
|
||||
(ref: React.ComponentRef<typeof View> | null) => {
|
||||
if (!ref) return
|
||||
const node = findNodeHandle(ref)
|
||||
if (node) {
|
||||
AccessibilityInfo.setAccessibilityFocus(node)
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -20,7 +20,7 @@ export const IS_GLASS_AVAILABLE =
|
||||
*/
|
||||
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
|
||||
|
||||
export type GlassViewProps = ExpoGlassViewProps & {
|
||||
export type GlassViewProps = Omit<ExpoGlassViewProps, 'ref'> & {
|
||||
fallbackStyle?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export function InterestTabs({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const listRef = useRef<ScrollView>(null)
|
||||
const listRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
|
||||
const [totalWidth, setTotalWidth] = useState(0)
|
||||
const [scrollX, setScrollX] = useState(0)
|
||||
const [contentWidth, setContentWidth] = useState(0)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {useRef} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {moderateProfile, type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
@@ -13,6 +9,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Link, type LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type app} from '#/lexicons'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
const AVI_SIZE = 30
|
||||
@@ -26,7 +23,7 @@ const AVI_BORDER = 1
|
||||
* `count` includes blocked users and `followers` does not.
|
||||
*/
|
||||
export function shouldShowKnownFollowers(
|
||||
knownFollowers?: AppBskyActorDefs.KnownFollowers,
|
||||
knownFollowers?: app.bsky.actor.defs.KnownFollowers,
|
||||
) {
|
||||
return knownFollowers && knownFollowers.followers.length > 0
|
||||
}
|
||||
@@ -44,7 +41,9 @@ export function KnownFollowers({
|
||||
minimal?: boolean
|
||||
showIfEmpty?: boolean
|
||||
}) {
|
||||
const cache = useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(new Map())
|
||||
const cache = useRef<Map<string, app.bsky.actor.defs.KnownFollowers>>(
|
||||
new Map(),
|
||||
)
|
||||
|
||||
/*
|
||||
* Results for `knownFollowers` are not sorted consistently, so when
|
||||
@@ -85,7 +84,7 @@ function KnownFollowersInner({
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||
cachedKnownFollowers: app.bsky.actor.defs.KnownFollowers
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
showIfEmpty?: boolean
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
@@ -13,10 +12,11 @@ import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||
import {Link as InternalLink, type LinkProps} from '#/components/Link'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type app} from '#/lexicons'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
|
||||
|
||||
type LabelingServiceProps = {
|
||||
labeler: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
labeler: app.bsky.labeler.defs.LabelerViewDetailed
|
||||
}
|
||||
|
||||
export function Outer({
|
||||
@@ -187,7 +187,7 @@ export function Loader({
|
||||
loading?: React.ComponentType<{}>
|
||||
error?: React.ComponentType<{error: string}>
|
||||
component: React.ComponentType<{
|
||||
labeler: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
labeler: app.bsky.labeler.defs.LabelerViewDetailed
|
||||
}>
|
||||
}) {
|
||||
const {isLoading, data, error} = useLabelerInfoQuery({did})
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Outer({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
noBottomBorder?: boolean
|
||||
headerRef?: React.RefObject<View | null>
|
||||
headerRef?: React.RefObject<React.ComponentRef<typeof View> | null>
|
||||
sticky?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -446,9 +446,7 @@ function LightboxGalleryItem({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avi: {
|
||||
// @ts-ignore web-only
|
||||
maxWidth: `calc(min(400px, 100vw))`,
|
||||
// @ts-ignore web-only
|
||||
maxHeight: `calc(min(400px, 100vh))`,
|
||||
padding: 16,
|
||||
boxSizing: 'border-box',
|
||||
@@ -458,7 +456,6 @@ const styles = StyleSheet.create({
|
||||
// column via ScrollView's default flexGrow.
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
// @ts-ignore web-only -sfn
|
||||
maxHeight: '50vh',
|
||||
},
|
||||
menuBtn: {
|
||||
|
||||
@@ -35,7 +35,7 @@ const TIMING_OUT = {duration: 150}
|
||||
|
||||
export function ImageMenu({onPressShare, onPressSave}: Props) {
|
||||
const {t: l} = useLingui()
|
||||
const triggerRef = useRef<View>(null)
|
||||
const triggerRef = useRef<React.ComponentRef<typeof View>>(null)
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
const [anchor, setAnchor] = useState<Anchor | null>(null)
|
||||
const progress = useSharedValue(0)
|
||||
|
||||
@@ -82,7 +82,7 @@ const ImageItem = ({
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
onScroll(e) {
|
||||
'worklet'
|
||||
const nextIsScaled = e.zoomScale > 1
|
||||
const nextIsScaled = (e.zoomScale ?? 1) > 1
|
||||
if (scaled !== nextIsScaled) {
|
||||
scheduleOnRN(handleZoom, nextIsScaled)
|
||||
}
|
||||
@@ -109,7 +109,6 @@ const ImageItem = ({
|
||||
height: number
|
||||
}) {
|
||||
const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
|
||||
// @ts-ignore
|
||||
scrollResponderRef?.scrollResponderZoomTo({
|
||||
...nextZoomRect, // This rect is in screen coordinates
|
||||
animated: true,
|
||||
@@ -218,7 +217,6 @@ const ImageItem = ({
|
||||
return (
|
||||
<GestureDetector gesture={composedGesture}>
|
||||
<Animated.ScrollView
|
||||
// @ts-ignore Something's up with the types here
|
||||
ref={scrollViewRef}
|
||||
pinchGestureEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
|
||||
@@ -11,7 +11,10 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||
import {SystemBars} from 'react-native-edge-to-edge'
|
||||
import {Gesture} from 'react-native-gesture-handler'
|
||||
import PagerView from 'react-native-pager-view'
|
||||
import PagerView, {
|
||||
type PagerViewOnPageSelectedEvent,
|
||||
type PageScrollStateChangedNativeEvent,
|
||||
} from 'react-native-pager-view'
|
||||
import Animated, {
|
||||
type AnimatableValue,
|
||||
type AnimatedRef,
|
||||
@@ -119,16 +122,12 @@ export default function ImageViewRoot({
|
||||
|
||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||
rAF_FIXED(() => {
|
||||
openProgress.set(() =>
|
||||
isAnimated ? withClampedSpring(1, SLOW_SPRING) : 1,
|
||||
)
|
||||
openProgress.set(isAnimated ? withClampedSpring(1, SLOW_SPRING) : 1)
|
||||
})
|
||||
return () => {
|
||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||
rAF_FIXED(() => {
|
||||
openProgress.set(() =>
|
||||
isAnimated ? withClampedSpring(0, SLOW_SPRING) : 0,
|
||||
)
|
||||
openProgress.set(isAnimated ? withClampedSpring(0, SLOW_SPRING) : 0)
|
||||
})
|
||||
}
|
||||
}, [nextLightbox, openProgress, thumbRects])
|
||||
@@ -387,7 +386,7 @@ function ImageView({
|
||||
<PagerView
|
||||
scrollEnabled={!isScaled}
|
||||
initialPage={initialImageIndex}
|
||||
onPageSelected={e => {
|
||||
onPageSelected={(e: PagerViewOnPageSelectedEvent) => {
|
||||
const next = e.nativeEvent.position
|
||||
setImageIndex(prev => {
|
||||
if (metricsContext && prev !== next) {
|
||||
@@ -405,7 +404,7 @@ function ImageView({
|
||||
})
|
||||
setIsScaled(false)
|
||||
}}
|
||||
onPageScrollStateChanged={e => {
|
||||
onPageScrollStateChanged={(e: PageScrollStateChangedNativeEvent) => {
|
||||
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
|
||||
}}
|
||||
overdrag={true}
|
||||
@@ -591,25 +590,23 @@ function LightboxImage({
|
||||
// This is a bug in Reanimated, but for now we'll work around it like this.
|
||||
dismissSwipeTranslateY.set(1)
|
||||
}
|
||||
dismissSwipeTranslateY.set(() => {
|
||||
'worklet'
|
||||
return withDecay({
|
||||
dismissSwipeTranslateY.set(
|
||||
withDecay({
|
||||
velocity: e.velocityY,
|
||||
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
|
||||
deceleration: 1, // Danger! This relies on the reaction below stopping it.
|
||||
reduceMotion: ReduceMotion.Never, // If this animation doesn't run, the image gets stuck - therefore override Reduce Motion
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
dismissSwipeTranslateY.set(() => {
|
||||
'worklet'
|
||||
return withSpring(0, {
|
||||
dismissSwipeTranslateY.set(
|
||||
withSpring(0, {
|
||||
stiffness: 700,
|
||||
damping: 50,
|
||||
mass: 1,
|
||||
reduceMotion: ReduceMotion.Never,
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {type AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -11,8 +10,15 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
|
||||
function renderItem({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: app.bsky.feed.getLikes.Like
|
||||
index: number
|
||||
}) {
|
||||
return (
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.actor.did}
|
||||
@@ -22,7 +28,7 @@ function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
|
||||
)
|
||||
}
|
||||
|
||||
function keyExtractor(item: GetLikes.Like) {
|
||||
function keyExtractor(item: app.bsky.feed.getLikes.Like) {
|
||||
return item.actor.did
|
||||
}
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ export function InlineLinkText({
|
||||
onIn: onInteract,
|
||||
onOut: onInteractOut,
|
||||
} = useInteractionState()
|
||||
const flattenedStyle = flatten(style) || {}
|
||||
const flattenedStyle = flatten(style)
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -570,7 +570,7 @@ export function SimpleInlineLinkText({
|
||||
onIn: onInteract,
|
||||
onOut: onInteractOut,
|
||||
} = useInteractionState()
|
||||
const flattenedStyle = flatten(style) || {}
|
||||
const flattenedStyle = flatten(style)
|
||||
const isExternal = isExternalUrl(to)
|
||||
|
||||
let href = to
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {useEffect, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyGraphDefs,
|
||||
AtUri,
|
||||
moderateUserList,
|
||||
type ModerationUI,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {moderateUserList, type ModerationUI} from '@bsky/sdk/moderation'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -26,6 +22,7 @@ import {
|
||||
import {Link as InternalLink, type LinkProps} from '#/components/Link'
|
||||
import * as Hider from '#/components/moderation/Hider'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type app} from '#/lexicons'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
/*
|
||||
@@ -47,7 +44,7 @@ const CURATELIST = 'app.bsky.graph.defs#curatelist'
|
||||
const MODLIST = 'app.bsky.graph.defs#modlist'
|
||||
|
||||
type Props = {
|
||||
view: AppBskyGraphDefs.ListView
|
||||
view: app.bsky.graph.defs.ListView
|
||||
showPinButton?: boolean
|
||||
}
|
||||
|
||||
@@ -111,7 +108,7 @@ export function TitleAndByline({
|
||||
}: {
|
||||
title: string
|
||||
creator?: bsky.profile.AnyProfileView
|
||||
purpose?: AppBskyGraphDefs.ListView['purpose']
|
||||
purpose?: app.bsky.graph.defs.ListView['purpose']
|
||||
modUi?: ModerationUI
|
||||
}) {
|
||||
const t = useTheme()
|
||||
@@ -160,7 +157,7 @@ export function TitleAndByline({
|
||||
export function createProfileListHref({
|
||||
list,
|
||||
}: {
|
||||
list: AppBskyGraphDefs.ListView
|
||||
list: app.bsky.graph.defs.ListView
|
||||
}) {
|
||||
const urip = new AtUri(list.uri)
|
||||
const handleOrDid = list.creator.handle || list.creator.did
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Loader(props: Props) {
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
rotation.set(() =>
|
||||
rotation.set(
|
||||
withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
|
||||
)
|
||||
}, [rotation])
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {
|
||||
AppBskyEmbedGallery,
|
||||
type AppBskyEmbedImages,
|
||||
type AppBskyFeedDefs,
|
||||
} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {shareImageModal} from '#/lib/media/manip'
|
||||
@@ -17,6 +12,7 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import * as PeekMenu from '#/components/PeekMenu'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
/**
|
||||
@@ -27,7 +23,7 @@ export function Embed({
|
||||
style,
|
||||
peekable = false,
|
||||
}: {
|
||||
embed: AppBskyFeedDefs.PostView['embed']
|
||||
embed: app.bsky.feed.defs.PostView['embed']
|
||||
style?: StyleProp<ViewStyle>
|
||||
peekable?: boolean
|
||||
}) {
|
||||
@@ -59,9 +55,9 @@ export function Embed({
|
||||
const tiles: React.ReactNode[] = []
|
||||
for (const item of e.view.items) {
|
||||
if (tiles.length >= 4) break
|
||||
if (!AppBskyEmbedGallery.isViewImage(item)) continue
|
||||
if (!bsky.isType(app.bsky.embed.gallery.viewImage, item)) continue
|
||||
if (peekable) {
|
||||
const image: AppBskyEmbedImages.ViewImage = {
|
||||
const image: app.bsky.embed.images.ViewImage = {
|
||||
thumb: item.thumbnail,
|
||||
fullsize: item.fullsize,
|
||||
alt: item.alt,
|
||||
@@ -107,7 +103,17 @@ export function Embed({
|
||||
// ignore any unknowns
|
||||
e.media.view !== null
|
||||
) {
|
||||
return <Embed embed={e.media.view} style={style} />
|
||||
/*
|
||||
* Every media arm's view is a `$Typed<...>` of a def that `PostView.embed`
|
||||
* also admits, but TS will not narrow the parsed union back into the raw
|
||||
* embed union, so the arm is re-asserted here.
|
||||
*/
|
||||
return (
|
||||
<Embed
|
||||
embed={e.media.view as app.bsky.feed.defs.PostView['embed']}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -202,7 +208,7 @@ export function VideoItem({
|
||||
)
|
||||
}
|
||||
|
||||
function PeekableImageItem({image}: {image: AppBskyEmbedImages.ViewImage}) {
|
||||
function PeekableImageItem({image}: {image: app.bsky.embed.images.ViewImage}) {
|
||||
const {t: l} = useLingui()
|
||||
const saveImage = useSaveImageToMediaLibrary()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {DropdownMenu} from 'radix-ui'
|
||||
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {atoms as a, flatten, useTheme, web} from '#/alf'
|
||||
import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {
|
||||
@@ -413,7 +413,7 @@ export function Divider() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<DropdownMenu.Separator
|
||||
style={flatten([
|
||||
style={flattenToCSS([
|
||||
a.my_xs,
|
||||
t.atoms.bg_contrast_100,
|
||||
a.flex_shrink_0,
|
||||
|
||||
@@ -28,7 +28,7 @@ export type RadixPassThroughTriggerProps = {
|
||||
['aria-controls']?: string
|
||||
['aria-haspopup']?: boolean
|
||||
['aria-expanded']?: AccessibilityProps['aria-expanded']
|
||||
onKeyDown: (e: React.KeyboardEvent) => void
|
||||
onKeyDown: PressableProps['onKeyDown']
|
||||
/**
|
||||
* Radix provides this, but we override on web to use `onPress` instead,
|
||||
* which is less sensitive while scrolling.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {moderateProfile} from '@bsky/sdk/moderation'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -19,12 +19,13 @@ import {Newskie} from '#/components/icons/Newskie'
|
||||
import * as StarterPackCard from '#/components/StarterPack/StarterPackCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export function NewskieDialog({
|
||||
profile,
|
||||
disabled,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: app.bsky.actor.defs.ProfileViewDetailed
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
@@ -74,7 +75,7 @@ function DialogInner({
|
||||
createdAt,
|
||||
now,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: app.bsky.actor.defs.ProfileViewDetailed
|
||||
createdAt: string
|
||||
now: number
|
||||
}) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api'
|
||||
import {api} from '@bsky/sdk'
|
||||
import {type ModerationCause} from '@bsky/sdk/moderation'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
@@ -66,7 +67,7 @@ export function Label({
|
||||
const desc = useModerationCauseDescription(cause)
|
||||
const isLabeler = Boolean(desc.sourceType && desc.sourceDid)
|
||||
const isBlueskyLabel =
|
||||
desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID
|
||||
desc.sourceType === 'labeler' && desc.sourceDid === api.moderation.did
|
||||
const avi = size === 'lg' ? 16 : 12
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as ChatInvite from '#/components/dms/ChatInvite'
|
||||
import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed'
|
||||
import {JoinRequestEmbedBody} from '#/components/Post/Embed/JoinRequestEmbed'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
/**
|
||||
* Renders a chat invite link found in an `app.bsky.embed.external` embed (e.g.
|
||||
@@ -18,7 +18,7 @@ export function ChatInviteEmbed({
|
||||
style,
|
||||
}: {
|
||||
code: string
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
link: app.bsky.embed.external.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
@@ -34,7 +34,7 @@ function ChatInviteEmbedBody({
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
link: app.bsky.embed.external.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Pressable,
|
||||
} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -17,12 +16,13 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export function ExternalGif({
|
||||
link,
|
||||
params,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
link: app.bsky.embed.external.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -15,7 +15,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {WebView} from 'react-native-webview'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {Image} from 'expo-image'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
@@ -34,6 +33,7 @@ import {Fill} from '#/components/Fill'
|
||||
import {KeepAwake} from '#/components/KeepAwake'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
interface ShouldStartLoadRequest {
|
||||
url: string
|
||||
@@ -122,7 +122,7 @@ export function ExternalPlayer({
|
||||
link,
|
||||
params,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
link: app.bsky.embed.external.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useMemo} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -20,6 +19,7 @@ import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
import {ExternalGif} from './ExternalGif'
|
||||
import {ExternalPlayer} from './ExternalPlayer'
|
||||
import {GifEmbed} from './Gif'
|
||||
@@ -30,7 +30,7 @@ export const ExternalEmbed = ({
|
||||
style,
|
||||
hideAlt,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
link: app.bsky.embed.external.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
hideAlt?: boolean
|
||||
@@ -92,7 +92,7 @@ export const ExternalEmbed = ({
|
||||
style={[a.rounded_md]}
|
||||
onPress={onPress}
|
||||
onLongPress={onShareExternal}>
|
||||
{({hovered}) => (
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.transition_color,
|
||||
@@ -101,7 +101,7 @@ export const ExternalEmbed = ({
|
||||
a.overflow_hidden,
|
||||
a.w_full,
|
||||
a.border,
|
||||
t.atoms.bg,
|
||||
pressed && t.atoms.bg,
|
||||
style,
|
||||
hovered
|
||||
? t.atoms.border_contrast_high
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useMemo} from 'react'
|
||||
import {moderateFeedGenerator} from '@atproto/api'
|
||||
import {moderateFeedGenerator} from '@bsky/sdk/moderation'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
@@ -2,7 +2,6 @@ import {useRef} from 'react'
|
||||
import {InteractionManager, View} from 'react-native'
|
||||
import {type AnimatedRef} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
import {AppBskyEmbedGallery, type AppBskyEmbedImages} from '@atproto/api'
|
||||
|
||||
import {atoms as a, tokens} from '#/alf'
|
||||
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
||||
@@ -16,6 +15,8 @@ import {type Dimensions} from '#/components/Lightbox/types'
|
||||
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
@@ -29,14 +30,16 @@ export function ImageEmbed({
|
||||
}) {
|
||||
const ax = useAnalytics()
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const images: AppBskyEmbedImages.ViewImage[] =
|
||||
const images: app.bsky.embed.images.ViewImage[] =
|
||||
embed.type === 'gallery'
|
||||
? embed.view.items.filter(AppBskyEmbedGallery.isViewImage).map(item => ({
|
||||
thumb: item.thumbnail,
|
||||
fullsize: item.fullsize,
|
||||
alt: item.alt,
|
||||
aspectRatio: item.aspectRatio,
|
||||
}))
|
||||
? embed.view.items
|
||||
.filter(item => bsky.isType(app.bsky.embed.gallery.viewImage, item))
|
||||
.map(item => ({
|
||||
thumb: item.thumbnail,
|
||||
fullsize: item.fullsize,
|
||||
alt: item.alt,
|
||||
aspectRatio: item.aspectRatio,
|
||||
}))
|
||||
: embed.view.images
|
||||
const useExpandedLayout =
|
||||
embed.type === 'gallery'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useMemo} from 'react'
|
||||
import {moderateUserList} from '@atproto/api'
|
||||
import {moderateUserList} from '@bsky/sdk/moderation'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Fragment, type ReactNode} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
import {Context, useAlf, utils} from '#/alf'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
/**
|
||||
* Overrides only the values needed for `secondary_inverted` buttons atm.
|
||||
@@ -12,7 +11,7 @@ export function StandardSiteThemeProvider({
|
||||
view,
|
||||
children,
|
||||
}: {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
view: app.bsky.embed.external.ViewExternal
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const alf = useAlf()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
@@ -129,7 +129,6 @@ export const StandardSiteEmbed = ({
|
||||
a.rounded_lg,
|
||||
a.w_full,
|
||||
a.border,
|
||||
t.atoms.bg,
|
||||
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
|
||||
preview && a.pointer_events_none,
|
||||
style,
|
||||
@@ -154,7 +153,7 @@ export const StandardSiteEmbed = ({
|
||||
})}
|
||||
onFocus={onInteract}
|
||||
onBlur={onInteractOut}>
|
||||
{() => (
|
||||
{({pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
@@ -171,7 +170,7 @@ export const StandardSiteEmbed = ({
|
||||
borderBottomLeftRadius: a.rounded_lg.borderRadius,
|
||||
borderBottomRightRadius: a.rounded_lg.borderRadius,
|
||||
},
|
||||
interacted ? t.atoms.bg_contrast_25 : t.atoms.bg,
|
||||
interacted ? t.atoms.bg_contrast_25 : pressed && t.atoms.bg,
|
||||
]}>
|
||||
{imageUri ? (
|
||||
<Image
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
import {Leaflet} from '#/components/icons/community/Leaflet'
|
||||
import {Offprint} from '#/components/icons/community/Offprint'
|
||||
import {Pckt} from '#/components/icons/community/Pckt'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export type StandardSitePublisher = {
|
||||
host: string
|
||||
@@ -25,7 +24,7 @@ function hostFromUri(uri: string | undefined): string | null {
|
||||
}
|
||||
|
||||
export function getStandardSitePublisherHost(
|
||||
view: AppBskyEmbedExternal.ViewExternal,
|
||||
view: app.bsky.embed.external.ViewExternal,
|
||||
): string | null {
|
||||
return hostFromUri(view.source?.uri)
|
||||
}
|
||||
@@ -40,7 +39,7 @@ function matchByHost(host: string | null): StandardSitePublisher | null {
|
||||
}
|
||||
|
||||
export function matchStandardSitePublisher(
|
||||
view: AppBskyEmbedExternal.ViewExternal,
|
||||
view: app.bsky.embed.external.ViewExternal,
|
||||
): StandardSitePublisher | null {
|
||||
return matchByHost(getStandardSitePublisherHost(view))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export type CommonProps = {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
view: app.bsky.embed.external.ViewExternal
|
||||
}
|
||||
|
||||
export type PreviewProps = {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
import {type app} from '#/lexicons'
|
||||
import {isStandardSiteEmbed, isStandardSitePublicationEmbed} from './utils'
|
||||
|
||||
function makeView(
|
||||
partial: Record<string, unknown>,
|
||||
): AppBskyEmbedExternal.ViewExternal {
|
||||
): app.bsky.embed.external.ViewExternal {
|
||||
return {
|
||||
uri: 'https://example.com/post',
|
||||
title: 'title',
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import {
|
||||
type AppBskyEmbedExternal,
|
||||
AtUri,
|
||||
type ComAtprotoRepoStrongRef,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
|
||||
export function isStandardSiteDocumentUri(ref: ComAtprotoRepoStrongRef.Main) {
|
||||
import {type app, type com} from '#/lexicons'
|
||||
|
||||
export function isStandardSiteDocumentUri(
|
||||
ref: com.atproto.repo.strongRef.Main,
|
||||
) {
|
||||
return new AtUri(ref.uri).collection.startsWith('site.standard.document')
|
||||
}
|
||||
|
||||
export function isStandardSitePublicationUri(
|
||||
ref: ComAtprotoRepoStrongRef.Main,
|
||||
ref: com.atproto.repo.strongRef.Main,
|
||||
) {
|
||||
return new AtUri(ref.uri).collection.startsWith('site.standard.publication')
|
||||
}
|
||||
|
||||
export function isStandardSiteUri(ref: ComAtprotoRepoStrongRef.Main) {
|
||||
export function isStandardSiteUri(ref: com.atproto.repo.strongRef.Main) {
|
||||
return new AtUri(ref.uri).collection.startsWith('site.standard.')
|
||||
}
|
||||
|
||||
export function isStandardSiteEmbed(view: AppBskyEmbedExternal.ViewExternal) {
|
||||
export function isStandardSiteEmbed(
|
||||
view: app.bsky.embed.external.ViewExternal,
|
||||
) {
|
||||
return view.associatedRefs?.some(ref => isStandardSiteUri(ref))
|
||||
}
|
||||
|
||||
export function isStandardSitePublicationEmbed(
|
||||
view: AppBskyEmbedExternal.ViewExternal,
|
||||
view: app.bsky.embed.external.ViewExternal,
|
||||
) {
|
||||
return (
|
||||
view.associatedRefs?.some(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useImperativeHandle, useRef, useState} from 'react'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@bsky.app/video'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
@@ -17,6 +16,7 @@ import {KeepAwake} from '#/components/KeepAwake'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useReportDialogMetadataContext} from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
|
||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {type app} from '#/lexicons'
|
||||
import {GifPresentationControls} from '../GifPresentationControls'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
@@ -29,7 +29,7 @@ export function VideoEmbedInnerNative({
|
||||
onError,
|
||||
}: {
|
||||
ref: React.Ref<{togglePlayback: () => void}>
|
||||
embed: AppBskyEmbedVideo.View
|
||||
embed: app.bsky.embed.video.View
|
||||
setStatus: (status: 'playing' | 'paused') => void
|
||||
setIsLoading: (isLoading: boolean) => void
|
||||
setIsActive: (isActive: boolean) => void
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export type VideoEmbedInnerWebProps = {
|
||||
embed: AppBskyEmbedVideo.View
|
||||
embed: app.bsky.embed.video.View
|
||||
active: boolean
|
||||
setActive: () => void
|
||||
onScreen: boolean
|
||||
|
||||
@@ -149,7 +149,7 @@ function canPlayBskyVideoCodecs(): boolean {
|
||||
|
||||
type CachedPromise<T> = Promise<T> & {value: undefined | T}
|
||||
const promiseForHls = import(
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
'hls.js/dist/hls.min'
|
||||
// oxlint-disable-next-line typescript/no-unsafe-member-access
|
||||
).then(mod => mod.default) as CachedPromise<typeof HlsTypes.default>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -17,12 +16,13 @@ import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {type app} from '#/lexicons'
|
||||
import {GifPresentationControls} from './GifPresentationControls'
|
||||
import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
interface Props {
|
||||
embed: AppBskyEmbedVideo.View
|
||||
embed: app.bsky.embed.video.View
|
||||
}
|
||||
|
||||
export function VideoEmbed({embed}: Props) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_WEB_FIREFOX} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
import {useActiveVideoWeb} from './ActiveVideoWebContext'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
@@ -37,7 +37,7 @@ const noop = () => {}
|
||||
*/
|
||||
const MIN_CARD_WIDTH = 280
|
||||
|
||||
export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
|
||||
export function VideoEmbed({embed}: {embed: app.bsky.embed.video.View}) {
|
||||
const t = useTheme()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
@@ -297,7 +297,7 @@ function VideoError({
|
||||
error,
|
||||
retry,
|
||||
}: {
|
||||
embed: AppBskyEmbedVideo.View
|
||||
embed: app.bsky.embed.video.View
|
||||
error: unknown
|
||||
retry: () => void
|
||||
}) {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type $Typed,
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AtUri,
|
||||
moderatePost,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {type $Typed} from '@atproto/lex'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {moderatePost} from '@bsky/sdk/moderation'
|
||||
import {RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -29,6 +25,7 @@ import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/uti
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {
|
||||
type Embed as TEmbed,
|
||||
@@ -68,7 +65,11 @@ export function Embed({embed: rawEmbed, ...rest}: EmbedProps) {
|
||||
}
|
||||
case 'post_with_media': {
|
||||
return (
|
||||
<View style={rest.style}>
|
||||
<View
|
||||
style={[
|
||||
rest.style,
|
||||
rest.viewContext === PostEmbedViewContext.ChatMessage && a.gap_sm,
|
||||
]}>
|
||||
<MediaEmbed embed={embed.media} {...rest} />
|
||||
<RecordEmbed embed={embed.view} {...rest} />
|
||||
</View>
|
||||
@@ -262,7 +263,7 @@ export function QuoteEmbed({
|
||||
linkDisabled?: boolean
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const quote = useMemo<$Typed<AppBskyFeedDefs.PostView>>(
|
||||
const quote = useMemo<$Typed<app.bsky.feed.defs.PostView>>(
|
||||
() => ({
|
||||
...embed.view,
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
@@ -282,13 +283,7 @@ export function QuoteEmbed({
|
||||
const itemTitle = `Post by ${quote.author.handle}`
|
||||
|
||||
const richText = useMemo(() => {
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
quote.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
)
|
||||
return undefined
|
||||
if (!bsky.isType(app.bsky.feed.post, quote.record)) return undefined
|
||||
const {text, facets} = quote.record
|
||||
return text.trim()
|
||||
? new RichTextAPI({text: text, facets: facets})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api'
|
||||
import {type ModerationDecision} from '@bsky/sdk/moderation'
|
||||
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export enum PostEmbedViewContext {
|
||||
ThreadHighlighted = 'ThreadHighlighted',
|
||||
@@ -19,10 +21,10 @@ export type CommonProps = {
|
||||
* events (post:photoEmbed:*). When the embed has no owning post (e.g.
|
||||
* composer previews), leave this undefined and no events will be emitted.
|
||||
*/
|
||||
post?: AppBskyFeedDefs.PostView
|
||||
post?: app.bsky.feed.defs.PostView
|
||||
feedDescriptor?: string
|
||||
}
|
||||
|
||||
export type EmbedProps = CommonProps & {
|
||||
embed?: AppBskyFeedDefs.PostView['embed']
|
||||
embed?: app.bsky.feed.defs.PostView['embed']
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {Platform, type StyleProp, type TextStyle, View} from 'react-native'
|
||||
import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
@@ -28,6 +27,7 @@ import * as Select from '#/components/Select'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const X_ICON_OFFSET = 16
|
||||
@@ -38,7 +38,7 @@ export function TranslatedPost({
|
||||
postTextStyle = a.text_md,
|
||||
}: {
|
||||
hideTranslateLink?: boolean
|
||||
post: AppBskyFeedDefs.PostView
|
||||
post: app.bsky.feed.defs.PostView
|
||||
postTextStyle?: StyleProp<TextStyle>
|
||||
}) {
|
||||
const langPrefs = useLanguagePrefs()
|
||||
@@ -46,11 +46,8 @@ export function TranslatedPost({
|
||||
key: post.uri,
|
||||
})
|
||||
|
||||
const record = useMemo<AppBskyFeedPost.Record | undefined>(() => {
|
||||
return bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
const record = useMemo<app.bsky.feed.post.Main | undefined>(() => {
|
||||
return bsky.isType(app.bsky.feed.post, post.record)
|
||||
? post.record
|
||||
: undefined
|
||||
}, [post])
|
||||
@@ -270,7 +267,7 @@ function TranslationResult({
|
||||
? codeToLanguageName(resultSourceLanguage, i18n.locale)
|
||||
: undefined
|
||||
|
||||
const flattenedStyle = flatten(postTextStyle) ?? {}
|
||||
const flattenedStyle = flatten(postTextStyle)
|
||||
const fontSize = flattenedStyle.fontSize
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {memo} from 'react'
|
||||
import {type Insets} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -15,6 +14,7 @@ import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark'
|
||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||
import * as toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {type app} from '#/lexicons'
|
||||
import {PostControlButton, PostControlButtonIcon} from './PostControlButton'
|
||||
|
||||
export const BookmarkButton = memo(function BookmarkButton({
|
||||
@@ -23,7 +23,7 @@ export const BookmarkButton = memo(function BookmarkButton({
|
||||
logContext,
|
||||
hitSlop,
|
||||
}: {
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
post: Shadow<app.bsky.feed.defs.PostView>
|
||||
big?: boolean
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
hitSlop?: Insets
|
||||
|
||||
@@ -27,7 +27,7 @@ export function PostControlButton({
|
||||
activeColor,
|
||||
...props
|
||||
}: Omit<ButtonProps, 'hitSlop'> & {
|
||||
ref?: React.Ref<View>
|
||||
ref?: React.Ref<React.ComponentRef<typeof View>>
|
||||
active?: boolean
|
||||
big?: boolean
|
||||
color?: string
|
||||
|
||||
@@ -6,13 +6,8 @@ import {
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {type RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
@@ -97,6 +92,7 @@ import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_INTERNAL} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
let PostMenuItems = ({
|
||||
post,
|
||||
@@ -110,17 +106,17 @@ let PostMenuItems = ({
|
||||
forceGoogleTranslate,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
post: Shadow<app.bsky.feed.defs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
postReqId: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
record: app.bsky.feed.post.Main
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
threadgateRecord?: app.bsky.feed.threadgate.Main
|
||||
onShowLess?: (interaction: app.bsky.feed.defs.Interaction) => void
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
forceGoogleTranslate: boolean
|
||||
}): React.ReactNode => {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
import {type Insets} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {type RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
@@ -13,6 +8,7 @@ import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
import {type app} from '#/lexicons'
|
||||
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
|
||||
import {PostMenuItems} from './PostMenuItems'
|
||||
|
||||
@@ -32,15 +28,15 @@ let PostMenuButton = ({
|
||||
forceGoogleTranslate,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
post: Shadow<app.bsky.feed.defs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
postReqId: string | undefined
|
||||
big?: boolean
|
||||
record: AppBskyFeedPost.Record
|
||||
record: app.bsky.feed.post.Main
|
||||
richText: RichTextAPI
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
threadgateRecord?: app.bsky.feed.threadgate.Main
|
||||
onShowLess?: (interaction: app.bsky.feed.defs.Interaction) => void
|
||||
hitSlop?: Insets
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
forceGoogleTranslate: boolean
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {
|
||||
type ChatBskyActorDefs,
|
||||
moderateProfile,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {moderateProfile, type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -25,6 +21,7 @@ import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {type chat} from '#/lexicons'
|
||||
|
||||
export function RecentChats({
|
||||
postUri,
|
||||
@@ -116,7 +113,7 @@ function RecentChatItem({
|
||||
onPress: () => void
|
||||
moderationOpts: ModerationOpts
|
||||
convo: ConvoWithDetails
|
||||
primaryMember: ChatBskyActorDefs.ProfileViewBasic
|
||||
primaryMember: chat.bsky.actor.defs.ProfileViewBasic
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {memo, useMemo} from 'react'
|
||||
import * as ExpoClipboard from 'expo-clipboard'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import {type PressableProps, type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {type RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
||||
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
export interface ShareMenuItemsProps {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
post: Shadow<app.bsky.feed.defs.PostView>
|
||||
record: app.bsky.feed.post.Main
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
threadgateRecord?: app.bsky.feed.threadgate.Main
|
||||
onShare: () => void
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {memo, useMemo} from 'react'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
import {type Insets} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {type RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -21,6 +16,7 @@ import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {type app} from '#/lexicons'
|
||||
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
|
||||
import {ShareMenuItems} from './ShareMenuItems'
|
||||
|
||||
@@ -37,12 +33,12 @@ let ShareMenuButton = ({
|
||||
logContext,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
post: Shadow<app.bsky.feed.defs.PostView>
|
||||
big?: boolean
|
||||
record: AppBskyFeedPost.Record
|
||||
record: app.bsky.feed.post.Main
|
||||
richText: RichTextAPI
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
threadgateRecord?: app.bsky.feed.threadgate.Main
|
||||
onShare: () => void
|
||||
hitSlop?: Insets
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {type RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
@@ -29,6 +24,7 @@ import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
import * as Skele from '#/components/Skeleton'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {type app} from '#/lexicons'
|
||||
import {BookmarkButton} from './BookmarkButton'
|
||||
import {
|
||||
PostControlButton,
|
||||
@@ -57,8 +53,8 @@ let PostControls = ({
|
||||
forceGoogleTranslate = false,
|
||||
}: {
|
||||
big?: boolean
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
post: Shadow<app.bsky.feed.defs.PostView>
|
||||
record: app.bsky.feed.post.Main
|
||||
richText: RichTextAPI
|
||||
feedContext?: string | undefined
|
||||
reqId?: string | undefined
|
||||
@@ -66,8 +62,8 @@ let PostControls = ({
|
||||
onPressReply: () => void
|
||||
onPostReply?: (postUri: string | undefined) => void
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
threadgateRecord?: app.bsky.feed.threadgate.Main
|
||||
onShowLess?: (interaction: app.bsky.feed.defs.Interaction) => void
|
||||
viaRepost?: {uri: string; cid: string}
|
||||
variant?: 'compact' | 'normal' | 'large'
|
||||
forceGoogleTranslate?: boolean
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
Pressable as NativePressable,
|
||||
type PressableStateCallbackType as NativePressableStateCallbackType,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
|
||||
export interface PressableStateCallbackType extends NativePressableStateCallbackType {
|
||||
/** Provided by react-native-web. */
|
||||
readonly focused?: boolean
|
||||
/** Provided by react-native-web. */
|
||||
readonly hovered?: boolean
|
||||
}
|
||||
|
||||
export type PressableProps = Omit<
|
||||
React.ComponentProps<typeof NativePressable>,
|
||||
'children' | 'style'
|
||||
> & {
|
||||
children?:
|
||||
React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode)
|
||||
style?:
|
||||
| StyleProp<ViewStyle>
|
||||
| ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)
|
||||
}
|
||||
|
||||
/**
|
||||
* React Native Pressable with react-native-web's callback state represented in
|
||||
* its types. The web-only fields are optional because native does not provide
|
||||
* them at runtime.
|
||||
*/
|
||||
export function Pressable({children, style, ...props}: PressableProps) {
|
||||
return (
|
||||
<NativePressable {...props} style={style}>
|
||||
{children}
|
||||
</NativePressable>
|
||||
)
|
||||
}
|
||||
@@ -6,11 +6,8 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
moderateProfile,
|
||||
type ModerationOpts,
|
||||
RichText as RichTextApi,
|
||||
} from '@atproto/api'
|
||||
import {moderateProfile, type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
import {RichText as RichTextApi} from '@bsky/sdk/richtext'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {getModerationCauseKey} from '#/lib/moderation'
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {memo, useCallback, useEffect, useMemo, useReducer, useRef} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {moderateProfile, type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
|
||||
import {msg, plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -42,6 +38,7 @@ import {Text} from '#/components/Typography'
|
||||
import {IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||
import {useActorStatus} from '#/features/liveNow'
|
||||
import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog'
|
||||
import {type app} from '#/lexicons'
|
||||
import {type ProfileHoverCardProps} from './types'
|
||||
|
||||
const floatingMiddlewares = [
|
||||
@@ -321,11 +318,10 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||
|
||||
return (
|
||||
<View
|
||||
// @ts-ignore View is being used as div
|
||||
ref={refs.setReference}
|
||||
onPointerMove={onPointerMoveTarget}
|
||||
onPointerLeave={onPointerLeaveTarget}
|
||||
// @ts-ignore web only prop
|
||||
// @ts-expect-error web only prop
|
||||
onMouseUp={onPress}
|
||||
style={[a.flex_shrink, props.inline && a.inline]}>
|
||||
{props.children}
|
||||
@@ -418,7 +414,7 @@ function Inner({
|
||||
moderationOpts,
|
||||
hide,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
profile: app.bsky.actor.defs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
hide: () => void
|
||||
}) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {TextInput, View, type ViewToken} from 'react-native'
|
||||
import {type ModerationOpts} from '@atproto/api'
|
||||
import {type ListViewToken as ViewToken, TextInput, View} from 'react-native'
|
||||
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
@@ -139,7 +139,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
const [searchText, setSearchText] = useState(lastSearchText)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
@@ -374,7 +374,7 @@ let Header = ({
|
||||
interestsDisplayNames,
|
||||
}: {
|
||||
guide?: Follow10ProgressGuide
|
||||
inputRef: React.RefObject<TextInput | null>
|
||||
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||
listRef: React.RefObject<ListMethods | null>
|
||||
onSelectTab: (v: string) => void
|
||||
searchText: string
|
||||
@@ -679,7 +679,7 @@ function SearchInput({
|
||||
}: {
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<TextInput | null>
|
||||
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||
defaultValue: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -62,7 +62,7 @@ export const ProgressGuideToast = forwardRef<
|
||||
|
||||
// animate the opacity then set isOpen to false when done
|
||||
const setIsntOpen = () => setIsOpen(false)
|
||||
opacity.set(() =>
|
||||
opacity.set(
|
||||
withTiming(
|
||||
0,
|
||||
{
|
||||
@@ -81,7 +81,7 @@ export const ProgressGuideToast = forwardRef<
|
||||
// animate the vertical translation, the opacity, and the checkmark
|
||||
const playCheckmark = () => animatedCheckRef.current?.play()
|
||||
opacity.set(0)
|
||||
opacity.set(() =>
|
||||
opacity.set(
|
||||
withTiming(
|
||||
1,
|
||||
{
|
||||
@@ -92,7 +92,7 @@ export const ProgressGuideToast = forwardRef<
|
||||
),
|
||||
)
|
||||
translateY.set(0)
|
||||
translateY.set(() =>
|
||||
translateY.set(
|
||||
withTiming(insets.top + 10, {
|
||||
duration: 500,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {useMemo} from 'react'
|
||||
import {type StyleProp, type TextStyle} from 'react-native'
|
||||
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
||||
import {RichText as RichTextAPI} from '@bsky/sdk/richtext'
|
||||
|
||||
import {toShortUrl} from '#/lib/strings/url-helpers'
|
||||
import {android, atoms as a, flatten, type TextStyleProp} from '#/alf'
|
||||
@@ -9,6 +9,8 @@ import {InlineLinkText, type LinkProps} from '#/components/Link'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {RichTextTag} from '#/components/RichTextTag'
|
||||
import {Text, type TextProps} from '#/components/Typography'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const WORD_WRAP = {wordWrap: 1}
|
||||
// lifted from facet detection in `RichText` impl, _without_ `gm` flags
|
||||
@@ -91,7 +93,7 @@ export function RichText({
|
||||
|
||||
if (!facets?.length) {
|
||||
if (isOnlyEmoji(text)) {
|
||||
const flattenedStyle = flatten(style) ?? {}
|
||||
const flattenedStyle = flatten(style)
|
||||
const fontSize =
|
||||
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
||||
return (
|
||||
@@ -102,7 +104,6 @@ export function RichText({
|
||||
style={[plainStyles, {fontSize}, suffixStyles]}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{text}
|
||||
{suffix ? ' ' : null}
|
||||
@@ -119,7 +120,6 @@ export function RichText({
|
||||
numberOfLines={numberOfLines}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{text}
|
||||
{suffix ? ' ' : null}
|
||||
@@ -139,7 +139,7 @@ export function RichText({
|
||||
if (
|
||||
mention &&
|
||||
(disableMentionFacetValidation ||
|
||||
AppBskyRichtextFacet.validateMention(mention).success) &&
|
||||
bsky.matches(app.bsky.richtext.facet.mention, mention)) &&
|
||||
!disableLinks
|
||||
) {
|
||||
els.push(
|
||||
@@ -148,7 +148,7 @@ export function RichText({
|
||||
selectable={selectable}
|
||||
to={`/profile/${mention.did}`}
|
||||
style={interactiveStyles}
|
||||
// @ts-ignore TODO
|
||||
// @ts-expect-error TODO
|
||||
dataSet={WORD_WRAP}
|
||||
shouldProxy={shouldProxyLinks}
|
||||
onPress={onLinkPress}>
|
||||
@@ -156,7 +156,7 @@ export function RichText({
|
||||
</InlineLinkText>
|
||||
</ProfileHoverCard>,
|
||||
)
|
||||
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
||||
} else if (link && bsky.matches(app.bsky.richtext.facet.link, link)) {
|
||||
const isValidLink = URL_REGEX.test(link.uri)
|
||||
if (!isValidLink || disableLinks) {
|
||||
els.push(toShortUrl(segment.text))
|
||||
@@ -167,7 +167,7 @@ export function RichText({
|
||||
key={key}
|
||||
to={link.uri}
|
||||
style={interactiveStyles}
|
||||
// @ts-ignore TODO
|
||||
// @ts-expect-error TODO
|
||||
dataSet={WORD_WRAP}
|
||||
shareOnLongPress
|
||||
shouldProxy={shouldProxyLinks}
|
||||
@@ -181,7 +181,7 @@ export function RichText({
|
||||
!disableLinks &&
|
||||
enableTags &&
|
||||
tag &&
|
||||
AppBskyRichtextFacet.validateTag(tag).success
|
||||
bsky.matches(app.bsky.richtext.facet.tag, tag)
|
||||
) {
|
||||
els.push(
|
||||
<RichTextTag
|
||||
@@ -207,7 +207,6 @@ export function RichText({
|
||||
numberOfLines={numberOfLines}
|
||||
onLayout={onLayout}
|
||||
onTextLayout={onTextLayout}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={WORD_WRAP}>
|
||||
{els}
|
||||
{suffix ? ' ' : null}
|
||||
|
||||
@@ -100,7 +100,16 @@ export function Trigger({children, hitSlop, label}: TriggerProps) {
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
hitSlop={hitSlop}
|
||||
hitSlop={
|
||||
typeof hitSlop === 'number'
|
||||
? {
|
||||
top: hitSlop,
|
||||
right: hitSlop,
|
||||
bottom: hitSlop,
|
||||
left: hitSlop,
|
||||
}
|
||||
: (hitSlop ?? undefined)
|
||||
}
|
||||
label={label}
|
||||
onPress={control.open}
|
||||
style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {View} from 'react-native'
|
||||
import {Select as RadixSelect} from 'radix-ui'
|
||||
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {atoms as a, flatten, useTheme, web} from '#/alf'
|
||||
import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {
|
||||
@@ -94,7 +94,7 @@ export function Trigger({children, label}: TriggerProps) {
|
||||
onBlur={onBlur}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
style={flatten([
|
||||
style={flattenToCSS([
|
||||
a.flex,
|
||||
a.relative,
|
||||
t.atoms.bg_contrast_50,
|
||||
@@ -196,7 +196,7 @@ export function Content<T>({
|
||||
return (
|
||||
<RadixSelect.Portal>
|
||||
<RadixSelect.Content
|
||||
style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
|
||||
style={flattenToCSS([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
|
||||
position="popper"
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
@@ -212,17 +212,17 @@ export function Content<T>({
|
||||
a.overflow_hidden,
|
||||
!reduceMotionEnabled && a.zoom_fade_in,
|
||||
]}>
|
||||
<RadixSelect.ScrollUpButton style={flatten(up)}>
|
||||
<RadixSelect.ScrollUpButton style={flattenToCSS(up)}>
|
||||
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
||||
</RadixSelect.ScrollUpButton>
|
||||
<RadixSelect.Viewport style={flatten([a.p_xs])}>
|
||||
<RadixSelect.Viewport style={flattenToCSS([a.p_xs])}>
|
||||
{items.map((item, index) => (
|
||||
<Fragment key={valueExtractor(item)}>
|
||||
{renderItem(item, index, selectedValue)}
|
||||
</Fragment>
|
||||
))}
|
||||
</RadixSelect.Viewport>
|
||||
<RadixSelect.ScrollDownButton style={flatten(down)}>
|
||||
<RadixSelect.ScrollDownButton style={flattenToCSS(down)}>
|
||||
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
|
||||
</RadixSelect.ScrollDownButton>
|
||||
</View>
|
||||
@@ -273,7 +273,7 @@ export function Item({ref, value, style, children}: ItemProps) {
|
||||
onMouseLeave={onMouseLeave}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={flatten([
|
||||
style={flattenToCSS([
|
||||
t.atoms.text,
|
||||
a.relative,
|
||||
a.flex,
|
||||
@@ -307,7 +307,7 @@ export const ItemText = function ItemText({children, style}: ItemTextProps) {
|
||||
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
|
||||
return (
|
||||
<RadixSelect.ItemIndicator
|
||||
style={flatten([
|
||||
style={flattenToCSS([
|
||||
a.absolute,
|
||||
{left: 0, width: 30},
|
||||
a.flex,
|
||||
@@ -324,7 +324,7 @@ export function Separator() {
|
||||
|
||||
return (
|
||||
<RadixSelect.Separator
|
||||
style={flatten([
|
||||
style={flattenToCSS([
|
||||
{
|
||||
height: 1,
|
||||
backgroundColor: t.atoms.border_contrast_low.borderColor,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
|
||||
import {type ListRenderItemInfo, View} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
|
||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||
import {List, type ListRef} from '#/view/com/util/List'
|
||||
@@ -8,13 +7,14 @@ import {type SectionRef} from '#/screens/Profile/Sections/types'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
function keyExtractor(item: AppBskyFeedDefs.GeneratorView) {
|
||||
function keyExtractor(item: app.bsky.feed.defs.GeneratorView) {
|
||||
return item.uri
|
||||
}
|
||||
|
||||
interface ProfilesListProps {
|
||||
feeds: AppBskyFeedDefs.GeneratorView[]
|
||||
feeds: app.bsky.feed.defs.GeneratorView[]
|
||||
headerHeight: number
|
||||
scrollElRef: ListRef
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export const FeedsList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
const renderItem = ({
|
||||
item,
|
||||
index,
|
||||
}: ListRenderItemInfo<AppBskyFeedDefs.GeneratorView>) => {
|
||||
}: ListRenderItemInfo<app.bsky.feed.defs.GeneratorView>) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
|
||||
import {type ListRenderItemInfo, View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type AppBskyGraphGetList,
|
||||
AtUri,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
type UseInfiniteQueryResult,
|
||||
} from '@tanstack/react-query'
|
||||
import {AtUri} from '@atproto/syntax'
|
||||
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
||||
|
||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
@@ -22,16 +14,14 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {Default as ProfileCard} from '#/components/ProfileCard'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
function keyExtractor(item: AppBskyActorDefs.ProfileView, index: number) {
|
||||
function keyExtractor(item: app.bsky.actor.defs.ProfileView, index: number) {
|
||||
return `${item.did}-${index}`
|
||||
}
|
||||
|
||||
interface ProfilesListProps {
|
||||
listUri: string
|
||||
listMembersQuery: UseInfiniteQueryResult<
|
||||
InfiniteData<AppBskyGraphGetList.OutputSchema>
|
||||
>
|
||||
moderationOpts: ModerationOpts
|
||||
headerHeight: number
|
||||
scrollElRef: ListRef
|
||||
@@ -87,7 +77,7 @@ export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
const renderItem = ({
|
||||
item,
|
||||
index,
|
||||
}: ListRenderItemInfo<AppBskyActorDefs.ProfileView>) => {
|
||||
}: ListRenderItemInfo<app.bsky.actor.defs.ProfileView>) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -37,6 +36,7 @@ import * as Prompt from '#/components/Prompt'
|
||||
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_IOS} from '#/env'
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
@@ -57,7 +57,7 @@ interface ProfileFeedgensProps {
|
||||
emptyStateIcon?: React.ComponentType<any> | React.ReactElement
|
||||
}
|
||||
|
||||
function keyExtractor(item: AppBskyGraphDefs.StarterPackViewBasic) {
|
||||
function keyExtractor(item: app.bsky.graph.defs.StarterPackViewBasic) {
|
||||
return item.uri
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export function ProfileStarterPacks({
|
||||
({
|
||||
item,
|
||||
index,
|
||||
}: ListRenderItemInfo<AppBskyGraphDefs.StarterPackViewBasic>) => {
|
||||
}: ListRenderItemInfo<app.bsky.graph.defs.StarterPackViewBasic>) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -2,8 +2,7 @@ import {lazy, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
// @ts-expect-error missing types
|
||||
import QRCode from 'react-native-qrcode-styled'
|
||||
import type ViewShot from 'react-native-view-shot'
|
||||
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {type ViewShotRef} from 'react-native-view-shot'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
@@ -12,30 +11,23 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const LazyViewShot = lazy(
|
||||
// @ts-expect-error dynamic import
|
||||
() => import('react-native-view-shot/src/index'),
|
||||
)
|
||||
const LazyViewShot = lazy(() => import('react-native-view-shot'))
|
||||
|
||||
export function QrCode({
|
||||
starterPack,
|
||||
link,
|
||||
ref,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackView
|
||||
starterPack: app.bsky.graph.defs.StarterPackView
|
||||
link: string
|
||||
ref: React.Ref<ViewShot>
|
||||
ref: React.Ref<ViewShotRef>
|
||||
}) {
|
||||
const {record} = starterPack
|
||||
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
) {
|
||||
if (!bsky.isType(app.bsky.graph.starterpack, record)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {Suspense, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import type ViewShot from 'react-native-view-shot'
|
||||
import {requestPermissionsAsync, saveToLibraryAsync} from 'expo-media-library'
|
||||
import {type ViewShotRef} from 'react-native-view-shot'
|
||||
import {
|
||||
requestPermissionsAsync,
|
||||
saveToLibraryAsync,
|
||||
} from 'expo-media-library/legacy'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -21,6 +23,7 @@ import {QrCode} from '#/components/StarterPack/QrCode'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {app} from '#/lexicons'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function QrCodeDialog({
|
||||
@@ -28,7 +31,7 @@ export function QrCodeDialog({
|
||||
link,
|
||||
control,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackView
|
||||
starterPack: app.bsky.graph.defs.StarterPackView
|
||||
link?: string
|
||||
control: DialogControlProps
|
||||
}) {
|
||||
@@ -38,7 +41,7 @@ export function QrCodeDialog({
|
||||
const [isSaveProcessing, setIsSaveProcessing] = useState(false)
|
||||
const [isCopyProcessing, setIsCopyProcessing] = useState(false)
|
||||
|
||||
const ref = useRef<ViewShot>(null)
|
||||
const ref = useRef<ViewShotRef>(null)
|
||||
|
||||
const getCanvas = (base64: string): Promise<HTMLCanvasElement> => {
|
||||
return new Promise(resolve => {
|
||||
@@ -87,12 +90,7 @@ export function QrCodeDialog({
|
||||
} else {
|
||||
setIsSaveProcessing(true)
|
||||
|
||||
if (
|
||||
!bsky.validate(
|
||||
starterPack.record,
|
||||
AppBskyGraphStarterpack.validateRecord,
|
||||
)
|
||||
) {
|
||||
if (!bsky.matches(app.bsky.graph.starterpack, starterPack.record)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user