,
) {
- 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
diff --git a/src/alf/util/dimensions.ts b/src/alf/util/dimensions.ts
index 31af2ffa99..e5d60055c1 100644
--- a/src/alf/util/dimensions.ts
+++ b/src/alf/util/dimensions.ts
@@ -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()
}, [])
diff --git a/src/alf/util/flatten.ts b/src/alf/util/flatten.ts
index 6d49ce6e51..b218c8879e 100644
--- a/src/alf/util/flatten.ts
+++ b/src/alf/util/flatten.ts
@@ -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
(
+ style?: StyleProp,
+): T extends (infer U)[] ? U : T {
+ return (StyleSheet.flatten(
+ style as unknown as Parameters[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[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,
diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts
index 7cb9b12723..dfcc57b53a 100644
--- a/src/alf/util/useColorModeTheme.ts
+++ b/src/alf/util/useColorModeTheme.ts
@@ -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
diff --git a/src/analytics/utils.ts b/src/analytics/utils.ts
index 95d2efb589..b20b3719c9 100644
--- a/src/analytics/utils.ts
+++ b/src/analytics/utils.ts
@@ -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
}
diff --git a/src/components/AltBadgeWithDialog.tsx b/src/components/AltBadgeWithDialog.tsx
index 07cf9544f0..4ae078e12b 100644
--- a/src/components/AltBadgeWithDialog.tsx
+++ b/src/components/AltBadgeWithDialog.tsx
@@ -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,
diff --git a/src/components/BetaBadge.tsx b/src/components/BetaBadge.tsx
index 115f8d0046..c8c0ddd2bf 100644
--- a/src/components/BetaBadge.tsx
+++ b/src/components/BetaBadge.tsx
@@ -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'
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 54332fbc07..2c1f7ff3f8 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -101,6 +101,8 @@ export type ButtonProps = Pick<
| 'onPressOut'
| 'onFocus'
| 'onBlur'
+ | 'onAccessibilityAction'
+ | 'onAccessibilityEscape'
> &
AccessibilityProps &
VariantProps & {
@@ -131,7 +133,7 @@ export function useButtonContext() {
return useContext(Context)
}
-export const Button = forwardRef(
+export const Button = forwardRef, ButtonProps>(
(
{
children,
@@ -575,7 +577,6 @@ export const Button = forwardRef(
role="button"
accessibilityHint={undefined} // optional
{...rest}
- // @ts-ignore - this will always be a pressable
ref={ref}
aria-label={label}
aria-pressed={state.pressed}
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index f4a0302ff7..b2266db5a1 100644
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -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
+type ViewInstance = React.ComponentRef
+
export type SubmitRequest =
| {
platform: 'web'
@@ -60,7 +63,7 @@ export type ComposerInternalApi = {
input?: ReturnType['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
+ ref?: React.RefObject
internalApiRef?: React.Ref
outerStyle?: ViewStyleProp['style']
contentTextStyle?: TextStyleProp['style']
@@ -139,6 +142,11 @@ export function Composer({
placement: autocompletePlacement,
dynamicWidth: IS_WEB,
})
+ const inputRef = mergeRefs([
+ ref,
+ tapper.inputProps.ref as React.Ref,
+ sift.targetProps.ref as React.Ref,
+ ])
/*
* 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
}}
diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx
index d09e0e60ec..95492a385a 100644
--- a/src/components/ContextMenu/index.tsx
+++ b/src/components/ContextMenu/index.tsx
@@ -244,7 +244,7 @@ export function Trigger({
const context = useContextMenuContext()
const playHaptic = useHaptics()
const insets = useSafeAreaInsets()
- const ref = useRef(null)
+ const ref = useRef>(null)
const isFocused = useIsFocused()
const [image, setImage] = useState(null)
const [pendingMeasurement, setPendingMeasurement] = useState<{
@@ -971,7 +971,10 @@ export function Divider() {
)
}
-function measureView(view: View | null, insets: EdgeInsets) {
+function measureView(
+ view: React.ComponentRef | null,
+ insets: EdgeInsets,
+) {
if (!view) return Promise.resolve(null)
return new Promise(resolve => {
view?.measureInWindow((x, y, width, height) =>
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index 4f6984a4a0..5cfa2d1c10 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -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
}
-export const ScrollableInner = forwardRef(
- 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>
+}) {
+ 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(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) => {
- 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) => {
+ if (!IS_ANDROID) {
+ return
}
+ const {contentOffset} = e.nativeEvent
+ if (contentOffset.y > 0 && !disableDrag) {
+ setDisableDrag(true)
+ } else if (contentOffset.y <= 1 && disableDrag) {
+ setDisableDrag(false)
+ }
+ }
- return (
- <>
-
- {header}
- {children}
-
- {footer}
- >
- )
- },
-)
+ return (
+ <>
+
+ {header}
+ {children}
+
+ {footer}
+ >
+ )
+}
export const InnerFlatList = forwardRef<
ListMethods,
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index c26a71825b..e867acd60a 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -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}) {
+}: DialogInnerProps & {
+ ref?: React.Ref>
+}) {
return
}
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index aa77a7b36b..59bbe85752 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -217,7 +217,7 @@ export function ProfileGrid({
// Track seen profiles
const seenProfilesRef = useRef>(new Set())
- const containerRef = useRef(null)
+ const containerRef = useRef>(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 {
diff --git a/src/components/FocusScope/index.tsx b/src/components/FocusScope/index.tsx
index 48e013500e..598c3cd6ca 100644
--- a/src/components/FocusScope/index.tsx
+++ b/src/components/FocusScope/index.tsx
@@ -41,7 +41,7 @@ export function FocusScope({children}: {children: React.ReactNode}) {
*/
function FocusTrap({children}: {children: React.ReactNode}) {
const {_} = useLingui()
- const child = useRef(null)
+ const child = useRef>(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 | null) => {
+ if (!ref) return
+ const node = findNodeHandle(ref)
+ if (node) {
+ AccessibilityInfo.setAccessibilityFocus(node)
+ }
+ },
+ [],
+ )
useEffect(() => {
setTimeout(() => {
diff --git a/src/components/GlassView.tsx b/src/components/GlassView.tsx
index cac5bd268d..ddbcc07d80 100644
--- a/src/components/GlassView.tsx
+++ b/src/components/GlassView.tsx
@@ -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 & {
fallbackStyle?: StyleProp
}
diff --git a/src/components/InterestTabs.tsx b/src/components/InterestTabs.tsx
index 5df1fb62e3..5a99a36992 100644
--- a/src/components/InterestTabs.tsx
+++ b/src/components/InterestTabs.tsx
@@ -47,7 +47,7 @@ export function InterestTabs({
}) {
const t = useTheme()
const {_} = useLingui()
- const listRef = useRef(null)
+ const listRef = useRef>(null)
const [totalWidth, setTotalWidth] = useState(0)
const [scrollX, setScrollX] = useState(0)
const [contentWidth, setContentWidth] = useState(0)
diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx
index a0bd45c8fc..c70cebd703 100644
--- a/src/components/Layout/Header/index.tsx
+++ b/src/components/Layout/Header/index.tsx
@@ -42,7 +42,7 @@ export function Outer({
}: {
children: React.ReactNode
noBottomBorder?: boolean
- headerRef?: React.RefObject
+ headerRef?: React.RefObject | null>
sticky?: boolean
}) {
const t = useTheme()
diff --git a/src/components/Lightbox/Lightbox.web.tsx b/src/components/Lightbox/Lightbox.web.tsx
index 0222aa3156..cccbe4f851 100644
--- a/src/components/Lightbox/Lightbox.web.tsx
+++ b/src/components/Lightbox/Lightbox.web.tsx
@@ -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: {
diff --git a/src/components/Lightbox/chrome/ImageMenu.tsx b/src/components/Lightbox/chrome/ImageMenu.tsx
index 9088b9ded9..14ce100c0f 100644
--- a/src/components/Lightbox/chrome/ImageMenu.tsx
+++ b/src/components/Lightbox/chrome/ImageMenu.tsx
@@ -35,7 +35,7 @@ const TIMING_OUT = {duration: 150}
export function ImageMenu({onPressShare, onPressSave}: Props) {
const {t: l} = useLingui()
- const triggerRef = useRef(null)
+ const triggerRef = useRef>(null)
const [isMounted, setIsMounted] = useState(false)
const [anchor, setAnchor] = useState(null)
const progress = useSharedValue(0)
diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx
index 9cf088f864..c4a842a74e 100644
--- a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx
+++ b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx
@@ -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 (
{
+ onPageSelected={(e: PagerViewOnPageSelectedEvent) => {
const next = e.nativeEvent.position
setImageIndex(prev => {
if (metricsContext && prev !== next) {
@@ -401,7 +404,7 @@ function ImageView({
})
setIsScaled(false)
}}
- onPageScrollStateChanged={e => {
+ onPageScrollStateChanged={(e: PageScrollStateChangedNativeEvent) => {
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
}}
overdrag={true}
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 6d60edb615..8e5891f3b2 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -478,7 +478,7 @@ export function InlineLinkText({
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
- const flattenedStyle = flatten(style) || {}
+ const flattenedStyle = flatten(style)
return (
void
+ onKeyDown: PressableProps['onKeyDown']
/**
* Radix provides this, but we override on web to use `onPress` instead,
* which is less sensitive while scrolling.
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
index d20cc0d90a..947d4ac3d6 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
@@ -149,7 +149,7 @@ function canPlayBskyVideoCodecs(): boolean {
type CachedPromise = Promise & {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
diff --git a/src/components/Post/Translated/index.tsx b/src/components/Post/Translated/index.tsx
index 849222df03..6d44e9ce75 100644
--- a/src/components/Post/Translated/index.tsx
+++ b/src/components/Post/Translated/index.tsx
@@ -267,7 +267,7 @@ function TranslationResult({
? codeToLanguageName(resultSourceLanguage, i18n.locale)
: undefined
- const flattenedStyle = flatten(postTextStyle) ?? {}
+ const flattenedStyle = flatten(postTextStyle)
const fontSize = flattenedStyle.fontSize
return (
diff --git a/src/components/PostControls/PostControlButton.tsx b/src/components/PostControls/PostControlButton.tsx
index 3ea85e2811..e65a931f7d 100644
--- a/src/components/PostControls/PostControlButton.tsx
+++ b/src/components/PostControls/PostControlButton.tsx
@@ -27,7 +27,7 @@ export function PostControlButton({
activeColor,
...props
}: Omit & {
- ref?: React.Ref
+ ref?: React.Ref>
active?: boolean
big?: boolean
color?: string
diff --git a/src/components/Pressable.tsx b/src/components/Pressable.tsx
new file mode 100644
index 0000000000..5fc96e3047
--- /dev/null
+++ b/src/components/Pressable.tsx
@@ -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,
+ 'children' | 'style'
+> & {
+ children?:
+ React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode)
+ style?:
+ | StyleProp
+ | ((state: PressableStateCallbackType) => StyleProp)
+}
+
+/**
+ * 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 (
+
+ {children}
+
+ )
+}
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index ccc48b8951..a5f47be28b 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -318,11 +318,10 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
return (
{props.children}
diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx
index d7fa9c1fab..1ab2668ae5 100644
--- a/src/components/ProgressGuide/FollowDialog.tsx
+++ b/src/components/ProgressGuide/FollowDialog.tsx
@@ -1,5 +1,5 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
-import {TextInput, View, type ViewToken} from 'react-native'
+import {type ListViewToken as ViewToken, TextInput, View} from 'react-native'
import {type ModerationOpts} from '@bsky/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
@@ -139,7 +139,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
const [searchText, setSearchText] = useState(lastSearchText)
const moderationOpts = useModerationOpts()
const listRef = useRef(null)
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const [headerHeight, setHeaderHeight] = useState(0)
const {currentAccount} = useSession()
@@ -374,7 +374,7 @@ let Header = ({
interestsDisplayNames,
}: {
guide?: Follow10ProgressGuide
- inputRef: React.RefObject
+ inputRef: React.RefObject | null>
listRef: React.RefObject
onSelectTab: (v: string) => void
searchText: string
@@ -679,7 +679,7 @@ function SearchInput({
}: {
onChangeText: (text: string) => void
onEscape: () => void
- inputRef: React.RefObject
+ inputRef: React.RefObject | null>
defaultValue: string
}) {
const t = useTheme()
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 0266dae0ce..1fa020f2a9 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -93,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 (
@@ -104,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}
@@ -121,7 +120,6 @@ export function RichText({
numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
- // @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{text}
{suffix ? ' ' : null}
@@ -150,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}>
@@ -169,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}
@@ -209,7 +207,6 @@ export function RichText({
numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
- // @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{els}
{suffix ? ' ' : null}
diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx
index a7dd70d8e7..af5849346b 100644
--- a/src/components/Select/index.tsx
+++ b/src/components/Select/index.tsx
@@ -100,7 +100,16 @@ export function Trigger({children, hitSlop, label}: TriggerProps) {
} else {
return (
@@ -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 (
- >
moderationOpts: ModerationOpts
headerHeight: number
scrollElRef: ListRef
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
index 4f89950527..350bf8b286 100644
--- a/src/components/Tooltip/index.tsx
+++ b/src/components/Tooltip/index.tsx
@@ -27,8 +27,9 @@ import {
import {Text} from '#/components/Typography'
const TooltipPortal = createPortalGroup()
-const TooltipProviderContext =
- createContext | null>(null)
+const TooltipProviderContext = createContext | null> | null>(null)
/**
* Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container,
@@ -37,7 +38,7 @@ const TooltipProviderContext =
* Only really necessary on iOS but can work on Android.
*/
export function SheetCompatProvider({children}: {children: React.ReactNode}) {
- const ref = useRef(null)
+ const ref = useRef | null>(null)
return (
@@ -154,7 +155,7 @@ export function Outer({
export function Target({children}: {children: React.ReactNode}) {
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
const [hasLaidOut, setHasLaidOut] = useState(false)
- const targetRef = useRef(null)
+ const targetRef = useRef>(null)
const containerRef = useContext(TooltipProviderContext)
const keyboardIsOpen = useIsKeyboardVisible()
diff --git a/src/components/Tooltip/index.web.tsx b/src/components/Tooltip/index.web.tsx
index fd017b43ca..8d6c9634ab 100644
--- a/src/components/Tooltip/index.web.tsx
+++ b/src/components/Tooltip/index.web.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {utils} from '@bsky.app/alf'
import {Popover} from 'radix-ui'
-import {atoms as a, flatten, useTheme} from '#/alf'
+import {atoms as a, flattenToCSS, useTheme} from '#/alf'
import {
ARROW_SIZE,
BUBBLE_MAX_WIDTH,
@@ -85,7 +85,7 @@ export function Content({
evt.preventDefault()
}
}}
- style={flatten([
+ style={flattenToCSS([
a.rounded_sm,
{
backgroundColor: style.surface,
diff --git a/src/components/contacts/components/OTPInput.tsx b/src/components/contacts/components/OTPInput.tsx
index 9834f6af9f..ef8944a575 100644
--- a/src/components/contacts/components/OTPInput.tsx
+++ b/src/components/contacts/components/OTPInput.tsx
@@ -25,13 +25,13 @@ export function OTPInput({
label: string
value: string
onChange: (text: string) => void
- ref?: React.Ref
+ ref?: React.Ref>
numberOfDigits?: number
onComplete?: (code: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
- const innerRef = useRef(null)
+ const innerRef = useRef>(null)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [selection, setSelection] = useState({start: 0, end: 0})
diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx
index 0d50bfdeeb..cba8f7092a 100644
--- a/src/components/contacts/screens/ViewMatches.tsx
+++ b/src/components/contacts/screens/ViewMatches.tsx
@@ -383,7 +383,7 @@ export function ViewMatches({
ref={listRef}
data={items}
renderItem={renderItem}
- ListFooterComponent={!isEmpty ? : null}
+ ListFooterComponent={!isEmpty ? : undefined}
keyExtractor={keyExtractor}
keyboardDismissMode="interactive"
automaticallyAdjustKeyboardInsets
diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx
index fdbdf0b7e3..3e405caef6 100644
--- a/src/components/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dialogs/SearchablePeopleList.tsx
@@ -97,7 +97,7 @@ export function SearchablePeopleList({
const [headerHeight, setHeaderHeight] = useState(0)
const listRef = useRef(null)
const {currentAccount} = useSession()
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const [searchText, setSearchText] = useState('')
@@ -634,7 +634,7 @@ function SearchInput({
value: string
onChangeText: (text: string) => void
onEscape: () => void
- inputRef: React.RefObject
+ inputRef: React.RefObject | null>
}) {
const t = useTheme()
const {t: l} = useLingui()
diff --git a/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx b/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
index de8f453d5a..2e171d9431 100644
--- a/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
+++ b/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
@@ -182,9 +182,9 @@ function ListsContent({
- ) : null
+ ) : undefined
}
- ListEmptyComponent={!isLoading && data ? : null}
+ ListEmptyComponent={!isLoading && data ? : undefined}
webInnerContentContainerStyle={[a.py_0]}
style={platform({
web: [a.px_2xl, a.pb_md],
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
index bbd40feb52..e8b6789120 100644
--- a/src/components/dialogs/nuxs/index.tsx
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -117,7 +117,7 @@ function Inner({
}, [activeNux, setActiveNux])
if (__DEV__ && typeof window !== 'undefined') {
- // @ts-ignore
+ // @ts-expect-error
window.clearNuxDialog = (id: Nux) => {
if (!__DEV__ || !id) return
resetNuxs([id])
diff --git a/src/components/dms/AddMembersFlow.tsx b/src/components/dms/AddMembersFlow.tsx
index bb591fb8c6..6a6b9fc49c 100644
--- a/src/components/dms/AddMembersFlow.tsx
+++ b/src/components/dms/AddMembersFlow.tsx
@@ -134,7 +134,7 @@ export function AddMembersFlow({
const [footerHeight, setFooterHeight] = useState(0)
const listRef = useRef(null)
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const [{groupChatDids, groupChatProfiles, searchText}, dispatch] = useReducer(
reducer,
@@ -502,7 +502,7 @@ export function AddMembersFlow({
- ) : null
+ ) : undefined
}
keyExtractor={(item: Item) => item.key}
style={[
diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx
index 4b86eb5f0e..a0a80149d7 100644
--- a/src/components/dms/InitiateChatFlow.tsx
+++ b/src/components/dms/InitiateChatFlow.tsx
@@ -249,7 +249,7 @@ export function InitiateChatFlow({
const listRef = useRef(null)
const {currentAccount} = useSession()
const aa = useAgeAssurance()
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const accountTooNewPromptControl = Dialog.useDialogControl()
const {data: convos} = useListConvosQuery({
diff --git a/src/components/dms/ReactionsDialog.tsx b/src/components/dms/ReactionsDialog.tsx
index 5a9f83c6e9..adaad953bb 100644
--- a/src/components/dms/ReactionsDialog.tsx
+++ b/src/components/dms/ReactionsDialog.tsx
@@ -255,7 +255,7 @@ function ReactionTabs({
const t = useTheme()
const {t: l} = useLingui()
- const scrollViewRef = useRef(null)
+ const scrollViewRef = useRef>(null)
const scrollState = useRef({x: 0, width: 0})
const tabLayouts = useRef
+ onChange={(selected: boolean) =>
+ void onToggleAdultContentEnabled(selected)
+ }>
{adultContentEnabled ? (
@@ -412,11 +467,11 @@ export function ModerationScreenInner({
Adult content can only be enabled via the Web at{' '}
{
evt.preventDefault()
- Linking.openURL('https://bsky.app/')
+ void Linking.openURL('https://bsky.app/')
return false
}}>
bsky.app
@@ -445,7 +500,6 @@ export function ModerationScreenInner({
)}
-
Advanced
-
{unavailableDids.length > 0 && (
@@ -470,7 +523,7 @@ export function ModerationScreenInner({
@@ -481,7 +534,6 @@ export function ModerationScreenInner({
)}
-
{isLabelersLoading ? (
diff --git a/src/screens/ModerationInbox/index.tsx b/src/screens/ModerationInbox/index.tsx
new file mode 100644
index 0000000000..a8e1fcfa5c
--- /dev/null
+++ b/src/screens/ModerationInbox/index.tsx
@@ -0,0 +1,28 @@
+import {Trans} from '@lingui/react/macro'
+
+import {NotFoundScreen} from '#/view/screens/NotFound'
+import * as Layout from '#/components/Layout'
+import {useAnalytics} from '#/analytics'
+
+export function ModerationInboxScreen() {
+ const ax = useAnalytics()
+ const isEnabled = ax.features.enabled(ax.features.ModerationInboxEnable)
+
+ if (!isEnabled) {
+ return
+ }
+
+ return (
+
+
+
+
+
+ Moderation inbox
+
+
+
+
+
+ )
+}
From a9c502c8cf5e395fe30ebb13f2790fd03a4b0823 Mon Sep 17 00:00:00 2001
From: Samuel Newman
Date: Wed, 26 Aug 2026 00:38:33 +0300
Subject: [PATCH 12/22] Remove multipart upload and 10-minute video feature
gates (#11528)
Co-authored-by: Claude
---
src/analytics/features/index.ts | 16 ---
src/analytics/features/types.ts | 2 -
src/analytics/metrics/types.ts | 7 +-
src/lib/constants.ts | 3 +-
src/lib/media/video/errors.ts | 7 --
src/lib/media/video/multipart/upload.ts | 37 ++----
src/lib/media/video/telemetry.ts | 10 --
src/lib/media/video/types.ts | 2 -
src/lib/media/video/upload.ts | 93 ++------------
src/lib/media/video/upload.web.ts | 129 --------------------
src/view/com/composer/Composer.tsx | 46 +------
src/view/com/composer/SelectMediaButton.tsx | 32 ++---
src/view/com/composer/state/video.ts | 12 +-
13 files changed, 40 insertions(+), 356 deletions(-)
delete mode 100644 src/lib/media/video/upload.web.ts
diff --git a/src/analytics/features/index.ts b/src/analytics/features/index.ts
index 7b1d9cbbaf..6cbafeaa22 100644
--- a/src/analytics/features/index.ts
+++ b/src/analytics/features/index.ts
@@ -77,22 +77,6 @@ export function getFeatures() {
export function getFeatureDescription(feature: Features, i18n: I18n) {
switch (feature) {
- case Features.VideoAllow10MinuteEnable:
- return {
- key: feature,
- name: i18n._(
- msg({
- message: 'Longer videos',
- comment: 'Name for a feature flag (longer videos)',
- }),
- ),
- description: i18n._(
- msg({
- message: 'Enable 10-minute video uploads.',
- comment: 'Description of a feature flag (10-minute video uploads)',
- }),
- ),
- }
case Features.CanonicalPostNumberingEnable:
return {
key: feature,
diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts
index c817bc5bf2..4d3cf5e197 100644
--- a/src/analytics/features/types.ts
+++ b/src/analytics/features/types.ts
@@ -18,8 +18,6 @@ export enum Features {
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
CustomLogoJapanEnable = 'custom_logo:japan:enable',
- VideoAllow10MinuteEnable = 'video:allow-10-minute:enable',
- VideoMultipartUploadEnable = 'video:multipart_upload:enable',
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
FollowSortEnable = 'follow_sort:enable',
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts
index 8e43589281..78a4574f0f 100644
--- a/src/analytics/metrics/types.ts
+++ b/src/analytics/metrics/types.ts
@@ -5,10 +5,7 @@
import {type Platform} from 'react-native'
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
-import {
- type VideoCompressSkipReason,
- type VideoUploadTransport,
-} from '#/lib/media/video/types'
+import {type VideoCompressSkipReason} from '#/lib/media/video/types'
import {type NotificationType} from '#/state/queries/notifications/types'
import {type FeedDescriptor} from '#/state/queries/post-feed'
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
@@ -1473,7 +1470,6 @@ export type Events = {
bytes: number
elapsedMs: number
throughputBytesPerSec: number
- transport: VideoUploadTransport
}
'video:upload:uploadFailed': {
uploadId: string
@@ -1481,7 +1477,6 @@ export type Events = {
bytes: number
errorClass: string
elapsedMs: number
- transport: VideoUploadTransport
}
'video:upload:processingStarted': {
uploadId: string
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 514312aeb9..d12c5a2aed 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -194,8 +194,7 @@ export const MAX_LABELERS = 20
export const VIDEO_SERVICE = 'https://video.bsky.app'
export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app'
-export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds
-export const VIDEO_10_MINUTE_MAX_DURATION_MS = 10 * 60 * 1000
+export const VIDEO_MAX_DURATION_MS = 10 * 60 * 1000 // 10 minutes in milliseconds
/**
* Maximum size of a video in megabytes, _not_ mebibytes. Backend uses
* ISO megabytes.
diff --git a/src/lib/media/video/errors.ts b/src/lib/media/video/errors.ts
index cbf3083d1f..cdeeb4095e 100644
--- a/src/lib/media/video/errors.ts
+++ b/src/lib/media/video/errors.ts
@@ -5,13 +5,6 @@ export class VideoTooLargeError extends Error {
}
}
-export class ServerError extends Error {
- constructor(message: string) {
- super(message)
- this.name = 'ServerError'
- }
-}
-
export class UploadLimitError extends Error {
constructor(message: string) {
super(message)
diff --git a/src/lib/media/video/multipart/upload.ts b/src/lib/media/video/multipart/upload.ts
index 3b0048ecd2..db9cc50218 100644
--- a/src/lib/media/video/multipart/upload.ts
+++ b/src/lib/media/video/multipart/upload.ts
@@ -26,15 +26,12 @@ import {createUploadPart} from './uploadPart'
import {uploadParts} from './uploadParts'
import {delay, isRetryableMultipartError, retryDelayMs} from './utils'
-export class MultipartFallbackError extends Error {}
-
export async function uploadVideoMultipart({
video,
client,
dispatchUrl,
setProgress,
signal,
- onStarted,
}: {
video: CompressedVideo
client: Client
@@ -42,25 +39,12 @@ export async function uploadVideoMultipart({
dispatchUrl: string | URL
setProgress: (progress: number) => void
signal: AbortSignal
- onStarted?: () => void
}): Promise {
throwIfAborted(signal)
const tokenProvider = createTokenProvider(client, dispatchUrl, signal)
const token = await tokenProvider.get()
const name = `${nanoid(12)}.${mimeToExt(video.mimeType)}`
- let session
- try {
- session = await startUpload({token, video, name, signal})
- } catch (err) {
- if (signal.aborted) throw new AbortError()
- // A server without multipart support, or one with the kill switch active,
- // leaves no reservation behind. The legacy path remains authoritative.
- throw new MultipartFallbackError(
- err instanceof Error ? err.message : 'Multipart upload unavailable',
- )
- }
- onStarted?.()
-
+ const session = await startUpload({token, video, name, signal})
const {jobId} = session
const abortOnCancel = () => {
void tokenProvider
@@ -89,7 +73,7 @@ export async function uploadVideoMultipart({
})
} catch (err) {
if (signal.aborted) throw new AbortError()
- return await abortThenFallbackOrResolve(
+ return await abortThenRethrowOrResolve(
jobId,
await tokenProvider.get(),
err,
@@ -166,14 +150,14 @@ async function finishAndRecover({
}
} catch (err) {
throwIfAborted(signal)
- return await abortThenFallbackOrResolve(jobId, token, err)
+ return await abortThenRethrowOrResolve(jobId, token, err)
}
createdFailures++
if (createdFailures < MULTIPART_FINISH_ATTEMPTS) {
await delay(500 * 2 ** (createdFailures - 1), signal)
continue
}
- return await abortThenFallbackOrResolve(jobId, token, finishError)
+ return await abortThenRethrowOrResolve(jobId, token, finishError)
case 'finishing':
// The service may have assembled the upload even though the finish
// request failed. Poll and retry instead of starting a second upload.
@@ -224,16 +208,21 @@ async function getUploadStatusWithRetry(
throw lastError
}
-async function abortThenFallbackOrResolve(
+/**
+ * Releases the reservation for an upload we can no longer finish, then surfaces
+ * the failure that got us here. The abort can race a service-side completion,
+ * so a `completed` result is resolved as a success instead.
+ */
+async function abortThenRethrowOrResolve(
jobId: string,
token: string,
cause: unknown,
): Promise {
const result = await abortUploadWithRetry(jobId, token)
if (result.state === 'aborted') {
- throw new MultipartFallbackError(
- cause instanceof Error ? cause.message : 'Multipart upload failed',
- )
+ throw cause instanceof Error
+ ? cause
+ : new MultipartUploadError('Multipart upload failed')
}
if (result.state === 'completed' && result.completedJobId) {
const status = await getUploadStatus(jobId, token)
diff --git a/src/lib/media/video/telemetry.ts b/src/lib/media/video/telemetry.ts
index 188abb6d80..6b17ffa571 100644
--- a/src/lib/media/video/telemetry.ts
+++ b/src/lib/media/video/telemetry.ts
@@ -5,7 +5,6 @@ import {nanoid} from 'nanoid/non-secure'
import {
type ProbedMetadata,
type VideoCompressSkipReason,
- type VideoUploadTransport,
} from '#/lib/media/video/types'
import {Sentry} from '#/logger/sentry/lib'
import {type Metrics} from '#/analytics/metrics'
@@ -46,7 +45,6 @@ export type VideoTelemetry = {
compressCompleted: (video: {size: number; mimeType: string}) => void
compressFailed: (e: unknown) => void
uploadStarted: (bytes: number) => void
- uploadTransport: (transport: VideoUploadTransport) => void
uploadCompleted: (jobId: string) => void
uploadFailed: (e: unknown) => void
processingStarted: (jobId: string) => void
@@ -72,7 +70,6 @@ export function createVideoTelemetry({
let phaseStartedAt = startedAt
let jobId: string | undefined
let uploadBytes: number | undefined
- let uploadTransport: VideoUploadTransport = 'legacy'
let txnEnded = false
let abortBound = true
@@ -229,11 +226,6 @@ export function createVideoTelemetry({
metric('video:upload:uploadStarted', {uploadId, engine, bytes})
},
- uploadTransport(transport) {
- uploadTransport = transport
- phaseSpan?.setAttribute('video.upload.transport', transport)
- },
-
uploadCompleted(id) {
jobId = id
const elapsedMs = Date.now() - phaseStartedAt
@@ -246,7 +238,6 @@ export function createVideoTelemetry({
elapsedMs,
throughputBytesPerSec:
elapsedMs > 0 ? Math.round((bytes * 1000) / elapsedMs) : 0,
- transport: uploadTransport,
})
endPhaseSpan()
phase = undefined
@@ -259,7 +250,6 @@ export function createVideoTelemetry({
bytes: uploadBytes ?? 0,
errorClass: errorClass(e),
elapsedMs: Date.now() - phaseStartedAt,
- transport: uploadTransport,
})
endTxn('error')
detachAbort()
diff --git a/src/lib/media/video/types.ts b/src/lib/media/video/types.ts
index f929c06664..64984d4388 100644
--- a/src/lib/media/video/types.ts
+++ b/src/lib/media/video/types.ts
@@ -5,8 +5,6 @@
export type VideoCompressSkipReason =
'gif' | 'below-byte-threshold' | 'no-webcodecs' | 'compress-error-fallback'
-export type VideoUploadTransport = 'multipart' | 'legacy' | 'legacy-fallback'
-
export type CompressedVideo = {
uri: string
mimeType: string
diff --git a/src/lib/media/video/upload.ts b/src/lib/media/video/upload.ts
index d5a2af5146..1e390372b5 100644
--- a/src/lib/media/video/upload.ts
+++ b/src/lib/media/video/upload.ts
@@ -1,116 +1,37 @@
-import {createUploadTask, FileSystemUploadType} from 'expo-file-system/legacy'
import {type Client} from '@atproto/lex'
import {type I18n} from '@lingui/core'
-import {msg} from '@lingui/core/macro'
-import {nanoid} from 'nanoid/non-secure'
import {AbortError} from '#/lib/async/cancelable'
-import {ServerError} from '#/lib/media/video/errors'
-import {
- type CompressedVideo,
- type VideoUploadTransport,
-} from '#/lib/media/video/types'
-import {Features, features} from '#/analytics/features'
-import {type app} from '#/lexicons'
-import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload'
-import {
- getServiceAuthToken,
- getVideoUploadLimits,
- serviceAuthExp,
-} from './upload.shared'
-import {createVideoEndpointUrl, mimeToExt} from './util'
+import {type CompressedVideo} from '#/lib/media/video/types'
+import {uploadVideoMultipart} from './multipart/upload'
+import {getVideoUploadLimits} from './upload.shared'
export async function uploadVideo({
video,
client,
dispatchUrl,
- did,
setProgress,
signal,
i18n,
- onTransport,
}: {
video: CompressedVideo
client: Client
/** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */
dispatchUrl: string | URL
- did: string
setProgress: (progress: number) => void
signal: AbortSignal
i18n: I18n
- onTransport?: (transport: VideoUploadTransport) => void
}) {
if (signal.aborted) {
throw new AbortError()
}
await getVideoUploadLimits(client, i18n)
- if (features.isOn(Features.VideoMultipartUploadEnable)) {
- try {
- return await uploadVideoMultipart({
- video,
- client,
- dispatchUrl,
- setProgress,
- signal,
- onStarted: () => onTransport?.('multipart'),
- })
- } catch (err) {
- if (!(err instanceof MultipartFallbackError)) throw err
- onTransport?.('legacy-fallback')
- setProgress(0)
- }
- } else {
- onTransport?.('legacy')
- }
-
- const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
- did,
- name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`,
- })
-
- if (signal.aborted) {
- throw new AbortError()
- }
- const token = await getServiceAuthToken({
+ return await uploadVideoMultipart({
+ video,
client,
dispatchUrl,
- lxm: 'com.atproto.repo.uploadBlob',
- exp: serviceAuthExp(),
+ setProgress,
+ signal,
})
- const uploadTask = createUploadTask(
- uri,
- video.uri,
- {
- headers: {
- 'content-type': video.mimeType,
- Authorization: `Bearer ${token}`,
- },
- httpMethod: 'POST',
- uploadType: FileSystemUploadType.BINARY_CONTENT,
- },
- p => setProgress(p.totalBytesSent / p.totalBytesExpectedToSend),
- )
-
- if (signal.aborted) {
- throw new AbortError()
- }
- const res = await uploadTask.uploadAsync()
-
- if (!res?.body) {
- throw new Error('No response')
- }
-
- const responseBody = JSON.parse(res.body) as app.bsky.video.defs.JobStatus
-
- if (!responseBody.jobId) {
- throw new ServerError(
- responseBody.error || i18n._(msg`Failed to upload video`),
- )
- }
-
- if (signal.aborted) {
- throw new AbortError()
- }
- return responseBody
}
diff --git a/src/lib/media/video/upload.web.ts b/src/lib/media/video/upload.web.ts
deleted file mode 100644
index b0fcbaaf09..0000000000
--- a/src/lib/media/video/upload.web.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import {type Client} from '@atproto/lex'
-import {type I18n} from '@lingui/core'
-import {msg} from '@lingui/core/macro'
-import {nanoid} from 'nanoid/non-secure'
-
-import {AbortError} from '#/lib/async/cancelable'
-import {ServerError} from '#/lib/media/video/errors'
-import {
- type CompressedVideo,
- type VideoUploadTransport,
-} from '#/lib/media/video/types'
-import {Features, features} from '#/analytics/features'
-import {type app} from '#/lexicons'
-import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload'
-import {
- getServiceAuthToken,
- getVideoUploadLimits,
- serviceAuthExp,
-} from './upload.shared'
-import {createVideoEndpointUrl, mimeToExt} from './util'
-
-export async function uploadVideo({
- video,
- client,
- dispatchUrl,
- did,
- setProgress,
- signal,
- i18n,
- onTransport,
-}: {
- video: CompressedVideo
- client: Client
- /** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */
- dispatchUrl: string | URL
- did: string
- setProgress: (progress: number) => void
- signal: AbortSignal
- i18n: I18n
- onTransport?: (transport: VideoUploadTransport) => void
-}) {
- if (signal.aborted) {
- throw new AbortError()
- }
- await getVideoUploadLimits(client, i18n)
-
- if (features.isOn(Features.VideoMultipartUploadEnable)) {
- try {
- return await uploadVideoMultipart({
- video,
- client,
- dispatchUrl,
- setProgress,
- signal,
- onStarted: () => onTransport?.('multipart'),
- })
- } catch (err) {
- if (!(err instanceof MultipartFallbackError)) throw err
- onTransport?.('legacy-fallback')
- setProgress(0)
- }
- } else {
- onTransport?.('legacy')
- }
-
- const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
- did,
- name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`,
- })
-
- let bytes = video.bytes
- if (!bytes) {
- if (signal.aborted) {
- throw new AbortError()
- }
- bytes = await fetch(video.uri).then(res => res.arrayBuffer())
- }
-
- if (signal.aborted) {
- throw new AbortError()
- }
- const token = await getServiceAuthToken({
- client,
- dispatchUrl,
- lxm: 'com.atproto.repo.uploadBlob',
- exp: serviceAuthExp(),
- })
-
- if (signal.aborted) {
- throw new AbortError()
- }
- const xhr = new XMLHttpRequest()
- const res = await new Promise(
- (resolve, reject) => {
- xhr.upload.addEventListener('progress', e => {
- const progress = e.loaded / e.total
- setProgress(progress)
- })
- xhr.onloadend = () => {
- if (signal.aborted) {
- reject(new AbortError())
- } else if (xhr.readyState === 4) {
- const uploadRes = JSON.parse(
- xhr.responseText,
- ) as app.bsky.video.defs.JobStatus
- resolve(uploadRes)
- } else {
- reject(new ServerError(i18n._(msg`Failed to upload video`)))
- }
- }
- xhr.onerror = () => {
- reject(new ServerError(i18n._(msg`Failed to upload video`)))
- }
- xhr.open('POST', uri)
- xhr.setRequestHeader('Content-Type', video.mimeType)
- xhr.setRequestHeader('Authorization', `Bearer ${token}`)
- xhr.send(bytes)
- },
- )
-
- if (!res.jobId) {
- throw new ServerError(res.error || i18n._(msg`Failed to upload video`))
- }
-
- if (signal.aborted) {
- throw new AbortError()
- }
- return res
-}
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 6b4bf9a63f..8738b1e1ed 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -62,7 +62,6 @@ import {
MAX_GRAPHEME_LENGTH,
SUPPORTED_MIME_TYPES,
type SupportedMimeTypes,
- VIDEO_10_MINUTE_MAX_DURATION_MS,
VIDEO_MAX_DURATION_MS,
} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -270,17 +269,10 @@ export const ComposePost = ({
const {currentAccount} = useSession()
const t = useTheme()
const ax = useAnalytics()
- const allow10MinuteVideos = ax.features.enabled(
- ax.features.VideoAllow10MinuteEnable,
- )
- const videoMaxDurationMs = allow10MinuteVideos
- ? VIDEO_10_MINUTE_MAX_DURATION_MS
- : VIDEO_MAX_DURATION_MS
const client = useAppviewClient()
const chatClient = useChatClient()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
- const currentDid = currentAccount!.did
/*
* The host the video service-auth token is minted for. This is the same value
* that seeds the session's PDS routing, so the audience always matches the host
@@ -451,7 +443,7 @@ export const ComposePost = ({
* Fail early on duration so we don't spend time compressing a video the
* server would reject anyway.
*/
- if (asset.duration != null && asset.duration > videoMaxDurationMs) {
+ if (asset.duration != null && asset.duration > VIDEO_MAX_DURATION_MS) {
composerDispatch({
type: 'update_post',
postId: postId,
@@ -459,9 +451,7 @@ export const ComposePost = ({
type: 'embed_update_video',
videoAction: {
type: 'to_error',
- error: allow10MinuteVideos
- ? l`Videos must be 10 minutes or less.`
- : l`Videos must be less than 3 minutes long.`,
+ error: l`Videos must be 10 minutes or less.`,
signal: abortController.signal,
},
},
@@ -483,23 +473,12 @@ export const ComposePost = ({
},
pdsClient,
currentDispatchUrl,
- currentDid,
abortController.signal,
i18n,
telemetry,
)
},
- [
- l,
- i18n,
- pdsClient,
- currentDispatchUrl,
- currentDid,
- composerDispatch,
- ax.metric,
- videoMaxDurationMs,
- allow10MinuteVideos,
- ],
+ [l, i18n, pdsClient, currentDispatchUrl, composerDispatch, ax.metric],
)
const onInitVideo = useNonReactiveCallback(() => {
@@ -596,7 +575,7 @@ export const ComposePost = ({
},
})
- if (asset.duration != null && asset.duration > videoMaxDurationMs) {
+ if (asset.duration != null && asset.duration > VIDEO_MAX_DURATION_MS) {
composerDispatch({
type: 'update_post',
postId,
@@ -604,9 +583,7 @@ export const ComposePost = ({
type: 'embed_update_video',
videoAction: {
type: 'to_error',
- error: allow10MinuteVideos
- ? l`Videos must be 10 minutes or less.`
- : l`Videos must be less than 3 minutes long.`,
+ error: l`Videos must be 10 minutes or less.`,
signal: abortController.signal,
},
},
@@ -667,7 +644,6 @@ export const ComposePost = ({
},
pdsClient,
currentDispatchUrl,
- currentDid,
abortController.signal,
i18n,
telemetry,
@@ -679,17 +655,7 @@ export const ComposePost = ({
})
}
},
- [
- l,
- i18n,
- pdsClient,
- currentDispatchUrl,
- currentDid,
- composerDispatch,
- ax.metric,
- videoMaxDurationMs,
- allow10MinuteVideos,
- ],
+ [l, i18n, pdsClient, currentDispatchUrl, composerDispatch, ax.metric],
)
const handleSelectDraft = useCallback(
diff --git a/src/view/com/composer/SelectMediaButton.tsx b/src/view/com/composer/SelectMediaButton.tsx
index 15872a4733..83c371d336 100644
--- a/src/view/com/composer/SelectMediaButton.tsx
+++ b/src/view/com/composer/SelectMediaButton.tsx
@@ -6,7 +6,6 @@ import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {
- VIDEO_10_MINUTE_MAX_DURATION_MS,
VIDEO_MAX_DURATION_MS,
VIDEO_MAX_SIZE,
VIDEO_MAX_SIZE_MB,
@@ -23,7 +22,6 @@ import {Button} from '#/components/Button'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {Image_Stroke2_Corner2_Rounded as ImageIcon} from '#/components/icons/Image'
import * as toast from '#/components/Toast'
-import {useAnalytics} from '#/analytics'
import {IS_NATIVE, IS_WEB} from '#/env'
import {isAnimatedGif} from './videos/isAnimatedGif'
import {hasWebCodecs} from './videos/metadata'
@@ -400,13 +398,6 @@ export function SelectMediaButton({
autoOpen,
}: SelectMediaButtonProps) {
const {_} = useLingui()
- const ax = useAnalytics()
- const allow10MinuteVideos = ax.features.enabled(
- ax.features.VideoAllow10MinuteEnable,
- )
- const videoMaxDurationMs = allow10MinuteVideos
- ? VIDEO_10_MINUTE_MAX_DURATION_MS
- : VIDEO_MAX_DURATION_MS
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
const sheetWrapper = useSheetWrapper()
@@ -426,7 +417,7 @@ export function SelectMediaButton({
} = await processImagePickerAssets(rawAssets, {
selectionCountRemaining,
allowedAssetTypes,
- videoMaxDurationMs,
+ videoMaxDurationMs: VIDEO_MAX_DURATION_MS,
})
/*
@@ -451,9 +442,9 @@ export function SelectMediaButton({
[SelectedAssetError.MaxVideos]: _(
msg`You can only select one video at a time.`,
),
- [SelectedAssetError.VideoTooLong]: allow10MinuteVideos
- ? _(msg`Videos must be 10 minutes or less.`)
- : _(msg`Videos must be less than 3 minutes long.`),
+ [SelectedAssetError.VideoTooLong]: _(
+ msg`Videos must be 10 minutes or less.`,
+ ),
[SelectedAssetError.MaxGIFs]: _(
msg`You can only select one GIF at a time.`,
),
@@ -473,14 +464,7 @@ export function SelectMediaButton({
errors,
})
},
- [
- _,
- onSelectAssets,
- selectionCountRemaining,
- allowedAssetTypes,
- videoMaxDurationMs,
- allow10MinuteVideos,
- ],
+ [_, onSelectAssets, selectionCountRemaining, allowedAssetTypes],
)
const onPressSelectMedia = useCallback(async () => {
@@ -503,7 +487,10 @@ export function SelectMediaButton({
}
const {assets, canceled} = await sheetWrapper(
- openUnifiedPicker({selectionCountRemaining, videoMaxDurationMs}),
+ openUnifiedPicker({
+ selectionCountRemaining,
+ videoMaxDurationMs: VIDEO_MAX_DURATION_MS,
+ }),
)
if (canceled) return
@@ -516,7 +503,6 @@ export function SelectMediaButton({
sheetWrapper,
processSelectedAssets,
selectionCountRemaining,
- videoMaxDurationMs,
])
useEffect(() => {
diff --git a/src/view/com/composer/state/video.ts b/src/view/com/composer/state/video.ts
index 2f7a1ead62..c0764d343e 100644
--- a/src/view/com/composer/state/video.ts
+++ b/src/view/com/composer/state/video.ts
@@ -6,11 +6,8 @@ import {msg} from '@lingui/core/macro'
import {AbortError} from '#/lib/async/cancelable'
import {VIDEO_MAX_SIZE_MB} from '#/lib/constants'
import {compressVideo} from '#/lib/media/video/compress'
-import {
- ServerError,
- UploadLimitError,
- VideoTooLargeError,
-} from '#/lib/media/video/errors'
+import {UploadLimitError, VideoTooLargeError} from '#/lib/media/video/errors'
+import {MultipartUploadError} from '#/lib/media/video/multipart/api'
import {type VideoTelemetry} from '#/lib/media/video/telemetry'
import {type CompressedVideo} from '#/lib/media/video/types'
import {uploadVideo} from '#/lib/media/video/upload'
@@ -294,7 +291,6 @@ export async function processVideo(
dispatch: (action: VideoAction) => void,
client: Client,
dispatchUrl: string | URL,
- did: string,
signal: AbortSignal,
i18n: I18n,
telemetry: VideoTelemetry,
@@ -344,10 +340,8 @@ export async function processVideo(
video,
client,
dispatchUrl,
- did,
signal,
i18n,
- onTransport: telemetry.uploadTransport,
setProgress: p => {
dispatch({type: 'update_progress', progress: p, signal})
},
@@ -513,7 +507,7 @@ function getUploadErrorMessage(e: unknown, i18n: I18n): string | null {
if (e instanceof AbortError) {
return null
}
- if (e instanceof ServerError || e instanceof UploadLimitError) {
+ if (e instanceof MultipartUploadError || e instanceof UploadLimitError) {
// https://github.com/bluesky-social/tango/blob/lumi/lumi/worker/permissions.go#L77
switch (e.message) {
case 'User is not allowed to upload videos':
From 196f41b52837a25f2e7b5bcc03b561150f96680b Mon Sep 17 00:00:00 2001
From: Austin McKinley <54160+amckinley@users.noreply.github.com>
Date: Tue, 25 Aug 2026 18:46:25 -0700
Subject: [PATCH 13/22] Remove legacy ota1 publisher (#11554)
---
.../workflows/bundle-deploy-eas-update.yml | 38 --------------
package.json | 1 -
scripts/bundleUpdate.sh | 52 -------------------
scripts/denisPublish.sh | 14 +----
4 files changed, 2 insertions(+), 103 deletions(-)
delete mode 100644 scripts/bundleUpdate.sh
diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml
index 7899b62269..d4e18cc510 100644
--- a/.github/workflows/bundle-deploy-eas-update.yml
+++ b/.github/workflows/bundle-deploy-eas-update.yml
@@ -225,31 +225,6 @@ jobs:
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
pnpm export
- # Pin ONE bundle version for both publishes below. Each script used to call
- # `date +%s` itself, so the same bytes reached denis and ota1 under versions
- # seconds apart (observed: 1785102575 vs 1785102614). The version is part of
- # the asset URL path, so each origin then served a manifest referencing a
- # path only it had -- meaning a manifest fetched from one origin and assets
- # fetched from the other 404. Both scripts fall back to `date +%s` when this
- # is unset, so single-publisher callers are unaffected.
- - name: 🔢 Pin bundle version
- if: ${{ !steps.fingerprint.outputs.includes-changes &&
- !steps.version.outputs.version-changed }}
- run: echo "BUNDLE_VERSION=$(date +%s)" >> "$GITHUB_ENV"
-
- # denis on EKS has been the sole origin for updates.bsky.app since
- # 2026-07-26, so it publishes FIRST: it is the path that actually serves
- # clients. The legacy ota1 upload runs after it, and exists only so that
- # rolling the Bunny origin back to ota1 would find current bundles there.
- #
- # The ordering is load-bearing, not cosmetic. While the legacy step ran
- # first, its failure skipped these steps and nothing reached EITHER origin
- # -- the dual-write took down the working path with it. Both steps are
- # still required to pass, so a stale ota1 remains a loud failure, but the
- # publish that serves users has already landed before the legacy one can
- # fail.
- #
- # Both halves are removed together when ota1 is decommissioned (Phase 5).
- name: ☁️ Configure AWS credentials (denis)
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
@@ -279,19 +254,6 @@ jobs:
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
- - name: 📦 Package Bundle and 🚀 Deploy (legacy ota1)
- if: ${{ !steps.fingerprint.outputs.includes-changes &&
- !steps.version.outputs.version-changed }}
- run: pnpm use-build-number bash scripts/bundleUpdate.sh
- env:
- DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
- RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
- CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
- # When set (required for production), these take precedence over the
- # global EAS counters inside the use-build-number wrapper
- BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
- BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
-
buildIfNecessaryIOS:
name: Build and Submit iOS
needs: [bundleDeploy]
diff --git a/package.json b/package.json
index 3ae22dc51a..861f9fa5f7 100644
--- a/package.json
+++ b/package.json
@@ -93,7 +93,6 @@
"update-extensions": "bash scripts/updateExtensions.sh",
"export": "expo export --dump-sourcemap && pnpm upload-native-sourcemaps",
"upload-native-sourcemaps": "pnpm exec sentry-expo-upload-sourcemaps dist",
- "make-deploy-bundle": "bash scripts/bundleUpdate.sh",
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 pnpm build-web",
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 pnpm build-web",
"icons:optimize": "svgo -f ./assets/icons",
diff --git a/scripts/bundleUpdate.sh b/scripts/bundleUpdate.sh
deleted file mode 100644
index 35bed14ed0..0000000000
--- a/scripts/bundleUpdate.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/bin/bash
-set -o errexit
-set -o pipefail
-set -o nounset
-
-rm -rf bundleTempDir
-rm -rf bundle.tar.gz
-
-echo "Creating tarball..."
-node scripts/bundleUpdate.js
-
-if [ -z "$RUNTIME_VERSION" ]; then
- RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
-fi
-
-cd bundleTempDir || exit
-
-# Shared with denisPublish.sh when both run in one job -- see the note there.
-# Both origins must receive the same bundle version for the same bytes, because
-# the version is part of the asset URL path.
-BUNDLE_VERSION="${BUNDLE_VERSION:-$(date +%s)}"
-
-# This MUST address ota1's own origin hostname, never updates.bsky.app.
-#
-# Since the 2026-07-26 cutover updates.bsky.app resolves to denis on EKS, which
-# deliberately has no /v1/upload route -- publishing there is out-of-band via
-# `denis publish` (see denisPublish.sh). Posting to the CDN hostname therefore
-# returns 404, which is what broke this step the first time it ran after the
-# flip. The dual-write was never independent of the cutover precisely because it
-# addressed the hostname being cut over.
-#
-# This upload exists only to keep ota1 carrying current bundles so a rollback of
-# the Bunny origin remains useful. It goes away with this whole script when ota1
-# is decommissioned (Phase 5).
-OTA1_ORIGIN="${OTA1_ORIGIN:-https://ota1.us-east.updates.bsky.network}"
-DEPLOYMENT_URL="$OTA1_ORIGIN/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
-
-tar czvf bundle.tar.gz ./*
-
-echo "Deploying to $DEPLOYMENT_URL..."
-echo " runtime-version: $RUNTIME_VERSION"
-echo " bundle-version: $BUNDLE_VERSION"
-echo " channel: $CHANNEL_NAME"
-echo " ios-build-number: $BSKY_IOS_BUILD_NUMBER"
-echo " android-build-number: $BSKY_ANDROID_VERSION_CODE"
-
-curl --fail-with-body -o - --form "bundle=@./bundle.tar.gz" --user "bsky:$DENIS_API_KEY" --basic "$DEPLOYMENT_URL"
-
-cd ..
-
-rm -rf bundleTempDir
-rm -rf bundle.tar.gz
diff --git a/scripts/denisPublish.sh b/scripts/denisPublish.sh
index c6a30b77b9..2b4624eca0 100755
--- a/scripts/denisPublish.sh
+++ b/scripts/denisPublish.sh
@@ -4,8 +4,7 @@ set -o pipefail
set -o nounset
# Publishes the just-exported Expo bundle to the denis OTA service (S3) via the
-# `denis publish` CLI. Mirrors bundleUpdate.sh's inputs (runtime version, bundle
-# version, build numbers) but targets denis instead of the legacy ota1 upload.
+# `denis publish` CLI.
# Expects: the `denis` binary on PATH (setup-denis action), ambient AWS creds
# (configure-aws-credentials OIDC), and BSKY_IOS_BUILD_NUMBER /
# BSKY_ANDROID_VERSION_CODE from the use-build-number wrapper.
@@ -19,16 +18,7 @@ if [ -z "$RUNTIME_VERSION" ]; then
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
fi
-# Accept a caller-supplied bundle version so that a dual-write publishes the SAME
-# version to every origin. When this script and bundleUpdate.sh each called
-# `date +%s` independently they produced versions seconds apart for identical
-# bytes -- observed 1785102575 (denis) vs 1785102614 (ota1) for one commit. Since
-# the version is part of the asset URL path, the two origins then served
-# manifests pointing at paths only one of them had, so the manifest and its
-# assets had to come from the same origin or the fetch 404s. Falling back to
-# `date +%s` keeps standalone callers (PR previews, `pnpm make-deploy-bundle`)
-# working unchanged.
-BUNDLE_VERSION="${BUNDLE_VERSION:-$(date +%s)}"
+BUNDLE_VERSION=$(date +%s)
DENIS_CDN_DOMAIN="${DENIS_CDN_DOMAIN:-updates.bsky.app}"
DENIS_S3_BUCKET="${DENIS_S3_BUCKET:-bsky-denis-ota-prod}"
From 5b88335bb118f4f65dcb564e10109d6c6a78cb67 Mon Sep 17 00:00:00 2001
From: pfrazee <1270099+pfrazee@users.noreply.github.com>
Date: Wed, 26 Aug 2026 02:35:19 +0000
Subject: [PATCH 14/22] Nightly source-language update
---
src/locale/locales/en/messages.po | 288 ++++++++++++++----------------
1 file changed, 134 insertions(+), 154 deletions(-)
diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po
index 799f6027ed..486b9bcfd5 100644
--- a/src/locale/locales/en/messages.po
+++ b/src/locale/locales/en/messages.po
@@ -401,9 +401,12 @@ msgctxt "gallery-badge-image-position-numbers"
msgid "{0}/{imageCount}"
msgstr "{0}/{imageCount}"
+#. Displayed when the number of notifications exceeds the cap – for example, 99+ notifications
#. Displayed when the number of requests exceeds the cap – for example, 99+ requests
#. placeholder {0}: UNREAD_REQUEST_CAP - 1
+#. placeholder {0}: i18n.number(UNREAD_NOTIFICATION_CAP - 1)
#: src/screens/Messages/components/InboxRequests.tsx:55
+#: src/screens/Moderation/index.tsx:173
msgid "{0}+"
msgstr "{0}+"
@@ -680,7 +683,7 @@ msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr ""
#. Badge indicating post count in a thread, e.g., the 3rd post of 5 total is '3/5'
-#: src/screens/PostThread/components/ThreadItemPostNumber.tsx:89
+#: src/screens/PostThread/components/ThreadItemPostNumber.tsx:95
msgctxt "post-number-in-thread"
msgid "{index}/{count}"
msgstr "{index}/{count}"
@@ -1196,11 +1199,11 @@ msgstr ""
msgid "Add another account"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1659
+#: src/view/com/composer/Composer.tsx:1625
msgid "Add another post"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2328
+#: src/view/com/composer/Composer.tsx:2294
msgid "Add another post to thread"
msgstr ""
@@ -1239,7 +1242,7 @@ msgid "Add image"
msgstr ""
#. Accessibility label for button in composer to add images, a video, or a GIF to a post
-#: src/view/com/composer/SelectMediaButton.tsx:534
+#: src/view/com/composer/SelectMediaButton.tsx:520
msgid "Add media to post"
msgstr ""
@@ -1380,7 +1383,7 @@ msgstr ""
msgid "Adult Content"
msgstr ""
-#: src/screens/Moderation/index.tsx:464
+#: src/screens/Moderation/index.tsx:467
msgid "Adult content can only be enabled via the Web at <0>bsky.app0>."
msgstr ""
@@ -1397,7 +1400,7 @@ msgstr ""
msgid "Adult sexual abuse content"
msgstr ""
-#: src/screens/Moderation/index.tsx:509
+#: src/screens/Moderation/index.tsx:511
msgid "Advanced"
msgstr ""
@@ -1582,7 +1585,7 @@ msgstr ""
msgid "An error occurred"
msgstr ""
-#: src/view/com/composer/state/video.ts:509
+#: src/view/com/composer/state/video.ts:503
msgid "An error occurred while compressing the video."
msgstr ""
@@ -1628,11 +1631,11 @@ msgstr ""
msgid "An error occurred while trying to follow all"
msgstr ""
-#: src/view/com/composer/state/video.ts:561
+#: src/view/com/composer/state/video.ts:555
msgid "An error occurred while uploading the video. {message}"
msgstr ""
-#: src/view/com/composer/state/video.ts:553
+#: src/view/com/composer/state/video.ts:547
msgid "An error occurred while uploading the video. Please check your internet connection and try again."
msgstr ""
@@ -1940,7 +1943,7 @@ msgstr ""
msgid "Are you sure you want to rescind your request to join {0}?"
msgstr "Are you sure you want to rescind your request to join {0}?"
-#: src/view/com/composer/Composer.tsx:1795
+#: src/view/com/composer/Composer.tsx:1761
msgid "Are you sure you'd like to discard this post?"
msgstr ""
@@ -2254,7 +2257,7 @@ msgstr "Block user and/or leave this conversation"
msgid "Blocked"
msgstr ""
-#: src/screens/Moderation/index.tsx:379
+#: src/screens/Moderation/index.tsx:385
msgid "Blocked accounts"
msgstr ""
@@ -2560,8 +2563,8 @@ msgstr "Camera access needed"
#: src/screens/Settings/Settings.tsx:312
#: src/screens/Takendown.tsx:100
#: src/screens/Takendown.tsx:103
-#: src/view/com/composer/Composer.tsx:1873
-#: src/view/com/composer/Composer.tsx:1883
+#: src/view/com/composer/Composer.tsx:1839
+#: src/view/com/composer/Composer.tsx:1849
#: src/view/com/composer/photos/EditImageDialog.web.tsx:44
#: src/view/com/composer/photos/EditImageDialog.web.tsx:53
#: src/view/shell/desktop/LeftNav.tsx:228
@@ -3114,7 +3117,7 @@ msgstr ""
msgid "Closes password update alert"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1881
+#: src/view/com/composer/Composer.tsx:1847
msgid "Closes post composer and discards post draft"
msgstr ""
@@ -3168,7 +3171,7 @@ msgid "Compose new post"
msgstr ""
#. placeholder {0}: MAX_GRAPHEME_LENGTH || 0
-#: src/view/com/composer/Composer.tsx:1757
+#: src/view/com/composer/Composer.tsx:1723
msgid "Compose posts up to {0, plural, other {# characters}} in length"
msgstr ""
@@ -3176,11 +3179,11 @@ msgstr ""
msgid "Compose reply"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2711
+#: src/view/com/composer/Composer.tsx:2677
msgid "Compressing GIF..."
msgstr ""
-#: src/view/com/composer/Composer.tsx:2713
+#: src/view/com/composer/Composer.tsx:2679
msgid "Compressing video..."
msgstr ""
@@ -3277,7 +3280,7 @@ msgstr ""
msgid "Content Blocked"
msgstr ""
-#: src/screens/Moderation/index.tsx:412
+#: src/screens/Moderation/index.tsx:417
msgid "Content filters"
msgstr ""
@@ -3907,7 +3910,7 @@ msgstr ""
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:808
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:810
-#: src/view/com/composer/Composer.tsx:1769
+#: src/view/com/composer/Composer.tsx:1735
msgid "Delete post"
msgstr ""
@@ -4052,15 +4055,15 @@ msgstr "Disable this invite link?"
#: src/lib/moderation/useLabelBehaviorDescription.ts:35
#: src/lib/moderation/useLabelBehaviorDescription.ts:45
#: src/lib/moderation/useLabelBehaviorDescription.ts:71
-#: src/screens/Moderation/index.tsx:454
+#: src/screens/Moderation/index.tsx:457
msgid "Disabled"
msgstr ""
#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:102
#: src/screens/Profile/Header/EditProfileDialog.tsx:79
-#: src/view/com/composer/Composer.tsx:1549
-#: src/view/com/composer/Composer.tsx:1593
-#: src/view/com/composer/Composer.tsx:1802
+#: src/view/com/composer/Composer.tsx:1515
+#: src/view/com/composer/Composer.tsx:1559
+#: src/view/com/composer/Composer.tsx:1768
#: src/view/com/composer/drafts/DraftItem.tsx:243
#: src/view/com/composer/drafts/DraftsButton.tsx:131
msgid "Discard"
@@ -4071,14 +4074,14 @@ msgstr ""
msgid "Discard changes?"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1547
+#: src/view/com/composer/Composer.tsx:1513
#: src/view/com/composer/drafts/DraftItem.tsx:240
#: src/view/com/composer/drafts/DraftsButton.tsx:98
msgid "Discard draft?"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1564
-#: src/view/com/composer/Composer.tsx:1794
+#: src/view/com/composer/Composer.tsx:1530
+#: src/view/com/composer/Composer.tsx:1760
msgid "Discard post?"
msgstr ""
@@ -4111,7 +4114,7 @@ msgstr ""
msgid "Dismiss banner"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2653
+#: src/view/com/composer/Composer.tsx:2619
msgid "Dismiss error"
msgstr ""
@@ -4538,12 +4541,7 @@ msgstr ""
msgid "Enable {0} only"
msgstr ""
-#. Description of a feature flag (10-minute video uploads)
-#: src/analytics/features/index.ts:90
-msgid "Enable 10-minute video uploads."
-msgstr "Enable 10-minute video uploads."
-
-#: src/screens/Moderation/index.tsx:439
+#: src/screens/Moderation/index.tsx:442
msgid "Enable adult content"
msgstr ""
@@ -4597,7 +4595,7 @@ msgstr ""
msgid "Enable trending videos in your Discover feed"
msgstr ""
-#: src/screens/Moderation/index.tsx:452
+#: src/screens/Moderation/index.tsx:455
msgid "Enabled"
msgstr ""
@@ -4674,7 +4672,7 @@ msgstr ""
msgid "Entertainment"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2731
+#: src/view/com/composer/Composer.tsx:2697
#: src/view/com/util/error/ErrorScreen.tsx:40
msgid "Error"
msgstr ""
@@ -5117,7 +5115,7 @@ msgstr "Failed to rescind your request. Please try again."
msgid "Failed to resolve location. Please try again."
msgstr ""
-#: src/view/com/composer/Composer.tsx:775
+#: src/view/com/composer/Composer.tsx:741
msgid "Failed to save draft"
msgstr ""
@@ -5193,13 +5191,6 @@ msgstr ""
msgid "Failed to update settings"
msgstr ""
-#: src/lib/media/video/upload.ts:108
-#: src/lib/media/video/upload.web.ts:108
-#: src/lib/media/video/upload.web.ts:112
-#: src/lib/media/video/upload.web.ts:122
-msgid "Failed to upload video"
-msgstr ""
-
#: src/components/dialogs/EmailDialog/screens/Verify.tsx:169
msgid "Failed to verify email, please try again."
msgstr ""
@@ -5808,7 +5799,7 @@ msgstr ""
msgid "GIF"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2736
+#: src/view/com/composer/Composer.tsx:2702
msgid "GIF uploaded"
msgstr ""
@@ -5896,7 +5887,7 @@ msgstr ""
#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:93
#: src/components/ageAssurance/AgeRestrictedScreen.tsx:73
#: src/components/ageAssurance/AgeRestrictedScreen.tsx:82
-#: src/screens/Moderation/index.tsx:267
+#: src/screens/Moderation/index.tsx:276
msgid "Go to account settings"
msgstr ""
@@ -6301,7 +6292,7 @@ msgstr ""
msgid "Hmmmm, we couldn't load that moderation service."
msgstr ""
-#: src/view/com/composer/state/video.ts:523
+#: src/view/com/composer/state/video.ts:517
msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!"
msgstr ""
@@ -6620,7 +6611,7 @@ msgstr ""
msgid "Interaction limited"
msgstr ""
-#: src/screens/Moderation/index.tsx:319
+#: src/screens/Moderation/index.tsx:325
msgid "Interaction settings"
msgstr ""
@@ -6785,7 +6776,7 @@ msgid "It's just you right now! Add more people to your starter pack by searchin
msgstr ""
#. placeholder {0}: videoState.jobId
-#: src/view/com/composer/Composer.tsx:2672
+#: src/view/com/composer/Composer.tsx:2638
msgid "Job ID: {0}"
msgstr ""
@@ -6825,8 +6816,8 @@ msgstr ""
msgid "Journalism"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1597
-#: src/view/com/composer/Composer.tsx:1607
+#: src/view/com/composer/Composer.tsx:1563
+#: src/view/com/composer/Composer.tsx:1573
#: src/view/com/composer/drafts/DraftsButton.tsx:135
msgid "Keep editing"
msgstr ""
@@ -7390,11 +7381,6 @@ msgstr ""
msgid "Long press to open tag menu for {0}"
msgstr ""
-#. Name for a feature flag (longer videos)
-#: src/analytics/features/index.ts:84
-msgid "Longer videos"
-msgstr "Longer videos"
-
#: src/screens/Login/SetNewPasswordForm.tsx:119
msgid "Looks like XXXXX-XXXXX"
msgstr ""
@@ -7433,7 +7419,7 @@ msgstr ""
msgid "Manage saved feeds"
msgstr ""
-#: src/screens/Moderation/index.tsx:389
+#: src/screens/Moderation/index.tsx:395
msgid "Manage verification settings"
msgstr ""
@@ -7644,7 +7630,7 @@ msgid "Moderation details"
msgstr ""
#: src/Navigation.tsx:185
-#: src/screens/Moderation/index.tsx:284
+#: src/screens/Moderation/index.tsx:292
#: src/screens/ModerationInbox/index.tsx:21
msgid "Moderation inbox"
msgstr "Moderation inbox"
@@ -7675,7 +7661,7 @@ msgctxt "toast"
msgid "Moderation list updated"
msgstr ""
-#: src/screens/Moderation/index.tsx:349
+#: src/screens/Moderation/index.tsx:355
msgid "Moderation lists"
msgstr ""
@@ -7692,7 +7678,7 @@ msgstr ""
msgid "Moderation states"
msgstr ""
-#: src/screens/Moderation/index.tsx:303
+#: src/screens/Moderation/index.tsx:310
msgid "Moderation tools"
msgstr ""
@@ -7825,7 +7811,7 @@ msgstr "Mute, leave, or remove people anytime. It’s your chat."
msgid "Muted"
msgstr "Muted"
-#: src/screens/Moderation/index.tsx:364
+#: src/screens/Moderation/index.tsx:370
msgid "Muted accounts"
msgstr ""
@@ -7843,7 +7829,7 @@ msgstr ""
msgid "Muted by \"{0}\""
msgstr ""
-#: src/screens/Moderation/index.tsx:334
+#: src/screens/Moderation/index.tsx:340
msgid "Muted words & tags"
msgstr ""
@@ -8514,27 +8500,27 @@ msgstr "One of the selected recipients does not allow group chats."
msgid "One of the selected recipients has blocked you and cannot be messaged."
msgstr "One of the selected recipients has blocked you and cannot be messaged."
-#: src/view/com/composer/Composer.tsx:983
+#: src/view/com/composer/Composer.tsx:949
msgid "One or more GIFs is missing alt text."
msgstr ""
-#: src/view/com/composer/Composer.tsx:980
+#: src/view/com/composer/Composer.tsx:946
msgid "One or more images is missing alt text."
msgstr ""
-#: src/view/com/composer/SelectMediaButton.tsx:438
+#: src/view/com/composer/SelectMediaButton.tsx:429
msgid "One or more of your selected files are not supported."
msgstr ""
-#: src/view/com/composer/SelectMediaButton.tsx:461
+#: src/view/com/composer/SelectMediaButton.tsx:452
msgid "One or more of your selected files are too large. Maximum size is {VIDEO_MAX_SIZE_MB} MB."
msgstr "One or more of your selected files are too large. Maximum size is {VIDEO_MAX_SIZE_MB} MB."
-#: src/view/com/composer/Composer.tsx:786
+#: src/view/com/composer/Composer.tsx:752
msgid "One or more posts are too long to save as a draft. {MAX_DRAFT_GRAPHEME_LENGTH, plural, one {The maximum number of characters is # character.} other {The maximum number of characters is # characters.}}"
msgstr ""
-#: src/view/com/composer/Composer.tsx:990
+#: src/view/com/composer/Composer.tsx:956
msgid "One or more videos is missing alt text."
msgstr ""
@@ -8547,7 +8533,7 @@ msgstr ""
#. placeholder {0}: result.accepted.length
#. placeholder {1}: next.length
#. placeholder {2}: next.length
-#: src/view/com/composer/Composer.tsx:237
+#: src/view/com/composer/Composer.tsx:236
msgid "Only {0} of {1} {2, plural, one {image} other {images}} added; limit is {MAX_GALLERY_IMAGES}"
msgstr "Only {0} of {1} {2, plural, one {image} other {images}} added; limit is {MAX_GALLERY_IMAGES}"
@@ -8655,7 +8641,7 @@ msgid "Open drawer menu"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:216
-#: src/view/com/composer/Composer.tsx:2305
+#: src/view/com/composer/Composer.tsx:2271
msgid "Open emoji picker"
msgstr ""
@@ -8704,7 +8690,7 @@ msgstr ""
msgid "Open moderation debug page"
msgstr ""
-#: src/screens/Moderation/index.tsx:330
+#: src/screens/Moderation/index.tsx:336
msgid "Open muted words and tags settings"
msgstr ""
@@ -8800,7 +8786,7 @@ msgid "Opens device camera"
msgstr ""
#. Accessibility hint for button in composer to add images, a video, or a GIF to a post.
-#: src/view/com/composer/SelectMediaButton.tsx:540
+#: src/view/com/composer/SelectMediaButton.tsx:526
msgid "Opens device gallery to select up to {MAX_GALLERY_IMAGES, plural, other {# images}}, or a single video or GIF."
msgstr "Opens device gallery to select up to {MAX_GALLERY_IMAGES, plural, other {# images}}, or a single video or GIF."
@@ -9210,7 +9196,7 @@ msgstr ""
msgid "Please complete the verification captcha."
msgstr ""
-#: src/view/com/composer/state/video.ts:547
+#: src/view/com/composer/state/video.ts:541
msgid "Please confirm your email address to upload videos."
msgstr ""
@@ -9358,7 +9344,7 @@ msgstr ""
msgid "Porn"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1947
+#: src/view/com/composer/Composer.tsx:1913
msgctxt "action"
msgid "Post"
msgstr ""
@@ -9369,7 +9355,7 @@ msgid "Post"
msgstr ""
#. Screen reader label indicating post count in a thread, e.g., the 3rd post of 5 total is 'Post 3 of 5'
-#: src/screens/PostThread/components/ThreadItemPostNumber.tsx:74
+#: src/screens/PostThread/components/ThreadItemPostNumber.tsx:80
msgctxt "post-number-in-thread"
msgid "Post {index} of {count}"
msgstr "Post {index} of {count}"
@@ -9384,12 +9370,12 @@ msgstr ""
msgid "Post a video"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1945
+#: src/view/com/composer/Composer.tsx:1911
msgctxt "action"
msgid "Post All"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1606
+#: src/view/com/composer/Composer.tsx:1572
msgid "Post anyway"
msgstr "Post anyway"
@@ -9573,11 +9559,11 @@ msgstr ""
msgid "Privacy violation of a minor"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2725
+#: src/view/com/composer/Composer.tsx:2691
msgid "Processing GIF..."
msgstr ""
-#: src/view/com/composer/Composer.tsx:2727
+#: src/view/com/composer/Composer.tsx:2693
msgid "Processing video..."
msgstr ""
@@ -9624,22 +9610,22 @@ msgid "Public, sharable lists of users to mute or block in bulk."
msgstr ""
#. Accessibility label for button to publish a single post
-#: src/view/com/composer/Composer.tsx:1931
+#: src/view/com/composer/Composer.tsx:1897
msgid "Publish post"
msgstr ""
#. Accessibility label for button to publish multiple posts in a thread
-#: src/view/com/composer/Composer.tsx:1926
+#: src/view/com/composer/Composer.tsx:1892
msgid "Publish posts"
msgstr ""
#. Accessibility label for button to publish multiple replies in a thread
-#: src/view/com/composer/Composer.tsx:1915
+#: src/view/com/composer/Composer.tsx:1881
msgid "Publish replies"
msgstr ""
#. Accessibility label for button to publish a single reply
-#: src/view/com/composer/Composer.tsx:1920
+#: src/view/com/composer/Composer.tsx:1886
msgid "Publish reply"
msgstr ""
@@ -9859,7 +9845,7 @@ msgstr ""
#: src/screens/Bookmarks.tsx:256
#: src/screens/Messages/ConversationSettings/Member.tsx:162
#: src/screens/Messages/ConversationSettings/prompts.tsx:178
-#: src/screens/Moderation/index.tsx:529
+#: src/screens/Moderation/index.tsx:530
#: src/screens/Settings/Settings.tsx:695
#: src/view/com/posts/PostFeedErrorMessage.tsx:219
msgid "Remove"
@@ -10001,7 +9987,7 @@ msgstr ""
msgid "Remove this feed from your saved feeds"
msgstr ""
-#: src/screens/Moderation/index.tsx:525
+#: src/screens/Moderation/index.tsx:526
msgid "Remove unavailable moderation services"
msgstr ""
@@ -10052,7 +10038,7 @@ msgstr ""
msgid "Removed from your feeds"
msgstr ""
-#: src/screens/Moderation/index.tsx:216
+#: src/screens/Moderation/index.tsx:224
msgid "Removed unavailable services"
msgstr ""
@@ -10132,7 +10118,7 @@ msgstr ""
msgid "Reply"
msgstr "Reply"
-#: src/view/com/composer/Composer.tsx:1943
+#: src/view/com/composer/Composer.tsx:1909
msgctxt "action"
msgid "Reply"
msgstr ""
@@ -10563,22 +10549,22 @@ msgstr ""
#: src/screens/SavedFeeds.tsx:124
#: src/screens/SavedFeeds.tsx:311
#: src/screens/SavedFeeds.tsx:315
-#: src/view/com/composer/Composer.tsx:1587
+#: src/view/com/composer/Composer.tsx:1553
#: src/view/com/composer/drafts/DraftsButton.tsx:125
msgid "Save changes"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1559
+#: src/view/com/composer/Composer.tsx:1525
#: src/view/com/composer/drafts/DraftsButton.tsx:93
msgid "Save changes?"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1587
+#: src/view/com/composer/Composer.tsx:1553
#: src/view/com/composer/drafts/DraftsButton.tsx:125
msgid "Save draft"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1561
+#: src/view/com/composer/Composer.tsx:1527
#: src/view/com/composer/drafts/DraftsButton.tsx:95
msgid "Save draft?"
msgstr ""
@@ -10841,7 +10827,7 @@ msgid "See more trending topics"
msgstr "See more trending topics"
#. Description of a feature flag (Thread numbering)
-#: src/analytics/features/index.ts:107
+#: src/analytics/features/index.ts:91
msgid "See numbered badges (1/3, 2/3, etc.) on posts in a thread by the same author."
msgstr "See numbered badges (1/3, 2/3, etc.) on posts in a thread by the same author."
@@ -11036,7 +11022,7 @@ msgstr ""
msgid "Select your preferred notification channels"
msgstr ""
-#: src/view/com/composer/SelectMediaButton.tsx:441
+#: src/view/com/composer/SelectMediaButton.tsx:432
msgid "Selecting multiple media types is not supported."
msgstr ""
@@ -11605,7 +11591,7 @@ msgstr ""
msgid "Skip contact sharing and continue to the app"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1604
+#: src/view/com/composer/Composer.tsx:1570
msgid "Skip empty posts?"
msgstr "Skip empty posts?"
@@ -11643,7 +11629,7 @@ msgstr ""
msgid "Software Dev"
msgstr ""
-#: src/screens/Moderation/index.tsx:518
+#: src/screens/Moderation/index.tsx:519
msgid "Some moderation services in your list are no longer available."
msgstr ""
@@ -12278,7 +12264,7 @@ msgstr ""
msgid "The birthdate you've entered means you are under 18 years old. Certain content and features may be unavailable to you."
msgstr ""
-#: src/screens/Moderation/index.tsx:467
+#: src/screens/Moderation/index.tsx:470
msgid "The Bluesky web application"
msgstr ""
@@ -12343,32 +12329,32 @@ msgstr "The post you’re replying to was marked as being written in {suggestedL
msgid "The Privacy Policy has been moved to <0/>"
msgstr ""
-#: src/view/com/composer/state/video.ts:494
+#: src/view/com/composer/state/video.ts:488
msgid "The processed video is too large. Please try again with a smaller file."
msgstr "The processed video is too large. Please try again with a smaller file."
-#: src/view/com/composer/state/video.ts:469
+#: src/view/com/composer/state/video.ts:463
msgid "The selected video could not be encoded."
msgstr "The selected video could not be encoded."
-#: src/view/com/composer/state/video.ts:459
+#: src/view/com/composer/state/video.ts:453
msgid "The selected video could not be processed."
msgstr "The selected video could not be processed."
-#: src/view/com/composer/state/video.ts:489
+#: src/view/com/composer/state/video.ts:483
msgid "The selected video has an unsupported aspect ratio."
msgstr "The selected video has an unsupported aspect ratio."
-#: src/view/com/composer/state/video.ts:505
-#: src/view/com/composer/state/video.ts:544
+#: src/view/com/composer/state/video.ts:499
+#: src/view/com/composer/state/video.ts:538
msgid "The selected video is larger than {VIDEO_MAX_SIZE_MB} MB. Please try again with a smaller file."
msgstr "The selected video is larger than {VIDEO_MAX_SIZE_MB} MB. Please try again with a smaller file."
-#: src/view/com/composer/state/video.ts:487
+#: src/view/com/composer/state/video.ts:481
msgid "The selected video is too long."
msgstr "The selected video is too long."
-#: src/view/com/composer/state/video.ts:491
+#: src/view/com/composer/state/video.ts:485
msgid "The selected video uses an unsupported format."
msgstr "The selected video uses an unsupported format."
@@ -12403,7 +12389,7 @@ msgstr ""
msgid "The verification provider was unable to send a code to your phone number. Please check your phone number and try again."
msgstr ""
-#: src/view/com/composer/state/video.ts:472
+#: src/view/com/composer/state/video.ts:466
msgid "The video could not be uploaded to your hosting provider. Please try again."
msgstr "The video could not be uploaded to your hosting provider. Please try again."
@@ -12452,7 +12438,7 @@ msgstr ""
msgid "There was an issue fetching notifications. Tap here to try again."
msgstr ""
-#: src/screens/Search/Explore.tsx:1012
+#: src/screens/Search/Explore.tsx:1013
#: src/view/com/posts/PostFeed.tsx:825
msgid "There was an issue fetching posts. Tap here to try again."
msgstr ""
@@ -12840,7 +12826,7 @@ msgstr ""
msgid "This post will be hidden from feeds and threads. This cannot be undone."
msgstr ""
-#: src/view/com/composer/Composer.tsx:1177
+#: src/view/com/composer/Composer.tsx:1143
msgid "This post's author has disabled quote posts."
msgstr ""
@@ -12949,7 +12935,7 @@ msgid "This will remove your post from this quote post for all users, and replac
msgstr ""
#. Name for a feature flag (See numbered badges (1/3, 2/3, etc.) on posts in a thread by the same author.)
-#: src/analytics/features/index.ts:100
+#: src/analytics/features/index.ts:84
msgid "Thread numbering"
msgstr "Thread numbering"
@@ -13011,7 +12997,7 @@ msgstr "To log out, <0>click here0>. Or if you’d prefer, you can <1>delete y
msgid "Today"
msgstr ""
-#: src/screens/Moderation/index.tsx:442
+#: src/screens/Moderation/index.tsx:445
msgid "Toggle to enable or disable adult content"
msgstr ""
@@ -13462,7 +13448,7 @@ msgstr ""
msgid "Unsupported clipboard content"
msgstr "Unsupported clipboard content"
-#: src/view/com/composer/Composer.tsx:1696
+#: src/view/com/composer/Composer.tsx:1662
msgid "Unsupported video type: {mimeType}"
msgstr ""
@@ -13555,7 +13541,7 @@ msgstr ""
msgid "Upload from Library"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2718
+#: src/view/com/composer/Composer.tsx:2684
msgid "Uploading GIF..."
msgstr ""
@@ -13569,7 +13555,7 @@ msgstr ""
msgid "Uploading link thumbnail..."
msgstr ""
-#: src/view/com/composer/Composer.tsx:2720
+#: src/view/com/composer/Composer.tsx:2686
msgid "Uploading video..."
msgstr ""
@@ -13706,7 +13692,7 @@ msgstr ""
msgid "Verification failed, please try again."
msgstr ""
-#: src/screens/Moderation/index.tsx:394
+#: src/screens/Moderation/index.tsx:400
msgid "Verification settings"
msgstr ""
@@ -13819,7 +13805,7 @@ msgstr "via starter pack <0/><1>{starterPackName}1>"
msgid "Video"
msgstr ""
-#: src/view/com/composer/state/video.ts:480
+#: src/view/com/composer/state/video.ts:474
msgid "Video failed to process"
msgstr ""
@@ -13858,7 +13844,7 @@ msgstr ""
msgid "Video settings"
msgstr ""
-#: src/view/com/composer/Composer.tsx:2738
+#: src/view/com/composer/Composer.tsx:2704
msgid "Video uploaded"
msgstr ""
@@ -13871,19 +13857,13 @@ msgstr ""
msgid "Videos"
msgstr ""
-#: src/view/com/composer/Composer.tsx:463
-#: src/view/com/composer/Composer.tsx:608
-#: src/view/com/composer/SelectMediaButton.tsx:455
+#: src/view/com/composer/Composer.tsx:454
+#: src/view/com/composer/Composer.tsx:586
+#: src/view/com/composer/SelectMediaButton.tsx:446
msgid "Videos must be 10 minutes or less."
msgstr "Videos must be 10 minutes or less."
-#: src/view/com/composer/Composer.tsx:464
-#: src/view/com/composer/Composer.tsx:609
-#: src/view/com/composer/SelectMediaButton.tsx:456
-msgid "Videos must be less than 3 minutes long."
-msgstr ""
-
-#: src/view/com/composer/Composer.tsx:1284
+#: src/view/com/composer/Composer.tsx:1250
msgctxt "Action to view the post the user just created"
msgid "View"
msgstr ""
@@ -13976,7 +13956,7 @@ msgstr ""
msgid "View more trending videos"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1279
+#: src/view/com/composer/Composer.tsx:1245
msgid "View post"
msgstr ""
@@ -14025,11 +14005,11 @@ msgstr ""
msgid "View video"
msgstr ""
-#: src/screens/Moderation/index.tsx:374
+#: src/screens/Moderation/index.tsx:380
msgid "View your blocked accounts"
msgstr ""
-#: src/screens/Moderation/index.tsx:314
+#: src/screens/Moderation/index.tsx:320
msgid "View your default post interaction settings"
msgstr ""
@@ -14038,15 +14018,15 @@ msgstr ""
msgid "View your feeds and explore more"
msgstr ""
-#: src/screens/Moderation/index.tsx:278
+#: src/screens/Moderation/index.tsx:286
msgid "View your moderation inbox"
msgstr "View your moderation inbox"
-#: src/screens/Moderation/index.tsx:344
+#: src/screens/Moderation/index.tsx:350
msgid "View your moderation lists"
msgstr ""
-#: src/screens/Moderation/index.tsx:359
+#: src/screens/Moderation/index.tsx:365
msgid "View your muted accounts"
msgstr ""
@@ -14189,7 +14169,7 @@ msgstr ""
msgid "We sent an email to <0>{0}0> containing a link. Please click on it to complete the email verification process."
msgstr ""
-#: src/view/com/composer/state/video.ts:527
+#: src/view/com/composer/state/video.ts:521
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
msgstr ""
@@ -14299,7 +14279,7 @@ msgstr "We’re sorry, but your search could not be completed. Please try again
msgid "We're sorry, you cannot access this screen at this time."
msgstr ""
-#: src/view/com/composer/Composer.tsx:1175
+#: src/view/com/composer/Composer.tsx:1141
msgid "We're sorry! The post you are replying to has been deleted."
msgstr ""
@@ -14355,7 +14335,7 @@ msgid "what’s up"
msgstr "what’s up"
#: src/view/com/auth/SplashScreen.web.tsx:99
-#: src/view/com/composer/Composer.tsx:1660
+#: src/view/com/composer/Composer.tsx:1626
#: src/view/com/feeds/ComposerPrompt.tsx:194
msgid "What's up?"
msgstr ""
@@ -14441,7 +14421,7 @@ msgstr "Would you like to block this user and/or leave this conversation?"
msgid "Would you like to save this as a draft before viewing your drafts?"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1575
+#: src/view/com/composer/Composer.tsx:1541
msgid "Would you like to save this as a draft to edit later?"
msgstr ""
@@ -14450,12 +14430,12 @@ msgstr ""
msgid "Write a post"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1756
+#: src/view/com/composer/Composer.tsx:1722
msgid "Write post"
msgstr ""
#: src/screens/PostThread/components/ThreadComposePrompt.tsx:91
-#: src/view/com/composer/Composer.tsx:1658
+#: src/view/com/composer/Composer.tsx:1624
msgid "Write your reply"
msgstr ""
@@ -14565,7 +14545,7 @@ msgstr ""
msgid "You are no longer live"
msgstr ""
-#: src/view/com/composer/state/video.ts:520
+#: src/view/com/composer/state/video.ts:514
msgid "You are not allowed to upload videos."
msgstr ""
@@ -14633,11 +14613,11 @@ msgid "You can now sign in with your new password."
msgstr ""
#. Toast shown when the user tries to add more images but the post gallery is already at the cap
-#: src/view/com/composer/Composer.tsx:224
+#: src/view/com/composer/Composer.tsx:223
msgid "You can only add up to {MAX_GALLERY_IMAGES, plural, other {# images}} per post"
msgstr "You can only add up to {MAX_GALLERY_IMAGES, plural, other {# images}} per post"
-#: src/view/com/composer/Composer.tsx:1580
+#: src/view/com/composer/Composer.tsx:1546
msgid "You can only save drafts up to 1000 characters."
msgstr ""
@@ -14645,11 +14625,11 @@ msgstr ""
msgid "You can only save drafts up to 1000 characters. Would you like to discard this post before viewing your drafts?"
msgstr ""
-#: src/view/com/composer/SelectMediaButton.tsx:458
+#: src/view/com/composer/SelectMediaButton.tsx:449
msgid "You can only select one GIF at a time."
msgstr ""
-#: src/view/com/composer/SelectMediaButton.tsx:452
+#: src/view/com/composer/SelectMediaButton.tsx:443
msgid "You can only select one video at a time."
msgstr ""
@@ -14662,7 +14642,7 @@ msgid "You can read chat history but can’t send new messages."
msgstr "You can read chat history but can’t send new messages."
#. Error message for maximum number of images that can be selected to add to a post.
-#: src/view/com/composer/SelectMediaButton.tsx:444
+#: src/view/com/composer/SelectMediaButton.tsx:435
msgid "You can select up to {MAX_GALLERY_IMAGES, plural, other {# images}} in total."
msgstr "You can select up to {MAX_GALLERY_IMAGES, plural, other {# images}} in total."
@@ -14782,7 +14762,7 @@ msgstr ""
msgid "You have temporarily reached the limit for video uploads. Please try again later."
msgstr ""
-#: src/view/com/composer/Composer.tsx:1570
+#: src/view/com/composer/Composer.tsx:1536
msgid "You have unsaved changes to this draft, would you like to save them?"
msgstr ""
@@ -14858,7 +14838,7 @@ msgstr ""
msgid "You must grant access to your photo library to save a QR code"
msgstr ""
-#: src/view/com/composer/SelectMediaButton.tsx:494
+#: src/view/com/composer/SelectMediaButton.tsx:478
msgid "You need to allow access to your media library."
msgstr ""
@@ -15018,7 +14998,7 @@ msgstr ""
msgid "You've reached the end of your feed! Find some more accounts to follow."
msgstr ""
-#: src/view/com/composer/Composer.tsx:773
+#: src/view/com/composer/Composer.tsx:739
msgid "You've reached the maximum number of drafts"
msgstr ""
@@ -15034,11 +15014,11 @@ msgstr "You’ve reached the maximum of {MAX_FILTERS, plural, one {# filter} oth
msgid "You've reached the start of the active content."
msgstr ""
-#: src/view/com/composer/state/video.ts:531
+#: src/view/com/composer/state/video.ts:525
msgid "You've reached your daily limit for video uploads (too many bytes)"
msgstr ""
-#: src/view/com/composer/state/video.ts:535
+#: src/view/com/composer/state/video.ts:529
msgid "You've reached your daily limit for video uploads (too many videos)"
msgstr ""
@@ -15062,7 +15042,7 @@ msgstr ""
msgid "Your account has been suspended"
msgstr ""
-#: src/view/com/composer/state/video.ts:539
+#: src/view/com/composer/state/video.ts:533
msgid "Your account is not yet old enough to upload videos. Please try again later."
msgstr ""
@@ -15115,7 +15095,7 @@ msgstr ""
msgid "Your current handle <0>{0}0> will automatically remain reserved for you. You can switch back to it at any time from this account."
msgstr ""
-#: src/screens/Moderation/index.tsx:261
+#: src/screens/Moderation/index.tsx:270
msgid "Your declared age is under 18. Some settings below may be disabled. If this was a mistake, you may edit your birthdate in your <0>account settings0>."
msgstr ""
@@ -15157,7 +15137,7 @@ msgstr ""
msgid "Your hosting provider can’t be detected from an email address, so the default Bluesky service will be used. Enter your username instead, or set your provider manually."
msgstr "Your hosting provider can’t be detected from an email address, so the default Bluesky service will be used. Enter your username instead, or set your provider manually."
-#: src/view/com/composer/state/video.ts:476
+#: src/view/com/composer/state/video.ts:470
msgid "Your hosting provider does not support videos this large. Please try again with a smaller file."
msgstr "Your hosting provider does not support videos this large. Please try again with a smaller file."
@@ -15202,11 +15182,11 @@ msgstr ""
msgid "Your password must be at least 8 characters long."
msgstr ""
-#: src/view/com/composer/Composer.tsx:1275
+#: src/view/com/composer/Composer.tsx:1241
msgid "Your post was sent"
msgstr ""
-#: src/view/com/composer/Composer.tsx:1272
+#: src/view/com/composer/Composer.tsx:1238
msgid "Your posts were sent"
msgstr ""
@@ -15227,7 +15207,7 @@ msgstr ""
msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in."
msgstr ""
-#: src/view/com/composer/Composer.tsx:1274
+#: src/view/com/composer/Composer.tsx:1240
msgid "Your reply was sent"
msgstr ""
@@ -15240,7 +15220,7 @@ msgstr ""
msgid "Your selected interests help us serve you content you care about."
msgstr ""
-#: src/view/com/composer/Composer.tsx:1605
+#: src/view/com/composer/Composer.tsx:1571
msgid "Your thread has empty posts that will be skipped. The remaining posts will be published as a thread."
msgstr "Your thread has empty posts that will be skipped. The remaining posts will be published as a thread."
From 8df64fcac18f913078d42b06061d9ce602cd9bbf Mon Sep 17 00:00:00 2001
From: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Date: Wed, 26 Aug 2026 06:35:35 -0700
Subject: [PATCH 15/22] Fix site initialization in older versions of Safari
(#11553)
Co-authored-by: Samuel Newman
---
oxlint-suppressions.json | 11 +++++++++++
webpack.config.js | 6 +++++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json
index b993e7875c..bef12f4397 100644
--- a/oxlint-suppressions.json
+++ b/oxlint-suppressions.json
@@ -1584,5 +1584,16 @@
"typescript/no-unsafe-member-access": {
"count": 3
}
+ },
+ "webpack.config.js": {
+ "import/no-nodejs-modules": {
+ "count": 1
+ },
+ "typescript/no-unsafe-call": {
+ "count": 12
+ },
+ "typescript/no-unsafe-member-access": {
+ "count": 33
+ }
}
}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index 24cfb2097a..6613692cef 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -46,7 +46,11 @@ function patchSourceMapFilter(rules, pathPattern) {
module.exports = async function (env, argv) {
env.babel = {
- dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo'],
+ dangerouslyAddModulePathsToTranspile: [
+ // this covers every package that starts with these strings (e.g. @atproto/lex-client)
+ '@bsky.app/expo',
+ '@atproto/lex',
+ ],
}
let config = await createExpoWebpackConfigAsync(env, argv)
/*
From 057f97ce115885a6975c0475330f31cdcb440ceb Mon Sep 17 00:00:00 2001
From: Samuel Newman
Date: Wed, 26 Aug 2026 16:57:58 +0300
Subject: [PATCH 16/22] Fix `URL.canParse` in older browsers (#11557)
---
package.json | 8 +--
pnpm-lock.yaml | 148 ++++++++++++++++++++++++-------------------------
2 files changed, 78 insertions(+), 78 deletions(-)
diff --git a/package.json b/package.json
index 861f9fa5f7..8e61af5d7e 100644
--- a/package.json
+++ b/package.json
@@ -99,10 +99,10 @@
"prettier": "prettier --check ."
},
"dependencies": {
- "@atproto/common-web": "0.5.9",
- "@atproto/lex": "0.3.6",
- "@atproto/lex-password-session": "0.2.0",
- "@atproto/syntax": "0.7.4",
+ "@atproto/common-web": "0.5.10",
+ "@atproto/lex": "0.3.7",
+ "@atproto/lex-password-session": "0.2.1",
+ "@atproto/syntax": "0.7.5",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.15",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 86133a02ef..8398c83103 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -243,17 +243,17 @@ importers:
.:
dependencies:
'@atproto/common-web':
- specifier: 0.5.9
- version: 0.5.9
+ specifier: 0.5.10
+ version: 0.5.10
'@atproto/lex':
- specifier: 0.3.6
- version: 0.3.6
+ specifier: 0.3.7
+ version: 0.3.7
'@atproto/lex-password-session':
- specifier: 0.2.0
- version: 0.2.0
+ specifier: 0.2.1
+ version: 0.2.1
'@atproto/syntax':
- specifier: 0.7.4
- version: 0.7.4
+ specifier: 0.7.5
+ version: 0.7.5
'@bitdrift/react-native':
specifier: ^0.6.8
version: 0.6.14(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)
@@ -298,7 +298,7 @@ importers:
version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)
'@bsky/sdk':
specifier: 1.0.1
- version: 1.0.1(@atproto/lex@0.3.6)
+ version: 1.0.1(@atproto/lex@0.3.7)
'@emoji-mart/data':
specifier: ^1.2.1
version: 1.2.1
@@ -897,12 +897,12 @@ packages:
resolution: {integrity: sha512-vfvoDhu6ds6BT3Pqe+d2/LBU1WpDRtt69y6zltlfMCoucPm82m1j50/Wb6xTvv+oZ+2j0JyZVXsmXQ12SWYQJg==}
engines: {node: '>=22'}
- '@atproto/common-web@0.5.9':
- resolution: {integrity: sha512-2c5C6YV8352JJz9Z5fNfcsstllaKGAb3cQW9aO7unMAGh/FzTGFq+xOwNMW2lTeagO6z530uxYm8AiSRM6O+DQ==}
+ '@atproto/common-web@0.5.10':
+ resolution: {integrity: sha512-w4JUdsJ3VXt8ewkavYh5m/u0UbxDtFdCKhNZPEpJ0U1vcWIjDaSInIexteETDgD7fBJAhidIEi30MGz1kk49ug==}
engines: {node: '>=22'}
- '@atproto/common@0.8.0':
- resolution: {integrity: sha512-fBtd08LdouLqNaL8SaGLEIa0l3Ue27yR810jhUu+iBv/xdoXp+1G0pxvPe6JUStH7YGtf/v/v0w/cRGh1xUOaA==}
+ '@atproto/common@0.8.1':
+ resolution: {integrity: sha512-WvgY0CDgodmTS7B1KDVFt868h1b85iQtQZeoDVI+6oOZmg1jINWUl+bZ1rwf59Tk1PqcMz5OzH6OS2J7Mb9/Iw==}
engines: {node: '>=22'}
'@atproto/crypto@0.5.4':
@@ -913,57 +913,57 @@ packages:
resolution: {integrity: sha512-BlnwQ+obL+4ZA71KH/EzZ3TY+cpSxnLiUI85mjBJIQwvDF/oN2sQA18wIj9jpduviIt2b/cMtlJuzHjzBkfXvw==}
engines: {node: '>=22'}
- '@atproto/lex-builder@0.1.12':
- resolution: {integrity: sha512-uo3mcdoOgqBfQnf+PhYYLNEBdUTgoNe2ARYb3cUaiec8O0oritOeZndUDgmths06ACrlqFkZNTV1YJX6CsH71g==}
+ '@atproto/lex-builder@0.1.13':
+ resolution: {integrity: sha512-YHSONY88fWdMcN6Xa/Noap98DNW2m5zVZGspQWaROsX2lFx1mLf7g/XKLBcPRVDpE3OWRZ98QkkSZN3SgbOXrg==}
engines: {node: '>=22'}
'@atproto/lex-cbor@0.1.6':
resolution: {integrity: sha512-PxLQy7jzV8u9zTGURNF8ZczLPk+ZH+WNx/2z/z7RacJbu7sWNpx26U2pC7cIkWtt4jyDaruaLhssw3w3atLotA==}
engines: {node: '>=22'}
- '@atproto/lex-client@0.3.3':
- resolution: {integrity: sha512-sWChe7Jukm9uxDfxT6ElICp6vMTqAFmwcHdSLdMDtsKBZSrwO7vWL9F6rpoMPyxS29Ob9nNAhMg30kKfZrb+uw==}
+ '@atproto/lex-client@0.3.4':
+ resolution: {integrity: sha512-0U0SsgrpJaKsRRoKU0TelWnP3f8OSj+a1+RdYaSf92mhxCMPmsz0CfURltpNbgIDR1yYHoh8tfFvAbMIyKw8jw==}
engines: {node: '>=22'}
'@atproto/lex-data@0.1.7':
resolution: {integrity: sha512-kW/dPLqo/WgCLV+XESR4JKwV6c1rZWJGOfuPupZGTjEDAKoBbKXdaEzX9/1vKQYbZ9U3j0DS/n7OFFK7wBugyQ==}
engines: {node: '>=22'}
- '@atproto/lex-document@0.1.9':
- resolution: {integrity: sha512-TmpwttQ9pKCXlvwUlJ/5HMZPtJMFBvgZ1bunEeQD5JfU6n/sgK/fob8tfjhTdL2QrpZMtM6opMEk3w5STm1RDQ==}
+ '@atproto/lex-document@0.1.10':
+ resolution: {integrity: sha512-7ivrxWttSxInDksR2rtM24xGdntY/3Ew5j08W5SiG45B+T2nyARL5EOk2xMXEm3MLjPrpNYAybA21ZEDYhGG0Q==}
engines: {node: '>=22'}
- '@atproto/lex-installer@0.1.15':
- resolution: {integrity: sha512-4/xDyY7yxxs66vF0WZB+7fcuEy4I5IRDyDoQFzLQx7C6PlQ76/e/PxvYTff7x0/N6hknmOlANODnnxo3rtdnCA==}
+ '@atproto/lex-installer@0.1.16':
+ resolution: {integrity: sha512-ascVCVvGMZtEErZ/2sNXU5f2InOHRTPNv1f01ovon5LP6J3bwXadrC78wci1kz/KNs3Y6c27Ymu1IaCddRCn2g==}
engines: {node: '>=22'}
'@atproto/lex-json@0.1.6':
resolution: {integrity: sha512-mvrAd0lbyuecIHjyld8QN6MN6CBf4j0GCxLzegsvLh0SvDf+GbYWklkcQqmITL44yFQOwmA/QNIQj0Uvh7+R/g==}
engines: {node: '>=22'}
- '@atproto/lex-password-session@0.2.0':
- resolution: {integrity: sha512-u958Etax/bPVr9FWko+/B5nRcU1klQHvrdiVtYb4lkgrM6uvV7BvaFJZYEu5DzlEeqNAZ9a65PmbC1F5oyUBjQ==}
+ '@atproto/lex-password-session@0.2.1':
+ resolution: {integrity: sha512-6f2+y5D+Vmw+bfjWip8QhallBqdP6ZKpdM6B2txqFbgOcwr2eaxK5sO+wLiyw4wlltudwOTaOvVoiMIeIRF1Cg==}
engines: {node: '>=22'}
- '@atproto/lex-resolver@0.2.8':
- resolution: {integrity: sha512-j3dIxeEAStp/RzJS4MECRDPuOFX41mX9fnteGgrQhe9053eRuTnl2oTyJbV7rKgzy+hEI4jH6X6Y4wOStjUb/Q==}
+ '@atproto/lex-resolver@0.2.9':
+ resolution: {integrity: sha512-W2tu6KuaSo796/K7O3ueNjMg/RvKjPf4eJMAIucyAcQXymNmnw+uk/qUt7BQTUPTFlw0CVgyq1cCAg8AP7HFuA==}
engines: {node: '>=22'}
- '@atproto/lex-schema@0.2.5':
- resolution: {integrity: sha512-/tP3dRqaZu0EIoq0BSVzhvFGL6u0uu2KFDV21i73DpFsg6TXOTddKJ9bvgK83XdAJYL7PWi703XsaR31v9u5Qg==}
+ '@atproto/lex-schema@0.2.6':
+ resolution: {integrity: sha512-8fe/gmhjMImBsy073jMANkbHHQLe/w4s9XLATpAz+Qd6WrR9RYTqc2DU8uZDL13F5bfdGTj8KECt/Xs4CoPXdQ==}
engines: {node: '>=22'}
- '@atproto/lex@0.3.6':
- resolution: {integrity: sha512-8Hpc6MbfkPJKaO5CvvIerEylbihI5GfrY5DzapZD4rV2TWfm3RU+DzeRpsxQJ0W9WOZRYvp1JnBcq/gZ48Qb0g==}
+ '@atproto/lex@0.3.7':
+ resolution: {integrity: sha512-k/+snIDoefYbjRdv+hDioo9wPVLzbS5bwhSU80qY2rgsyuN74tNwHGQslnQekCCKv85QHm9O/sD7qJDnhllySQ==}
engines: {node: '>=22'}
hasBin: true
- '@atproto/repo@0.10.11':
- resolution: {integrity: sha512-QqDzEv8d6NUuqZ0xvWG+n8DfWd4Vuka04nyuD9oglKsj5avB1zbEsWx3Oit1oYtZFUf9JSVWMDqMXhu3UbB+lA==}
+ '@atproto/repo@0.10.12':
+ resolution: {integrity: sha512-SnDSoFi1bRAfN0IcDjSPFcefknDCIIjKgJXgFsd5jvktCkopmzml8BpQEP5t2/mcZ7NvEn5onQ0kaWkXhgL+5g==}
engines: {node: '>=22'}
- '@atproto/syntax@0.7.4':
- resolution: {integrity: sha512-EHsEHtasH/DGPljBYecVDjweGMQ5eTu6Ns0GZ5z0qdqpOI8ipBvldWnMIxWkA+5HfZ9Dsu4V1MX0WP7wgL8cuA==}
+ '@atproto/syntax@0.7.5':
+ resolution: {integrity: sha512-6vnLQK8OAzg0dO6z/xnvuXn5zMV0UMI54zbxk7G7BXhGlIlLMb1yo+JVYtAlv8Nxzr+AaFdNZ8AJt+L/QhJMFQ==}
engines: {node: '>=22'}
'@babel/code-frame@7.10.4':
@@ -9686,16 +9686,16 @@ snapshots:
'@atproto-labs/simple-store@0.5.1': {}
- '@atproto/common-web@0.5.9':
+ '@atproto/common-web@0.5.10':
dependencies:
'@atproto/lex-data': 0.1.7
'@atproto/lex-json': 0.1.6
- '@atproto/syntax': 0.7.4
+ '@atproto/syntax': 0.7.5
zod: 3.25.76
- '@atproto/common@0.8.0':
+ '@atproto/common@0.8.1':
dependencies:
- '@atproto/common-web': 0.5.9
+ '@atproto/common-web': 0.5.10
'@atproto/lex-cbor': 0.1.6
'@atproto/lex-data': 0.1.7
multiformats: 13.4.2
@@ -9711,10 +9711,10 @@ snapshots:
dependencies:
zod: 3.25.76
- '@atproto/lex-builder@0.1.12':
+ '@atproto/lex-builder@0.1.13':
dependencies:
- '@atproto/lex-document': 0.1.9
- '@atproto/lex-schema': 0.2.5
+ '@atproto/lex-document': 0.1.10
+ '@atproto/lex-schema': 0.2.6
prettier: 3.9.6
ts-morph: 27.0.2
tslib: 2.8.1
@@ -9725,11 +9725,11 @@ snapshots:
cborg: 4.5.8
tslib: 2.8.1
- '@atproto/lex-client@0.3.3':
+ '@atproto/lex-client@0.3.4':
dependencies:
'@atproto/lex-data': 0.1.7
'@atproto/lex-json': 0.1.6
- '@atproto/lex-schema': 0.2.5
+ '@atproto/lex-schema': 0.2.6
tslib: 2.8.1
'@atproto/lex-data@0.1.7':
@@ -9738,21 +9738,21 @@ snapshots:
tslib: 2.8.1
unicode-segmenter: 0.14.5
- '@atproto/lex-document@0.1.9':
+ '@atproto/lex-document@0.1.10':
dependencies:
- '@atproto/lex-schema': 0.2.5
+ '@atproto/lex-schema': 0.2.6
core-js: 3.50.0
tslib: 2.8.1
- '@atproto/lex-installer@0.1.15':
+ '@atproto/lex-installer@0.1.16':
dependencies:
- '@atproto/lex-builder': 0.1.12
+ '@atproto/lex-builder': 0.1.13
'@atproto/lex-cbor': 0.1.6
'@atproto/lex-data': 0.1.7
- '@atproto/lex-document': 0.1.9
- '@atproto/lex-resolver': 0.2.8
- '@atproto/lex-schema': 0.2.5
- '@atproto/syntax': 0.7.4
+ '@atproto/lex-document': 0.1.10
+ '@atproto/lex-resolver': 0.2.9
+ '@atproto/lex-schema': 0.2.6
+ '@atproto/syntax': 0.7.5
tslib: 2.8.1
'@atproto/lex-json@0.1.6':
@@ -9760,54 +9760,54 @@ snapshots:
'@atproto/lex-data': 0.1.7
tslib: 2.8.1
- '@atproto/lex-password-session@0.2.0':
+ '@atproto/lex-password-session@0.2.1':
dependencies:
- '@atproto/lex-client': 0.3.3
- '@atproto/lex-schema': 0.2.5
+ '@atproto/lex-client': 0.3.4
+ '@atproto/lex-schema': 0.2.6
tslib: 2.8.1
- '@atproto/lex-resolver@0.2.8':
+ '@atproto/lex-resolver@0.2.9':
dependencies:
'@atproto-labs/did-resolver': 0.3.7
'@atproto/crypto': 0.5.4
- '@atproto/lex-client': 0.3.3
+ '@atproto/lex-client': 0.3.4
'@atproto/lex-data': 0.1.7
- '@atproto/lex-document': 0.1.9
- '@atproto/lex-schema': 0.2.5
- '@atproto/repo': 0.10.11
- '@atproto/syntax': 0.7.4
+ '@atproto/lex-document': 0.1.10
+ '@atproto/lex-schema': 0.2.6
+ '@atproto/repo': 0.10.12
+ '@atproto/syntax': 0.7.5
tslib: 2.8.1
- '@atproto/lex-schema@0.2.5':
+ '@atproto/lex-schema@0.2.6':
dependencies:
'@atproto/lex-data': 0.1.7
- '@atproto/syntax': 0.7.4
+ '@atproto/syntax': 0.7.5
'@standard-schema/spec': 1.1.0
tslib: 2.8.1
- '@atproto/lex@0.3.6':
+ '@atproto/lex@0.3.7':
dependencies:
- '@atproto/lex-builder': 0.1.12
- '@atproto/lex-client': 0.3.3
+ '@atproto/lex-builder': 0.1.13
+ '@atproto/lex-client': 0.3.4
'@atproto/lex-data': 0.1.7
- '@atproto/lex-installer': 0.1.15
+ '@atproto/lex-installer': 0.1.16
'@atproto/lex-json': 0.1.6
- '@atproto/lex-schema': 0.2.5
+ '@atproto/lex-schema': 0.2.6
tslib: 2.8.1
yargs: 18.1.0
- '@atproto/repo@0.10.11':
+ '@atproto/repo@0.10.12':
dependencies:
- '@atproto/common': 0.8.0
- '@atproto/common-web': 0.5.9
+ '@atproto/common': 0.8.1
+ '@atproto/common-web': 0.5.10
'@atproto/crypto': 0.5.4
'@atproto/lex-cbor': 0.1.6
'@atproto/lex-data': 0.1.7
- '@atproto/syntax': 0.7.4
+ '@atproto/syntax': 0.7.5
varint: 6.0.0
zod: 3.25.76
- '@atproto/syntax@0.7.4':
+ '@atproto/syntax@0.7.5':
dependencies:
iso-datestring-validator: 2.2.2
tslib: 2.8.1
@@ -10770,11 +10770,11 @@ snapshots:
react: 19.2.3
react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)
- '@bsky/sdk@1.0.1(@atproto/lex@0.3.6)':
+ '@bsky/sdk@1.0.1(@atproto/lex@0.3.7)':
dependencies:
'@atproto-labs/handle-resolver': 0.4.8
- '@atproto/lex': 0.3.6
- '@atproto/syntax': 0.7.4
+ '@atproto/lex': 0.3.7
+ '@atproto/syntax': 0.7.5
tlds: 1.261.0
'@crowdin/cli@4.14.2':
From c44f329b54de3913c641596db93c4e18a8d081f4 Mon Sep 17 00:00:00 2001
From: Michael Black
Date: Wed, 26 Aug 2026 10:03:43 -0500
Subject: [PATCH 17/22] Update video embed lexicon (#11559)
---
lexicons.json | 2 +-
lexicons/app/bsky/embed/video.json | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/lexicons.json b/lexicons.json
index 08f35c1eaf..ca0e64f570 100644
--- a/lexicons.json
+++ b/lexicons.json
@@ -426,7 +426,7 @@
},
"app.bsky.embed.video": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.video",
- "cid": "bafyreie3nug4ezpwodl6yrpgv5edkazzn22t7ea4yaeuun4rctyekkngai"
+ "cid": "bafyreiaqos23yv3t4ptrxily6s6qea5fcxfjzjlm42zq46xweby2mkgr4m"
},
"app.bsky.feed.defs": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.defs",
diff --git a/lexicons/app/bsky/embed/video.json b/lexicons/app/bsky/embed/video.json
index 67278da9c2..5033261982 100644
--- a/lexicons/app/bsky/embed/video.json
+++ b/lexicons/app/bsky/embed/video.json
@@ -18,8 +18,8 @@
"accept": [
"video/mp4"
],
- "maxSize": 100000000,
- "description": "The mp4 video file. May be up to 100mb, formerly limited to 50mb."
+ "maxSize": 300000000,
+ "description": "The mp4 video file. May be up to 300mb, formerly limited to 100mb."
},
"captions": {
"type": "array",
From 59082016e9dfb999d13bfafd2145f0edc0ee3b91 Mon Sep 17 00:00:00 2001
From: Spence Pope
Date: Wed, 26 Aug 2026 11:45:28 -0400
Subject: [PATCH 18/22] Update algorithmic recommendations helper text (#11546)
---
src/screens/Settings/components/AlgoVisibilityOptOut.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/screens/Settings/components/AlgoVisibilityOptOut.tsx b/src/screens/Settings/components/AlgoVisibilityOptOut.tsx
index c512905749..e7618395ad 100644
--- a/src/screens/Settings/components/AlgoVisibilityOptOut.tsx
+++ b/src/screens/Settings/components/AlgoVisibilityOptOut.tsx
@@ -41,9 +41,9 @@ export function AlgoVisibilityOptOut() {
- Bluesky will not show your posts in the Discover feed (except to your
- followers) and will ask other apps not to show your posts in their own
- algorithmic recommendations.
+ On Bluesky, this means your posts will only appear in the Discover
+ feed to people who follow you. Other apps can choose to use this
+ preference in their own algorithmic recommendations.
From ed4126f5a222acee5f85492e2b48b08955fad462 Mon Sep 17 00:00:00 2001
From: mozzius <10959775+mozzius@users.noreply.github.com>
Date: Wed, 26 Aug 2026 15:48:46 +0000
Subject: [PATCH 19/22] Nightly source-language update
---
src/locale/locales/en/messages.po | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po
index 486b9bcfd5..231135b3e9 100644
--- a/src/locale/locales/en/messages.po
+++ b/src/locale/locales/en/messages.po
@@ -2359,10 +2359,6 @@ msgstr ""
msgid "Bluesky will not show your account to logged-out users and will ask other apps to do the same. Other apps may not honor this request. It doesn't make your account private."
msgstr "Bluesky will not show your account to logged-out users and will ask other apps to do the same. Other apps may not honor this request. It doesn't make your account private."
-#: src/screens/Settings/components/AlgoVisibilityOptOut.tsx:43
-msgid "Bluesky will not show your posts in the Discover feed (except to your followers) and will ask other apps not to show your posts in their own algorithmic recommendations."
-msgstr "Bluesky will not show your posts in the Discover feed (except to your followers) and will ask other apps not to show your posts in their own algorithmic recommendations."
-
#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:136
msgid "Bluesky will proactively verify notable and authentic accounts."
msgstr ""
@@ -8482,6 +8478,10 @@ msgstr ""
msgid "On"
msgstr "On"
+#: src/screens/Settings/components/AlgoVisibilityOptOut.tsx:43
+msgid "On Bluesky, this means your posts will only appear in the Discover feed to people who follow you. Other apps can choose to use this preference in their own algorithmic recommendations."
+msgstr "On Bluesky, this means your posts will only appear in the Discover feed to people who follow you. Other apps can choose to use this preference in their own algorithmic recommendations."
+
#: src/components/StarterPack/QrCode.tsx:78
msgid "on<0><1/><2><3/>2>0>"
msgstr ""
From a0ed2da0060328dadfed360c9ce22c6a285f0e90 Mon Sep 17 00:00:00 2001
From: Samuel Newman
Date: Wed, 26 Aug 2026 20:13:51 +0300
Subject: [PATCH 20/22] Fix starter pack navigation from search and explore
(#11563)
---
src/screens/StarterPack/StarterPackScreen.tsx | 5 +-
src/types/bsky/__tests__/starterPack.test.ts | 51 ++++++++++++++++---
src/types/bsky/starterPack.ts | 13 +++++
3 files changed, 59 insertions(+), 10 deletions(-)
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 2043ae2e7d..e73bab09ce 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -147,10 +147,7 @@ export function StarterPackScreenInner({
const isValid =
starterPack &&
(starterPack.list || starterPack?.creator?.did === currentAccount?.did) &&
- // Cards may precache a synthetic full view while navigating. Its list CID
- // is intentionally empty until the server response replaces it, so use
- // the trusted app-view discriminator here instead of strict validation.
- bsky.isType(app.bsky.graph.defs.starterPackView, starterPack) &&
+ bsky.starterPack.isTrustedView(starterPack) &&
bsky.matches(app.bsky.graph.starterpack, starterPack.record)
if (!did || !starterPack || !isValid || !moderationOpts) {
diff --git a/src/types/bsky/__tests__/starterPack.test.ts b/src/types/bsky/__tests__/starterPack.test.ts
index 161de2a867..c679abe600 100644
--- a/src/types/bsky/__tests__/starterPack.test.ts
+++ b/src/types/bsky/__tests__/starterPack.test.ts
@@ -1,7 +1,11 @@
-import {type app} from '#/lexicons'
+/* Full schema matching exercises CID validation, so use the real CID parser. */
+jest.unmock('multiformats/cid')
+
+import {app} from '#/lexicons'
import {
type AnyStarterPackView,
isBasicView,
+ isTrustedView,
isView,
} from '#/types/bsky/starterPack'
@@ -9,15 +13,21 @@ const now = () => new Date().toISOString()
const creator = {
$type: 'app.bsky.actor.defs#profileViewBasic',
- did: 'did:plc:abc',
- handle: 'alice.test',
+ did: 'did:plc:qrllvid7s54k4hnwtqxwetrf',
+ handle: 'joshuajfriedman.com',
}
const basicView = {
$type: 'app.bsky.graph.defs#starterPackViewBasic',
- uri: 'at://did:plc:abc/app.bsky.graph.starterpack/123',
- cid: 'bafypack',
- record: {},
+ uri: 'at://did:plc:qrllvid7s54k4hnwtqxwetrf/app.bsky.graph.starterpack/3l4poszxde32k',
+ cid: 'bafyreiaxduxpwdpjgvve3klfs4flkwjwfqiurszw4o6jvjarpqqmeqwiza',
+ record: {
+ $type: 'app.bsky.graph.starterpack',
+ createdAt: '2024-09-22T03:52:03.686Z',
+ feeds: [],
+ list: 'at://did:plc:qrllvid7s54k4hnwtqxwetrf/app.bsky.graph.list/3l4posztwzy2e',
+ name: 'Bluesky for Art History',
+ },
creator,
indexedAt: now(),
}
@@ -27,6 +37,18 @@ const fullView = {
$type: 'app.bsky.graph.defs#starterPackView',
}
+const {$type: _, ...directFullView} = fullView
+
+const syntheticFullView = {
+ ...fullView,
+ list: {
+ uri: 'at://did:plc:abc/app.bsky.graph.list/123',
+ cid: '',
+ name: 'Starter pack',
+ purpose: 'app.bsky.graph.defs#referencelist',
+ },
+}
+
/*
* Type-level assertions for the view alias: it must accept both the basic and
* the full starter pack view. Compile-time only - a failure surfaces as a
@@ -76,6 +98,23 @@ describe('types/bsky/starterPack guards', () => {
})
})
+ describe('isTrustedView', () => {
+ it('accepts a direct full view with an omitted $type', () => {
+ expect(isTrustedView(directFullView)).toBe(true)
+ })
+
+ it('accepts a typed synthetic view with placeholder fields', () => {
+ expect(
+ app.bsky.graph.defs.starterPackView.matches(syntheticFullView),
+ ).toBe(false)
+ expect(isTrustedView(syntheticFullView)).toBe(true)
+ })
+
+ it('rejects the basic view', () => {
+ expect(isTrustedView(basicView)).toBe(false)
+ })
+ })
+
it('narrows a view from either world to a readable shape', () => {
/*
* The `$type` string is world-independent, so one guard narrows values from
diff --git a/src/types/bsky/starterPack.ts b/src/types/bsky/starterPack.ts
index 594fd40c89..b6894287e5 100644
--- a/src/types/bsky/starterPack.ts
+++ b/src/types/bsky/starterPack.ts
@@ -25,6 +25,19 @@ export function isView(v: unknown): v is app.bsky.graph.defs.StarterPackView {
)
}
+/**
+ * Accepts both forms of a full starter pack view used by the app:
+ *
+ * - direct lexicon refs returned by the app view, where `$type` may be omitted
+ * - trusted synthetic cache entries, which carry `$type` but may contain
+ * placeholder fields that do not yet pass full schema validation
+ */
+export function isTrustedView(
+ v: unknown,
+): v is app.bsky.graph.defs.StarterPackView {
+ return isView(v) || app.bsky.graph.defs.starterPackView.matches(v)
+}
+
/**
* Matches any starter pack view exported by our SDK.
*/
From 206a932c5d2e8ef5acdfaafd8a153d9e17ef983e Mon Sep 17 00:00:00 2001
From: Samuel Newman
Date: Wed, 26 Aug 2026 21:46:32 +0300
Subject: [PATCH 21/22] Fix auto handle resolution during sign in (#11567)
---
package.json | 4 ++--
pnpm-lock.yaml | 54 +++++++++++++++++++++++++-------------------------
2 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/package.json b/package.json
index 8e61af5d7e..1ac7f45e11 100644
--- a/package.json
+++ b/package.json
@@ -100,8 +100,8 @@
},
"dependencies": {
"@atproto/common-web": "0.5.10",
- "@atproto/lex": "0.3.7",
- "@atproto/lex-password-session": "0.2.1",
+ "@atproto/lex": "0.3.8",
+ "@atproto/lex-password-session": "0.2.2",
"@atproto/syntax": "0.7.5",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8398c83103..9af9df2865 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -246,11 +246,11 @@ importers:
specifier: 0.5.10
version: 0.5.10
'@atproto/lex':
- specifier: 0.3.7
- version: 0.3.7
+ specifier: 0.3.8
+ version: 0.3.8
'@atproto/lex-password-session':
- specifier: 0.2.1
- version: 0.2.1
+ specifier: 0.2.2
+ version: 0.2.2
'@atproto/syntax':
specifier: 0.7.5
version: 0.7.5
@@ -298,7 +298,7 @@ importers:
version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)
'@bsky/sdk':
specifier: 1.0.1
- version: 1.0.1(@atproto/lex@0.3.7)
+ version: 1.0.1(@atproto/lex@0.3.8)
'@emoji-mart/data':
specifier: ^1.2.1
version: 1.2.1
@@ -921,8 +921,8 @@ packages:
resolution: {integrity: sha512-PxLQy7jzV8u9zTGURNF8ZczLPk+ZH+WNx/2z/z7RacJbu7sWNpx26U2pC7cIkWtt4jyDaruaLhssw3w3atLotA==}
engines: {node: '>=22'}
- '@atproto/lex-client@0.3.4':
- resolution: {integrity: sha512-0U0SsgrpJaKsRRoKU0TelWnP3f8OSj+a1+RdYaSf92mhxCMPmsz0CfURltpNbgIDR1yYHoh8tfFvAbMIyKw8jw==}
+ '@atproto/lex-client@0.3.5':
+ resolution: {integrity: sha512-IOYjgQ2H+pw8QhV0vMMG9PTRg+oh5In9APbGBvotNZlbF4SCZNDVd7dONFY4rU2Dg6BHSkiqbqoNUHNgTC+ITA==}
engines: {node: '>=22'}
'@atproto/lex-data@0.1.7':
@@ -933,28 +933,28 @@ packages:
resolution: {integrity: sha512-7ivrxWttSxInDksR2rtM24xGdntY/3Ew5j08W5SiG45B+T2nyARL5EOk2xMXEm3MLjPrpNYAybA21ZEDYhGG0Q==}
engines: {node: '>=22'}
- '@atproto/lex-installer@0.1.16':
- resolution: {integrity: sha512-ascVCVvGMZtEErZ/2sNXU5f2InOHRTPNv1f01ovon5LP6J3bwXadrC78wci1kz/KNs3Y6c27Ymu1IaCddRCn2g==}
+ '@atproto/lex-installer@0.1.17':
+ resolution: {integrity: sha512-XSXiFLoDT7YEe+GuUdwczKQmhuLrA2CHVVlL8s5U8FLtWC5Cr3ELmz1t1b/Y8dV+R31hWyvdsk/t992B6+WW3Q==}
engines: {node: '>=22'}
'@atproto/lex-json@0.1.6':
resolution: {integrity: sha512-mvrAd0lbyuecIHjyld8QN6MN6CBf4j0GCxLzegsvLh0SvDf+GbYWklkcQqmITL44yFQOwmA/QNIQj0Uvh7+R/g==}
engines: {node: '>=22'}
- '@atproto/lex-password-session@0.2.1':
- resolution: {integrity: sha512-6f2+y5D+Vmw+bfjWip8QhallBqdP6ZKpdM6B2txqFbgOcwr2eaxK5sO+wLiyw4wlltudwOTaOvVoiMIeIRF1Cg==}
+ '@atproto/lex-password-session@0.2.2':
+ resolution: {integrity: sha512-lifxp9sMdOGP1AfPhjNUMson6yzqEbS0PS8Q7UCVQucXbBlM89r5ff9DMBJF2sTiH5Qt8yXXsthI543OT9FXgg==}
engines: {node: '>=22'}
- '@atproto/lex-resolver@0.2.9':
- resolution: {integrity: sha512-W2tu6KuaSo796/K7O3ueNjMg/RvKjPf4eJMAIucyAcQXymNmnw+uk/qUt7BQTUPTFlw0CVgyq1cCAg8AP7HFuA==}
+ '@atproto/lex-resolver@0.2.10':
+ resolution: {integrity: sha512-0Ml37emPqi6zr7hBAVR8fi7XDgG6xWsksBotoMU4Doh/PNFNt/H79Zopt/jnmDRcl8DGj9PNqZOZjzcAr2IuFw==}
engines: {node: '>=22'}
'@atproto/lex-schema@0.2.6':
resolution: {integrity: sha512-8fe/gmhjMImBsy073jMANkbHHQLe/w4s9XLATpAz+Qd6WrR9RYTqc2DU8uZDL13F5bfdGTj8KECt/Xs4CoPXdQ==}
engines: {node: '>=22'}
- '@atproto/lex@0.3.7':
- resolution: {integrity: sha512-k/+snIDoefYbjRdv+hDioo9wPVLzbS5bwhSU80qY2rgsyuN74tNwHGQslnQekCCKv85QHm9O/sD7qJDnhllySQ==}
+ '@atproto/lex@0.3.8':
+ resolution: {integrity: sha512-8X7f6wxVDm4IVNR80Yg3BV3IXb89sKo/KIZeYb4or/EHP7ljazVU4qU+RPXmlELnWiIcw+PiJE+hPSkGLnMPOA==}
engines: {node: '>=22'}
hasBin: true
@@ -9725,7 +9725,7 @@ snapshots:
cborg: 4.5.8
tslib: 2.8.1
- '@atproto/lex-client@0.3.4':
+ '@atproto/lex-client@0.3.5':
dependencies:
'@atproto/lex-data': 0.1.7
'@atproto/lex-json': 0.1.6
@@ -9744,13 +9744,13 @@ snapshots:
core-js: 3.50.0
tslib: 2.8.1
- '@atproto/lex-installer@0.1.16':
+ '@atproto/lex-installer@0.1.17':
dependencies:
'@atproto/lex-builder': 0.1.13
'@atproto/lex-cbor': 0.1.6
'@atproto/lex-data': 0.1.7
'@atproto/lex-document': 0.1.10
- '@atproto/lex-resolver': 0.2.9
+ '@atproto/lex-resolver': 0.2.10
'@atproto/lex-schema': 0.2.6
'@atproto/syntax': 0.7.5
tslib: 2.8.1
@@ -9760,17 +9760,17 @@ snapshots:
'@atproto/lex-data': 0.1.7
tslib: 2.8.1
- '@atproto/lex-password-session@0.2.1':
+ '@atproto/lex-password-session@0.2.2':
dependencies:
- '@atproto/lex-client': 0.3.4
+ '@atproto/lex-client': 0.3.5
'@atproto/lex-schema': 0.2.6
tslib: 2.8.1
- '@atproto/lex-resolver@0.2.9':
+ '@atproto/lex-resolver@0.2.10':
dependencies:
'@atproto-labs/did-resolver': 0.3.7
'@atproto/crypto': 0.5.4
- '@atproto/lex-client': 0.3.4
+ '@atproto/lex-client': 0.3.5
'@atproto/lex-data': 0.1.7
'@atproto/lex-document': 0.1.10
'@atproto/lex-schema': 0.2.6
@@ -9785,12 +9785,12 @@ snapshots:
'@standard-schema/spec': 1.1.0
tslib: 2.8.1
- '@atproto/lex@0.3.7':
+ '@atproto/lex@0.3.8':
dependencies:
'@atproto/lex-builder': 0.1.13
- '@atproto/lex-client': 0.3.4
+ '@atproto/lex-client': 0.3.5
'@atproto/lex-data': 0.1.7
- '@atproto/lex-installer': 0.1.16
+ '@atproto/lex-installer': 0.1.17
'@atproto/lex-json': 0.1.6
'@atproto/lex-schema': 0.2.6
tslib: 2.8.1
@@ -10770,10 +10770,10 @@ snapshots:
react: 19.2.3
react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)
- '@bsky/sdk@1.0.1(@atproto/lex@0.3.7)':
+ '@bsky/sdk@1.0.1(@atproto/lex@0.3.8)':
dependencies:
'@atproto-labs/handle-resolver': 0.4.8
- '@atproto/lex': 0.3.7
+ '@atproto/lex': 0.3.8
'@atproto/syntax': 0.7.5
tlds: 1.261.0
From c4c999ff4f8f6bf42e752a1b0d39718a6330b68b Mon Sep 17 00:00:00 2001
From: estrattonbailey <4732330+estrattonbailey@users.noreply.github.com>
Date: Wed, 26 Aug 2026 23:37:52 +0000
Subject: [PATCH 22/22] Nightly source-language update
---
src/locale/locales/en/messages.po | 80 +++++++++++++++----------------
1 file changed, 40 insertions(+), 40 deletions(-)
diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po
index 231135b3e9..122b0b4724 100644
--- a/src/locale/locales/en/messages.po
+++ b/src/locale/locales/en/messages.po
@@ -198,7 +198,7 @@ msgstr ""
#. Number of users (always at least 25) who have joined Bluesky using a specific starter pack
#. placeholder {0}: starterPack.joinedAllTimeCount || 0
-#: src/screens/StarterPack/StarterPackScreen.tsx:496
+#: src/screens/StarterPack/StarterPackScreen.tsx:493
msgid "{0, plural, other {# people have}} joined Bluesky via this starter pack!"
msgstr ""
@@ -1443,7 +1443,7 @@ msgid "All {0}"
msgstr "All {0}"
#: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:98
-#: src/screens/StarterPack/StarterPackScreen.tsx:397
+#: src/screens/StarterPack/StarterPackScreen.tsx:394
msgid "All accounts have been followed!"
msgstr ""
@@ -1626,8 +1626,8 @@ msgstr ""
#: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:55
#: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:83
-#: src/screens/StarterPack/StarterPackScreen.tsx:356
-#: src/screens/StarterPack/StarterPackScreen.tsx:383
+#: src/screens/StarterPack/StarterPackScreen.tsx:353
+#: src/screens/StarterPack/StarterPackScreen.tsx:380
msgid "An error occurred while trying to follow all"
msgstr ""
@@ -1912,7 +1912,7 @@ msgstr ""
msgid "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants."
msgstr "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants."
-#: src/screens/StarterPack/StarterPackScreen.tsx:676
+#: src/screens/StarterPack/StarterPackScreen.tsx:673
msgid "Are you sure you want to delete this starter pack?"
msgstr ""
@@ -3447,7 +3447,7 @@ msgstr "Copy invite link"
#: src/components/dms/ChatInvite/Root.tsx:92
#: src/components/StarterPack/ShareDialog.tsx:115
-#: src/screens/StarterPack/StarterPackScreen.tsx:636
+#: src/screens/StarterPack/StarterPackScreen.tsx:633
msgid "Copy link"
msgstr ""
@@ -3472,7 +3472,7 @@ msgstr ""
msgid "Copy link to profile"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:629
+#: src/screens/StarterPack/StarterPackScreen.tsx:626
msgid "Copy link to starter pack"
msgstr ""
@@ -3627,7 +3627,7 @@ msgstr ""
msgid "Create a list"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:612
+#: src/screens/StarterPack/StarterPackScreen.tsx:609
msgid "Create a list from this starter pack"
msgstr ""
@@ -3694,7 +3694,7 @@ msgstr "Create group chat"
msgid "Create list"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:618
+#: src/screens/StarterPack/StarterPackScreen.tsx:615
msgid "Create list from members"
msgstr ""
@@ -3837,9 +3837,9 @@ msgstr ""
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:824
#: src/screens/ProfileList/components/MoreOptionsMenu.tsx:276
#: src/screens/Settings/AppPasswords.tsx:213
-#: src/screens/StarterPack/StarterPackScreen.tsx:607
-#: src/screens/StarterPack/StarterPackScreen.tsx:707
-#: src/screens/StarterPack/StarterPackScreen.tsx:786
+#: src/screens/StarterPack/StarterPackScreen.tsx:604
+#: src/screens/StarterPack/StarterPackScreen.tsx:704
+#: src/screens/StarterPack/StarterPackScreen.tsx:783
msgid "Delete"
msgstr ""
@@ -3910,12 +3910,12 @@ msgstr ""
msgid "Delete post"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:601
-#: src/screens/StarterPack/StarterPackScreen.tsx:777
+#: src/screens/StarterPack/StarterPackScreen.tsx:598
+#: src/screens/StarterPack/StarterPackScreen.tsx:774
msgid "Delete starter pack"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:673
+#: src/screens/StarterPack/StarterPackScreen.tsx:670
msgid "Delete starter pack?"
msgstr ""
@@ -4336,7 +4336,7 @@ msgstr ""
#: src/screens/Messages/components/EditTextButton.tsx:52
#: src/screens/Settings/AccountSettings.tsx:148
#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:250
-#: src/screens/StarterPack/StarterPackScreen.tsx:596
+#: src/screens/StarterPack/StarterPackScreen.tsx:593
#: src/screens/StarterPack/Wizard/index.tsx:327
#: src/screens/StarterPack/Wizard/index.tsx:332
msgid "Edit"
@@ -4440,7 +4440,7 @@ msgstr ""
msgid "Edit Profile"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:588
+#: src/screens/StarterPack/StarterPackScreen.tsx:585
msgid "Edit starter pack"
msgstr ""
@@ -4936,7 +4936,7 @@ msgstr ""
msgid "Failed to delete post, please try again"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:746
+#: src/screens/StarterPack/StarterPackScreen.tsx:743
msgid "Failed to delete starter pack"
msgstr ""
@@ -5252,7 +5252,7 @@ msgstr ""
#: src/screens/SavedFeeds.tsx:112
#: src/screens/SavedFeeds.tsx:303
#: src/screens/Search/SearchResults.tsx:113
-#: src/screens/StarterPack/StarterPackScreen.tsx:197
+#: src/screens/StarterPack/StarterPackScreen.tsx:194
#: src/view/screens/Feeds.tsx:504
#: src/view/screens/Profile.tsx:240
#: src/view/shell/desktop/LeftNav.tsx:712
@@ -5469,8 +5469,8 @@ msgstr ""
#: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:165
#: src/screens/Settings/FindContactsSettings.tsx:469
#: src/screens/Settings/FindContactsSettings.tsx:479
-#: src/screens/StarterPack/StarterPackScreen.tsx:444
-#: src/screens/StarterPack/StarterPackScreen.tsx:452
+#: src/screens/StarterPack/StarterPackScreen.tsx:441
+#: src/screens/StarterPack/StarterPackScreen.tsx:449
msgid "Follow all"
msgstr ""
@@ -5833,7 +5833,7 @@ msgstr ""
#: src/screens/List/ListHiddenScreen.tsx:228
#: src/screens/Profile/ErrorState.tsx:63
#: src/screens/Profile/ErrorState.tsx:67
-#: src/screens/StarterPack/StarterPackScreen.tsx:799
+#: src/screens/StarterPack/StarterPackScreen.tsx:796
msgid "Go Back"
msgstr ""
@@ -6788,8 +6788,8 @@ msgstr "Join"
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:202
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:207
-#: src/screens/StarterPack/StarterPackScreen.tsx:470
-#: src/screens/StarterPack/StarterPackScreen.tsx:480
+#: src/screens/StarterPack/StarterPackScreen.tsx:467
+#: src/screens/StarterPack/StarterPackScreen.tsx:477
msgid "Join Bluesky"
msgstr ""
@@ -8720,7 +8720,7 @@ msgstr "Open settings"
msgid "Open share menu"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:574
+#: src/screens/StarterPack/StarterPackScreen.tsx:571
msgid "Open starter pack menu"
msgstr ""
@@ -8997,7 +8997,7 @@ msgstr ""
#: src/screens/ProfileList/index.tsx:164
#: src/screens/Search/SearchResults.tsx:107
-#: src/screens/StarterPack/StarterPackScreen.tsx:196
+#: src/screens/StarterPack/StarterPackScreen.tsx:193
msgid "People"
msgstr ""
@@ -9464,7 +9464,7 @@ msgstr ""
#: src/components/activity-notifications/SubscribeProfileDialog.tsx:266
#: src/screens/ProfileList/index.tsx:164
#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:216
-#: src/screens/StarterPack/StarterPackScreen.tsx:198
+#: src/screens/StarterPack/StarterPackScreen.tsx:195
#: src/view/screens/Profile.tsx:235
msgid "Posts"
msgstr ""
@@ -10202,8 +10202,8 @@ msgstr ""
msgid "Report sent"
msgstr "Report sent"
+#: src/screens/StarterPack/StarterPackScreen.tsx:646
#: src/screens/StarterPack/StarterPackScreen.tsx:649
-#: src/screens/StarterPack/StarterPackScreen.tsx:652
msgid "Report starter pack"
msgstr ""
@@ -10269,7 +10269,7 @@ msgstr ""
#: src/components/PostControls/RepostButton.tsx:146
#: src/components/PostControls/RepostButton.web.tsx:43
#: src/components/PostControls/RepostButton.web.tsx:103
-#: src/screens/StarterPack/StarterPackScreen.tsx:569
+#: src/screens/StarterPack/StarterPackScreen.tsx:566
msgid "Repost or quote post"
msgstr ""
@@ -10488,7 +10488,7 @@ msgstr ""
#: src/components/Error.tsx:68
#: src/screens/List/ListHiddenScreen.tsx:223
-#: src/screens/StarterPack/StarterPackScreen.tsx:793
+#: src/screens/StarterPack/StarterPackScreen.tsx:790
msgid "Return to previous page"
msgstr ""
@@ -11219,7 +11219,7 @@ msgstr ""
#: src/screens/Hashtag.tsx:132
#: src/screens/Messages/components/InviteLinkDialog.tsx:412
#: src/screens/Messages/components/InviteLinkDialog.tsx:423
-#: src/screens/StarterPack/StarterPackScreen.tsx:439
+#: src/screens/StarterPack/StarterPackScreen.tsx:436
#: src/screens/Topic.tsx:91
msgid "Share"
msgstr ""
@@ -11295,7 +11295,7 @@ msgstr ""
msgid "Share this search"
msgstr "Share this search"
-#: src/screens/StarterPack/StarterPackScreen.tsx:432
+#: src/screens/StarterPack/StarterPackScreen.tsx:429
msgid "Share this starter pack"
msgstr ""
@@ -11307,8 +11307,8 @@ msgstr ""
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:133
#: src/screens/ProfileList/components/MoreOptionsMenu.tsx:161
#: src/screens/ProfileList/components/MoreOptionsMenu.tsx:167
-#: src/screens/StarterPack/StarterPackScreen.tsx:630
-#: src/screens/StarterPack/StarterPackScreen.tsx:638
+#: src/screens/StarterPack/StarterPackScreen.tsx:627
+#: src/screens/StarterPack/StarterPackScreen.tsx:635
#: src/view/com/profile/ProfileMenu.tsx:321
#: src/view/com/profile/ProfileMenu.tsx:333
msgid "Share via..."
@@ -11823,7 +11823,7 @@ msgstr ""
msgid "Starter pack by you"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:757
+#: src/screens/StarterPack/StarterPackScreen.tsx:754
msgid "Starter pack is invalid"
msgstr ""
@@ -12225,8 +12225,8 @@ msgstr ""
#: src/screens/StarterPack/StarterPackScreen.tsx:113
#: src/screens/StarterPack/StarterPackScreen.tsx:114
-#: src/screens/StarterPack/StarterPackScreen.tsx:161
-#: src/screens/StarterPack/StarterPackScreen.tsx:162
+#: src/screens/StarterPack/StarterPackScreen.tsx:158
+#: src/screens/StarterPack/StarterPackScreen.tsx:159
#: src/screens/StarterPack/Wizard/index.tsx:112
#: src/screens/StarterPack/Wizard/index.tsx:122
msgid "That starter pack could not be found."
@@ -12363,7 +12363,7 @@ msgstr "The selected video uses an unsupported format."
msgid "The server appears to be experiencing issues. Please try again in a few moments."
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:767
+#: src/screens/StarterPack/StarterPackScreen.tsx:764
msgid "The starter pack that you are trying to view is invalid. You may delete this starter pack instead."
msgstr ""
@@ -13172,7 +13172,7 @@ msgstr ""
msgid "Unable to contact your service. Please check your Internet connection."
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:692
+#: src/screens/StarterPack/StarterPackScreen.tsx:689
msgid "Unable to delete"
msgstr ""
@@ -14478,7 +14478,7 @@ msgstr ""
msgid "Yes, delete my account"
msgstr ""
-#: src/screens/StarterPack/StarterPackScreen.tsx:704
+#: src/screens/StarterPack/StarterPackScreen.tsx:701
msgid "Yes, delete this starter pack"
msgstr ""