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:
Eric Bailey
2026-08-25 16:58:07 -05:00
1169 changed files with 159530 additions and 128659 deletions
+9 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 (
+6 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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,
},
}),
],
},
],
+4 -6
View File
@@ -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 -1
View File
@@ -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'
+19 -13
View File
@@ -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'
+38 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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 =
+8 -2
View File
@@ -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
+7 -4
View File
@@ -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
View File
@@ -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,
+1 -4
View File
@@ -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
+2 -1
View File
@@ -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',
+25 -14
View File
@@ -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': {}
+1 -1
View File
@@ -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
}
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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,
+1 -4
View File
@@ -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,
+2 -2
View File
@@ -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 -1
View File
@@ -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'
+2 -1
View File
@@ -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'
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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}
+13 -6
View File
@@ -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
}}
+5 -2
View File
@@ -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) =>
+76 -71
View File
@@ -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,
+3 -2
View File
@@ -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} />
}
+7 -10
View File
@@ -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
+4 -4
View File
@@ -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
+11 -8
View File
@@ -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(() => {
+1 -1
View File
@@ -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>
}
+1 -1
View File
@@ -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)
+7 -8
View File
@@ -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
+3 -3
View File
@@ -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})
+1 -1
View File
@@ -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()
-3
View File
@@ -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: {
+1 -1
View File
@@ -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}
+16 -19
View File
@@ -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,
})
})
}),
)
}
})
+9 -3
View File
@@ -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
}
+2 -2
View File
@@ -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
+6 -9
View File
@@ -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
+1 -1
View File
@@ -21,7 +21,7 @@ export function Loader(props: Props) {
}))
useEffect(() => {
rotation.set(() =>
rotation.set(
withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
)
}, [rotation])
+16 -10
View File
@@ -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()
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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.
+4 -3
View File
@@ -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
}) {
+3 -2
View File
@@ -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 -1
View File
@@ -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'
+11 -8
View File
@@ -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 -1
View File
@@ -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
}) {
+12 -17
View File
@@ -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})
+5 -3
View File
@@ -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']
}
+5 -8
View File
@@ -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 => {
+6 -10
View File
@@ -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'
+6 -10
View File
@@ -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
+37
View File
@@ -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>
)
}
+2 -5
View File
@@ -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()
+3 -3
View File
@@ -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),
+9 -10
View File
@@ -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}
+10 -1
View File
@@ -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]}
+9 -9
View File
@@ -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={[
+6 -14
View File
@@ -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
}
+9 -11
View File
@@ -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