diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 40b918e77..5b7eef4c9 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -76,7 +76,7 @@ }, "src/components/Post/Embed/ImageEmbed.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 1 + "count": 2 } }, "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx": { @@ -169,7 +169,7 @@ }, "src/components/images/AutoSizedImage.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 1 + "count": 2 } }, "src/components/images/Gallery/index.tsx": { diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt index b7e81a61f..1b9224c6e 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -41,11 +41,14 @@ class BottomSheetView( private val screenHeight: Float = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) { // API 35+: edge-to-edge is mandatory, heightPixels is the full display - context.resources.displayMetrics.heightPixels.toFloat() + context.resources.displayMetrics.heightPixels + .toFloat() } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { // API 30-34: heightPixels may exclude nav bar, use currentWindowMetrics val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager - wm.currentWindowMetrics.bounds.height().toFloat() + wm.currentWindowMetrics.bounds + .height() + .toFloat() } else { // API < 30: currentWindowMetrics not available, use getRealSize // which includes system bars (heightPixels may exclude them) @@ -166,6 +169,7 @@ class BottomSheetView( when { // Full height sheets contentHeight >= screenHeight -> 0.99f + else -> this.clampRatio(this.getTargetHeight() / screenHeight) } @@ -271,8 +275,9 @@ class BottomSheetView( } // Apply deferred layout update after gesture completes if (newState != BottomSheetBehavior.STATE_DRAGGING && - newState != BottomSheetBehavior.STATE_SETTLING && - pendingLayoutUpdate) { + newState != BottomSheetBehavior.STATE_SETTLING && + pendingLayoutUpdate + ) { pendingLayoutUpdate = false updateLayout() } @@ -292,7 +297,6 @@ class BottomSheetView( if (!fullHeight) { this.startObservingContentHeight() } - } fun updateLayout() { @@ -365,17 +369,18 @@ class BottomSheetView( val innerViewGroup = this.innerView as? ViewGroup ?: return - val listener = OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, oldBottom -> - val newHeight = bottom - top - val oldHeight = oldBottom - oldTop - if (newHeight != oldHeight) { - val contentHeight = getContentHeight() - if (contentHeight != lastObservedContentHeight && contentHeight > 0 && (isOpen || isOpening) && !isClosing) { - lastObservedContentHeight = contentHeight - updateLayout() + val listener = + OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, oldBottom -> + val newHeight = bottom - top + val oldHeight = oldBottom - oldTop + if (newHeight != oldHeight) { + val contentHeight = getContentHeight() + if (contentHeight != lastObservedContentHeight && contentHeight > 0 && (isOpen || isOpening) && !isClosing) { + lastObservedContentHeight = contentHeight + updateLayout() + } } } - } val children = mutableListOf() for (i in 0 until innerViewGroup.childCount) { diff --git a/package.json b/package.json index 0716bd085..b8bd26e24 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@bsky.app/expo-image-crop-tool": "^0.5.1", "@bsky.app/expo-scroll-edge-effect": "^0.1.4", "@bsky.app/expo-translate-text": "^0.2.9", + "@bsky.app/peek-menu": "^0.2.4", "@bsky.app/react-native-mmkv": "2.12.5", "@bsky.app/sift": "^0.3.8", "@bsky.app/tapper": "^0.5.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d9210cc2..b3d000310 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,6 +268,9 @@ importers: '@bsky.app/expo-translate-text': specifier: ^0.2.9 version: 0.2.9(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@bsky.app/peek-menu': + specifier: ^0.2.4 + version: 0.2.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@bsky.app/react-native-mmkv': specifier: 2.12.5 version: 2.12.5(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -1649,6 +1652,13 @@ packages: react: '*' react-native: '*' + '@bsky.app/peek-menu@0.2.4': + resolution: {integrity: sha512-3E5FwgCXMU6baye3NWoBKih3SCh6s8AgAtxN523YBHZGC5TxpjGaBsjFi765y31nKmTll3QH7x4wtDgYqVvCQg==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + '@bsky.app/react-native-mmkv@2.12.5': resolution: {integrity: sha512-3vUz1nQY1DiKIPAWRkpp5ZGxH5f2G6Ui0UuQuEYjYv81xx1qFcSzS9KQ2sHcOKYdkOM9amWV2Q8TQCxt1lrAHg==} peerDependencies: @@ -10461,6 +10471,12 @@ snapshots: react: 19.1.0 react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + '@bsky.app/peek-menu@0.2.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + expo: 54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + '@bsky.app/react-native-mmkv@2.12.5(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index 530c85ae6..9aad6da86 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -1,11 +1,16 @@ import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' import {Image} from 'expo-image' -import {type AppBskyFeedDefs} from '@atproto/api' -import {Trans} from '@lingui/react/macro' +import {type AppBskyEmbedImages, type AppBskyFeedDefs} from '@atproto/api' +import {Trans, useLingui} from '@lingui/react/macro' +import {shareImageModal} from '#/lib/media/manip' +import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' import {isGifEmbed} from '#/lib/strings/embed-player' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' +import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import * as PeekMenu from '#/components/PeekMenu' import {Text} from '#/components/Typography' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import * as bsky from '#/types/bsky' @@ -16,9 +21,11 @@ import * as bsky from '#/types/bsky' export function Embed({ embed, style, + peekable = false, }: { embed: AppBskyFeedDefs.PostView['embed'] style?: StyleProp + peekable?: boolean }) { const e = bsky.post.parseEmbed(embed) @@ -27,13 +34,17 @@ export function Embed({ if (e.type === 'images') { return ( - {e.view.images.map(image => ( - - ))} + {e.view.images.map(image => + peekable ? ( + + ) : ( + + ), + )} ) } else if (e.type === 'link') { @@ -156,6 +167,45 @@ export function VideoItem({ ) } +function PeekableImageItem({image}: {image: AppBskyEmbedImages.ViewImage}) { + const {t: l} = useLingui() + const saveImage = useSaveImageToMediaLibrary() + + const aspect = + image.aspectRatio && image.aspectRatio.height > 0 + ? image.aspectRatio.width / image.aspectRatio.height + : undefined + + return ( + + 0 ? aspect : 1, + }} + borderRadius={tokens.borderRadius.xs}> + + + + void saveImage(image.fullsize)}> + + {l`Save image`} + + void shareImageModal({uri: image.fullsize})}> + + {l`Share`} + + + + ) +} + const styles = StyleSheet.create({ altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', diff --git a/src/components/PeekMenu.tsx b/src/components/PeekMenu.tsx new file mode 100644 index 000000000..53732ec3e --- /dev/null +++ b/src/components/PeekMenu.tsx @@ -0,0 +1 @@ +export * from '@bsky.app/peek-menu' diff --git a/src/components/Post/Embed/ImageContextMenu.tsx b/src/components/Post/Embed/ImageContextMenu.tsx new file mode 100644 index 000000000..3aadb5690 --- /dev/null +++ b/src/components/Post/Embed/ImageContextMenu.tsx @@ -0,0 +1,79 @@ +import {type ReactNode} from 'react' +import {type StyleProp, type ViewStyle} from 'react-native' +import {useLingui} from '@lingui/react/macro' + +import {shareImageModal} from '#/lib/media/manip' +import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' +import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' +import * as PeekMenu from '#/components/PeekMenu' +import {IS_IOS} from '#/env' + +/** + * Wraps an image embed with the iOS peek-and-menu interaction. On non-iOS + * platforms this renders children unchanged. + * + * The aspect ratio is consumed by the native side to size the preview + * viewController correctly — which is what makes the lift animation clean + * for portrait/panorama images. + */ +export function ImageContextMenu({ + fullsizeUri, + thumbUri, + aspectRatio, + borderRadius, + onPreviewPress, + style, + children, +}: { + fullsizeUri: string + /** Thumbnail URL. Used as an instant placeholder in the native preview + * while the fullsize loads, so there's no black flash on first peek. */ + thumbUri?: string + /** width / height; defaults to 1 if missing. */ + aspectRatio: number | undefined + borderRadius?: number + onPreviewPress?: () => void + style?: StyleProp + children: ReactNode +}) { + const {t: l} = useLingui() + const saveImage = useSaveImageToMediaLibrary() + + if (!IS_IOS) { + return children + } + + const handleSave = () => { + void saveImage(fullsizeUri) + } + const handleShare = () => { + void shareImageModal({uri: fullsizeUri}) + } + + return ( + + 0 ? aspectRatio : 1, + }} + borderRadius={borderRadius} + onPreviewPress={onPreviewPress}> + {children} + + + + + {l`Save image`} + + + + {l`Share`} + + + + ) +} diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index d779d0487..ccd1ceb27 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -1,3 +1,4 @@ +import {useRef} from 'react' import {InteractionManager, View} from 'react-native' import {type AnimatedRef} from 'react-native-reanimated' import {Image} from 'expo-image' @@ -8,6 +9,7 @@ import {Gallery} from '#/components/images/Gallery' import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid' import {useLightboxControls} from '#/components/Lightbox/state' import {type Dimensions} from '#/components/Lightbox/types' +import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {useAnalytics} from '#/analytics' import {type EmbedType} from '#/types/bsky/post' @@ -24,6 +26,11 @@ export function ImageEmbed({ const {images} = embed.view const galleryEnabled = ax.features.enabled(ax.features.PostGalleryEmbedEnable) + // Captured from AutoSizedImage so the peek-commit handler can reuse the same + // ref + dims that a tap would — keeps the lightbox's return animation intact. + const singleContainerRef = useRef | null>(null) + const singleDimsRef = useRef(null) + if (images.length > 0) { const items = images.map(img => ({ uri: img.fullsize, @@ -59,24 +66,49 @@ export function ImageEmbed({ if (images.length === 1) { const image = images[0] + const aspect = + image.aspectRatio && image.aspectRatio.height > 0 + ? image.aspectRatio.width / image.aspectRatio.height + : undefined + const openFromSingle = () => { + if (singleContainerRef.current) { + onPress(0, [singleContainerRef.current], [singleDimsRef.current]) + } + } return ( - onPress(0, [containerRef], [dims])} - onPressIn={() => onPressIn(0)} - hideBadge={ - rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - } - /> + + { + singleContainerRef.current = ref + }} + onDimsChange={dims => { + singleDimsRef.current = dims + }} + onPress={(containerRef, dims) => + onPress(0, [containerRef], [dims]) + } + onPressIn={() => onPressIn(0)} + hideBadge={ + rest.viewContext === + PostEmbedViewContext.FeedEmbedRecordWithMedia + } + /> + ) } diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx index 4feaaef35..a7ef371cd 100644 --- a/src/components/icons/TEMPLATE.tsx +++ b/src/components/icons/TEMPLATE.tsx @@ -1,8 +1,20 @@ -import {forwardRef} from 'react' +import { + forwardRef, + type ForwardRefExoticComponent, + type RefAttributes, +} from 'react' import Svg, {Path} from 'react-native-svg' import {type Props, useCommonSVGProps} from '#/components/icons/common' +export type IconWithSvgMeta = ForwardRefExoticComponent< + Props & RefAttributes +> & { + svgPaths: string[] + svgViewBox: string + svgStrokeWidth: number +} + export const IconTemplate_Stroke2_Corner0_Rounded = forwardRef( function LogoImpl(props: Props, ref) { const {fill, size, style, ...rest} = useCommonSVGProps(props) @@ -41,7 +53,7 @@ export function createSinglePathSVG({ strokeLinecap?: 'butt' | 'round' | 'square' strokeLinejoin?: 'miter' | 'round' | 'bevel' }) { - return forwardRef(function LogoImpl(props, ref) { + const Icon = forwardRef(function LogoImpl(props, ref) { const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) const hasStroke = strokeWidth > 0 @@ -68,7 +80,11 @@ export function createSinglePathSVG({ /> ) - }) + }) as IconWithSvgMeta + Icon.svgPaths = [path] + Icon.svgViewBox = viewBox || '0 0 24 24' + Icon.svgStrokeWidth = strokeWidth + return Icon } export function createMultiPathSVG({ @@ -78,7 +94,7 @@ export function createMultiPathSVG({ paths: string[] viewBox?: string }) { - return forwardRef(function LogoImpl(props, ref) { + const Icon = forwardRef(function LogoImpl(props, ref) { const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) return ( @@ -102,5 +118,9 @@ export function createMultiPathSVG({ ))} ) - }) + }) as IconWithSvgMeta + Icon.svgPaths = paths + Icon.svgViewBox = viewBox || '0 0 24 24' + Icon.svgStrokeWidth = 0 + return Icon } diff --git a/src/components/images/AutoSizedImage.tsx b/src/components/images/AutoSizedImage.tsx index 2df8d056d..79b41311c 100644 --- a/src/components/images/AutoSizedImage.tsx +++ b/src/components/images/AutoSizedImage.tsx @@ -1,4 +1,4 @@ -import {useMemo, useRef} from 'react' +import {useEffect, useMemo, useRef} from 'react' import {type DimensionValue, Pressable, View} from 'react-native' import Animated, { type AnimatedRef, @@ -69,6 +69,8 @@ export function AutoSizedImage({ onPress, onLongPress, onPressIn, + onContainerRef, + onDimsChange, }: { image: AppBskyEmbedImages.ViewImage crop?: 'none' | 'square' | 'constrained' @@ -79,6 +81,11 @@ export function AutoSizedImage({ ) => void onLongPress?: () => void onPressIn?: () => void + /** Fires once with the internal container ref so a parent can drive its + * own lightbox-return animation without waiting for an `onPress`. */ + onContainerRef?: (ref: AnimatedRef) => void + /** Fires when the underlying image reports its natural dimensions. */ + onDimsChange?: (dims: Dimensions) => void }) { const t = useTheme() const {_} = useLingui() @@ -86,6 +93,10 @@ export function AutoSizedImage({ const containerRef = useAnimatedRef() const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) + useEffect(() => { + onContainerRef?.(containerRef) + }, [containerRef, onContainerRef]) + let aspectRatio: number | undefined const dims = image.aspectRatio if (dims) { @@ -122,10 +133,12 @@ export function AutoSizedImage({ accessibilityHint="" onLoad={e => { if (!isContain) { - fetchedDimsRef.current = { + const dims = { width: e.source.width, height: e.source.height, } + fetchedDimsRef.current = dims + onDimsChange?.(dims) } }} loading="lazy" diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 1566f0973..f4ffcd715 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -24,7 +24,7 @@ import {mergeRefs} from '#/lib/merge-refs' import {useA11y} from '#/state/a11y' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' -import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf' import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' import {AutoSizedImage} from '#/components/images/AutoSizedImage' import { @@ -36,6 +36,7 @@ import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandle import {usePointerHandlers} from '#/components/images/Gallery/usePointerHandlers' import {getAspectRatio} from '#/components/images/Gallery/utils' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' @@ -264,6 +265,21 @@ export function Gallery({ data={images} keyExtractor={(item, index) => item.thumb + index} renderItem={({item, index}) => { + const openLightboxAtIndex = onPress + ? () => { + ax.metric('post:gallery:openLightbox', { + fromImage: index + 1, // convert to 1-based index for easier analysis + totalImages: images.length, + }) + const refs: AnimatedRef[] = [] + const dims: (Dimensions | null)[] = [] + for (let i = 0; i < images.length; i++) { + refs.push(containerRefsRef.current.get(i)!) + dims.push(thumbDimsRef.current.get(i) ?? null) + } + onPress(index, refs, dims) + } + : undefined return ( { thumbDimsRef.current.set(i, dims) }} - onPress={ - onPress - ? () => { - ax.metric('post:gallery:openLightbox', { - fromImage: index + 1, // convert to 1-based index for easier analysis - totalImages: images.length, - }) - const refs: AnimatedRef[] = [] - const dims: (Dimensions | null)[] = [] - for (let i = 0; i < images.length; i++) { - refs.push(containerRefsRef.current.get(i)!) - dims.push(thumbDimsRef.current.get(i) ?? null) - } - onPress(index, refs, dims) - } - : undefined - } + onPress={openLightboxAtIndex} onPressIn={onPressIn ? () => onPressIn(index) : undefined} + onPreviewPress={openLightboxAtIndex} /> ) }} @@ -378,6 +379,7 @@ function GalleryImage({ onThumbDims, onPress, onPressIn, + onPreviewPress, }: { contentHeight: number image: AppBskyEmbedImages.ViewImage @@ -391,6 +393,7 @@ function GalleryImage({ onThumbDims: (index: number, dims: Dimensions) => void onPress?: () => void onPressIn?: () => void + onPreviewPress?: () => void }) { const t = useTheme() const {t: l} = useLingui() @@ -416,124 +419,131 @@ function GalleryImage({ collapsable={false} aria-roledescription={l`slide`} aria-label={image.alt || l`Image ${index + 1} of ${imageCount}`}> - setFocused(true)} - onBlur={() => setFocused(false)} - accessibilityRole="button" - accessibilityLabel={image.alt || l`Image ${index + 1}`} - accessibilityHint={l`Opens full image`} - android_ripple={{ - color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), - foreground: true, - }} - style={({pressed}) => [ - a.rounded_md, - a.overflow_hidden, - t.atoms.bg_contrast_25, - web([ - { - cursor: 'inherit', - outline: 0, - border: 0, - }, - a.transition_transform, - {transitionDuration: '200ms'}, - pressed && {transform: [{scale: 0.99}]}, - ]), - ]}> - { - const ar = getAspectRatio(e.source) - if (ar && ar !== aspectRatio) { - setAspectRatio(ar) - } - onThumbDims(index, { - width: e.source.width, - height: e.source.height, - }) + + setFocused(true)} + onBlur={() => setFocused(false)} + accessibilityRole="button" + accessibilityLabel={image.alt || l`Image ${index + 1}`} + accessibilityHint={l`Opens full image`} + android_ripple={{ + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, }} - /> - - {(hasAlt || isCropped) && !hideBadges ? ( - [ + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + web([ { - bottom: a.p_xs.padding, - right: a.p_xs.padding, - gap: 3, - }, - largeAltBadge && { - gap: 4, + cursor: 'inherit', + outline: 0, + border: 0, }, - ]}> - {isCropped && ( - - - - )} - {hasAlt && ( - - + { + const ar = getAspectRatio(e.source) + if (ar && ar !== aspectRatio) { + setAspectRatio(ar) + } + onThumbDims(index, { + width: e.source.width, + height: e.source.height, + }) + }} + /> + + {(hasAlt || isCropped) && !hideBadges ? ( + + {isCropped && ( + + + + )} + {hasAlt && ( + - ALT - - - )} - - ) : null} + + ALT + + + )} + + ) : null} - - + /> + + ) } diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index b809a783f..0c349b661 100644 --- a/src/components/images/ImageLayoutGridItem.tsx +++ b/src/components/images/ImageLayoutGridItem.tsx @@ -9,8 +9,9 @@ import {Trans} from '@lingui/react/macro' import {type Dimensions} from '#/lib/media/types' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' @@ -52,46 +53,63 @@ export function GalleryItem({ const hasAlt = !!image.alt const hideBadges = viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + + const aspect = + image.aspectRatio && image.aspectRatio.height > 0 + ? image.aspectRatio.width / image.aspectRatio.height + : undefined + + // The tap handler and the peek-commit handler do the same thing: open the + // lightbox with this cell's ref + dims so the lightbox's return animation + // can target the original thumbnail. + const openLightboxAtIndex = onPress + ? () => onPress(index, containerRefs, thumbDimsRef.current.slice()) + : undefined + return ( - onPress(index, containerRefs, thumbDimsRef.current.slice()) - : undefined - } - onPressIn={onPressIn ? () => onPressIn(index) : undefined} - onLongPress={onLongPress ? () => onLongPress(index) : undefined} - android_ripple={{ - color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), - foreground: true, - }} - style={[ - a.flex_1, - a.overflow_hidden, - t.atoms.bg_contrast_25, - imageStyle, - ]} - accessibilityRole="button" - accessibilityLabel={image.alt || _(msg`Image`)} - accessibilityHint=""> - { - thumbDimsRef.current[index] = { - width: e.source.width, - height: e.source.height, - } + + onPressIn(index) : undefined} + onLongPress={onLongPress ? () => onLongPress(index) : undefined} + android_ripple={{ + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, }} - loading="lazy" - /> - - + style={[ + a.flex_1, + a.overflow_hidden, + t.atoms.bg_contrast_25, + imageStyle, + ]} + accessibilityRole="button" + accessibilityLabel={image.alt || _(msg`Image`)} + accessibilityHint=""> + { + thumbDimsRef.current[index] = { + width: e.source.width, + height: e.source.height, + } + }} + loading="lazy" + /> + + + {hasAlt && !hideBadges ? ( ) -- 2.51.2 From 3c06ecfa1a18cd57b26960dbe36e5d7aacd89b82 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:13:34 -0700 Subject: [PATCH 02/61] Transpile `@atproto/api` package for older browser support (#10700) --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 101a762ff..bccf95ade 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -43,7 +43,7 @@ function patchSourceMapFilter(rules, pathPattern) { module.exports = async function (env, argv) { env.babel = { - dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo'], + dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo', '@atproto/api'], } let config = await createExpoWebpackConfigAsync(env, argv) config = withAlias(config, { -- 2.51.2 From f4f66af31c3c313916ee1ec00e9cf08f3ca151be Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Wed, 3 Jun 2026 03:21:33 +0000 Subject: [PATCH 03/61] Nightly source-language update --- src/locale/locales/en/messages.po | 865 +++++++++++++++++++----------- 1 file changed, 566 insertions(+), 299 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 4de882a99..b1fd0337d 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -86,6 +86,11 @@ msgstr "" msgid "{0, plural, one {# new join request} other {# new join requests}}" msgstr "{0, plural, one {# new join request} other {# new join requests}}" +#. placeholder {0}: convo.details.memberLimit +#: src/screens/Messages/components/InviteLinkDialog.tsx:181 +msgid "{0, plural, one {# person} other {# people}}" +msgstr "{0, plural, one {# person} other {# people}}" + #. placeholder {0}: reactions.length #. placeholder {1}: groupedReactions.map(g => g.value).join(' ') #: src/components/dms/MessageItem.tsx:297 @@ -319,6 +324,20 @@ msgstr "" msgid "{0}’s avatar" msgstr "{0}’s avatar" +#. The number of active group chat members out of the total number allowed. +#. placeholder {0}: data.joinLinkPreviews[0].memberCount +#. placeholder {1}: data.joinLinkPreviews[0].memberLimit +#: src/screens/Messages/JoinRequest.tsx:130 +msgid "{0}/{1}" +msgstr "{0}/{1}" + +#. The number of active group chat members out of the total number allowed. +#. placeholder {0}: joinLinkPreview.memberCount +#. placeholder {1}: joinLinkPreview.memberLimit +#: src/components/intents/GroupChatJoinDialog.tsx:307 +msgid "{0}/{1} members" +msgstr "{0}/{1} members" + #. How many days have passed, displayed in a narrow form #. placeholder {0}: diff.value #: src/lib/hooks/useTimeAgo.ts:171 @@ -355,6 +374,10 @@ msgstr "{count, plural, one {# request} other {# requests}}" msgid "{count, plural, one {# unread item} other {# unread items}}" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:429 +msgid "{count, plural, zero {No requests to join} one {# request to join} other {# requests to join}}" +msgstr "{count, plural, zero {No requests to join} one {# request to join} other {# requests to join}}" + #. Displayed when the number of requests is greater than 20 #: src/screens/Messages/components/InboxRequests.tsx:53 msgid "{count}+" @@ -365,6 +388,11 @@ msgstr "{count}+" msgid "{count}+ requests" msgstr "{count}+ requests" +#. Displayed when there are more requests to join a group chat than have been loaded +#: src/screens/Messages/JoinRequests.tsx:423 +msgid "{count}+ requests to join" +msgstr "{count}+ requests to join" + #: src/components/dms/DateDivider.tsx:60 msgid "{date} at {time}" msgstr "{date} at {time}" @@ -539,7 +567,7 @@ msgid "{following} following" msgstr "" #: src/components/dms/components/GroupChatProfileCard.tsx:56 -#: src/components/dms/InitiateChatFlow.tsx:888 +#: src/components/dms/InitiateChatFlow.tsx:889 msgid "{handle} can’t be added" msgstr "{handle} can’t be added" @@ -547,7 +575,7 @@ msgstr "{handle} can’t be added" msgid "{handle} can't be messaged" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:849 +#: src/components/dms/InitiateChatFlow.tsx:850 msgid "{handle} can’t be messaged" msgstr "{handle} can’t be messaged" @@ -581,7 +609,7 @@ msgid "{MAX_HIDDEN_REPLIES, plural, other {You can hide a maximum of # replies.} msgstr "" #. The number of group chat members out of the total number of permitted users. -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:34 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:39 msgid "{memberCount}/{memberLimit}" msgstr "{memberCount}/{memberLimit}" @@ -629,12 +657,12 @@ msgid "{rank}." msgstr "" #. The number of requests to join a group chat. -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:53 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:61 msgid "{requestCount, plural, one {# request} other {# requests}}" msgstr "{requestCount, plural, one {# request} other {# requests}}" #. Displayed when there are more than 20 requests to join a group chat -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:48 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:56 msgid "{requestCount}+ requests" msgstr "{requestCount}+ requests" @@ -802,6 +830,8 @@ msgstr "" #: src/components/contacts/screens/VerifyNumber.tsx:155 #: src/components/dms/dialogs/NewChatDialog.tsx:52 #: src/components/dms/dialogs/NewChatDialog.tsx:90 +#: src/screens/Messages/JoinRequests.tsx:188 +#: src/screens/Messages/JoinRequests.tsx:220 msgid "A network error occurred. Please check your internet connection." msgstr "" @@ -836,7 +866,7 @@ msgstr "" msgid "A selected recipient is not followed by the sender." msgstr "A selected recipient is not followed by the sender." -#: src/Navigation.tsx:457 +#: src/Navigation.tsx:459 #: src/screens/Settings/AboutSettings.tsx:78 #: src/screens/Settings/Settings.tsx:255 #: src/screens/Settings/Settings.tsx:258 @@ -852,10 +882,20 @@ msgstr "" msgid "Accept" msgstr "" +#. Accept a request to join a chat +#: src/screens/Messages/JoinRequests.tsx:460 +msgctxt "button" +msgid "Accept" +msgstr "Accept" + #: src/screens/Messages/components/RequestButtons.tsx:280 msgid "Accept chat request" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:454 +msgid "Accept join request" +msgstr "Accept join request" + #. Accept a chat request #: src/screens/Messages/components/RequestListItem.tsx:48 msgid "Accept Request" @@ -870,17 +910,21 @@ msgstr "" msgid "Accepted conversations" msgstr "Accepted conversations" +#: src/components/intents/GroupChatJoinDialog.tsx:93 +msgid "Access requested! The group owner will review your request." +msgstr "Access requested! The group owner will review your request." + #: src/screens/Settings/AccessibilitySettings.tsx:45 #: src/screens/Settings/Settings.tsx:233 #: src/screens/Settings/Settings.tsx:236 msgid "Accessibility" msgstr "" -#: src/Navigation.tsx:380 +#: src/Navigation.tsx:382 msgid "Accessibility Settings" msgstr "" -#: src/Navigation.tsx:396 +#: src/Navigation.tsx:398 #: src/screens/Login/LoginForm.tsx:192 #: src/screens/Settings/AccountSettings.tsx:56 #: src/screens/Settings/Settings.tsx:173 @@ -1023,11 +1067,11 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:1401 +#: src/view/com/composer/Composer.tsx:1383 msgid "Add another post" msgstr "" -#: src/view/com/composer/Composer.tsx:2067 +#: src/view/com/composer/Composer.tsx:2049 msgid "Add another post to thread" msgstr "" @@ -1058,7 +1102,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:499 +#: src/view/com/composer/SelectMediaButton.tsx:503 msgid "Add media to post" msgstr "" @@ -1086,8 +1130,8 @@ msgstr "" msgid "Add muted words and tags" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:135 -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:174 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:132 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:171 #: src/screens/ProfileList/AboutSection.tsx:72 #: src/screens/ProfileList/AboutSection.tsx:90 msgid "Add people" @@ -1344,9 +1388,9 @@ msgstr "" msgid "Already signed in as @{0}" msgstr "" -#: src/components/images/AutoSizedImage.tsx:190 -#: src/components/images/Gallery/index.tsx:522 -#: src/components/images/ImageLayoutGridItem.tsx:120 +#: src/components/images/AutoSizedImage.tsx:203 +#: src/components/images/Gallery/index.tsx:531 +#: src/components/images/ImageLayoutGridItem.tsx:138 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94 #: src/view/com/composer/GifAltText.tsx:100 #: src/view/com/composer/photos/Gallery.tsx:209 @@ -1393,7 +1437,7 @@ msgstr "" msgid "An error occurred" msgstr "" -#: src/view/com/composer/state/video.ts:411 +#: src/view/com/composer/state/video.ts:401 msgid "An error occurred while compressing the video." msgstr "" @@ -1438,11 +1482,11 @@ msgstr "" msgid "An error occurred while trying to follow all" msgstr "" -#: src/view/com/composer/state/video.ts:468 +#: src/view/com/composer/state/video.ts:453 msgid "An error occurred while uploading the video. {message}" msgstr "" -#: src/view/com/composer/state/video.ts:460 +#: src/view/com/composer/state/video.ts:445 msgid "An error occurred while uploading the video. Please check your internet connection and try again." msgstr "" @@ -1468,8 +1512,8 @@ msgid "An illustration showing that Bluesky selects trusted verifiers, and trust msgstr "" #: src/screens/Messages/components/InviteLinkDialog.tsx:172 -msgid "An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time." -msgstr "An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time." +msgid "An invite link lets people join this group chat without being added directly. You control who can join the chat. You can disable the link at any time." +msgstr "An invite link lets people join this group chat without being added directly. You control who can join the chat. You can disable the link at any time." #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:239 msgid "An issue not included in these options" @@ -1545,6 +1589,10 @@ msgstr "" msgid "Anyone can interact" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:322 +msgid "Anyone can join" +msgstr "Anyone can join" + #: src/screens/Messages/components/InviteLinkDialog.tsx:143 #: src/screens/Messages/components/InviteLinkDialog.tsx:144 msgid "Anyone can join instantly" @@ -1561,11 +1609,11 @@ msgstr "Anyone can request to join" msgid "Anyone who follows me" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:436 +#: src/screens/Messages/components/InviteLinkDialog.tsx:446 msgid "Anyone who has it will no longer be able to join or request to join. You can always create a new one." msgstr "Anyone who has it will no longer be able to join or request to join. You can always create a new one." -#: src/Navigation.tsx:465 +#: src/Navigation.tsx:467 #: src/screens/Settings/AppIconSettings/index.tsx:65 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:19 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:24 @@ -1603,7 +1651,7 @@ msgstr "" msgid "App passwords" msgstr "" -#: src/Navigation.tsx:348 +#: src/Navigation.tsx:350 #: src/screens/Settings/AppPasswords.tsx:51 msgid "App Passwords" msgstr "" @@ -1645,7 +1693,7 @@ msgstr "" msgid "Appeal this decision" msgstr "" -#: src/Navigation.tsx:388 +#: src/Navigation.tsx:390 #: src/screens/Settings/AppearanceSettings.tsx:73 #: src/screens/Settings/Settings.tsx:225 #: src/screens/Settings/Settings.tsx:228 @@ -1710,7 +1758,7 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:1541 +#: src/view/com/composer/Composer.tsx:1523 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1758,7 +1806,7 @@ msgstr "Automated account" msgid "Automation label" msgstr "Automation label" -#: src/Navigation.tsx:404 +#: src/Navigation.tsx:406 #: src/screens/Settings/AutomationLabelSettings.tsx:103 msgid "Automation Label" msgstr "Automation Label" @@ -1776,9 +1824,9 @@ msgstr "" #: src/components/dms/AddMembersFlow.tsx:342 #: src/components/dms/AddMembersFlow.tsx:509 #: src/components/dms/AddMembersFlow.tsx:515 -#: src/components/dms/InitiateChatFlow.tsx:508 -#: src/components/dms/InitiateChatFlow.tsx:701 -#: src/components/dms/InitiateChatFlow.tsx:708 +#: src/components/dms/InitiateChatFlow.tsx:509 +#: src/components/dms/InitiateChatFlow.tsx:702 +#: src/components/dms/InitiateChatFlow.tsx:709 #: src/components/moderation/LabelsOnMeDialog.tsx:334 #: src/components/moderation/LabelsOnMeDialog.tsx:335 #: src/screens/Login/ChooseAccountForm.tsx:98 @@ -1791,6 +1839,8 @@ msgstr "" #: src/screens/Login/SetNewPasswordForm.tsx:178 #: src/screens/Messages/components/ChatDisabled.tsx:164 #: src/screens/Messages/components/ChatDisabled.tsx:166 +#: src/screens/Messages/components/InviteLinkDialog.tsx:256 +#: src/screens/Messages/components/InviteLinkDialog.tsx:284 #: src/screens/Messages/Inbox.tsx:242 #: src/screens/Profile/Header/Shell.tsx:174 #: src/screens/Settings/components/ChangePasswordDialog.tsx:273 @@ -1970,7 +2020,7 @@ msgstr "" msgid "Blocked accounts" msgstr "" -#: src/Navigation.tsx:189 +#: src/Navigation.tsx:191 #: src/view/screens/ModerationBlockedAccounts.tsx:95 msgid "Blocked Accounts" msgstr "" @@ -2143,11 +2193,13 @@ msgstr "" msgid "Button to go back to the home timeline" msgstr "" +#. The owner (creator) of a group chat. +#. placeholder {0}: createSanitizedDisplayName( data.joinLinkPreviews[0].owner, true, moderateProfile( data.joinLinkPreviews[0].owner, moderationOpts, ).ui('displayName'), ) #. placeholder {0}: sanitizeHandle(handle, '@') #. placeholder {0}: sanitizeHandle(item.feed.creator.handle, '@') -#. placeholder {0}: sanitizeHandle(labeler.creator.handle, '@') #: src/components/LabelingServiceCard/index.tsx:62 #: src/components/moderation/ReportDialog/index.tsx:845 +#: src/screens/Messages/JoinRequest.tsx:166 #: src/screens/Search/components/StarterPackCard.tsx:107 #: src/screens/Search/Explore.tsx:971 msgid "By {0}" @@ -2218,8 +2270,8 @@ msgstr "" #: src/features/liveNow/components/GoLiveDialog.tsx:254 #: src/lib/media/picker.tsx:38 #: src/screens/Deactivated.tsx:150 -#: src/screens/Messages/components/InviteLinkDialog.tsx:458 -#: src/screens/Messages/components/InviteLinkDialog.tsx:462 +#: src/screens/Messages/components/InviteLinkDialog.tsx:468 +#: src/screens/Messages/components/InviteLinkDialog.tsx:472 #: src/screens/Messages/ConversationSettings/prompts.tsx:70 #: src/screens/Messages/ConversationSettings/prompts.tsx:94 #: src/screens/Profile/Header/EditProfileDialog.tsx:215 @@ -2234,8 +2286,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:300 #: src/screens/Takendown.tsx:102 #: src/screens/Takendown.tsx:105 -#: src/view/com/composer/Composer.tsx:1619 -#: src/view/com/composer/Composer.tsx:1629 +#: src/view/com/composer/Composer.tsx:1601 +#: src/view/com/composer/Composer.tsx:1611 #: src/view/com/composer/photos/EditImageDialog.web.tsx:44 #: src/view/com/composer/photos/EditImageDialog.web.tsx:53 #: src/view/shell/desktop/LeftNav.tsx:227 @@ -2270,7 +2322,7 @@ msgstr "" msgid "Captions & alt text" msgstr "" -#: src/components/images/Gallery/index.tsx:255 +#: src/components/images/Gallery/index.tsx:256 msgid "carousel" msgstr "carousel" @@ -2350,7 +2402,7 @@ msgid "Changes to the starter pack will not be reflected in the list after creat msgstr "" #: src/lib/hooks/useNotificationHandler.ts:133 -#: src/Navigation.tsx:483 +#: src/Navigation.tsx:485 #: src/view/shell/bottom-bar/BottomBar.tsx:229 #: src/view/shell/desktop/LeftNav.tsx:697 #: src/view/shell/Drawer.tsx:469 @@ -2367,6 +2419,11 @@ msgstr "" msgid "Chat ended" msgstr "Chat ended" +#: src/components/intents/GroupChatJoinDialog.tsx:227 +#: src/components/intents/GroupChatJoinDialog.tsx:258 +msgid "Chat invite link no longer available" +msgstr "Chat invite link no longer available" + #: src/components/dms/getSystemMessageInfo.ts:104 msgid "Chat locked" msgstr "Chat locked" @@ -2388,7 +2445,7 @@ msgstr "" msgid "Chat recipient is not followed by the sender." msgstr "Chat recipient is not followed by the sender." -#: src/Navigation.tsx:498 +#: src/Navigation.tsx:505 msgid "Chat request inbox" msgstr "" @@ -2398,7 +2455,7 @@ msgid "Chat requests" msgstr "" #: src/components/dms/ConvoMenu.tsx:84 -#: src/Navigation.tsx:493 +#: src/Navigation.tsx:500 #: src/screens/Messages/ChatList.tsx:83 #: src/screens/Messages/ChatList.tsx:87 #: src/screens/Messages/ChatList.tsx:500 @@ -2485,7 +2542,7 @@ msgstr "" msgid "Choose this color as your avatar" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:211 +#: src/screens/Messages/components/InviteLinkDialog.tsx:222 msgid "Choose who can join this group chat and how." msgstr "Choose who can join this group chat and how." @@ -2539,7 +2596,7 @@ msgstr "" msgid "click here" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:131 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:128 msgid "Click here to add people to this group chat" msgstr "Click here to add people to this group chat" @@ -2547,7 +2604,7 @@ msgstr "Click here to add people to this group chat" msgid "Click here to contact our support team" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:145 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:142 msgid "Click here to create or manage an invite link for this group chat" msgstr "Click here to create or manage an invite link for this group chat" @@ -2577,7 +2634,7 @@ msgstr "" msgid "Click here to update your email" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:146 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:143 msgid "Click here to view the invite link for this group chat" msgstr "Click here to view the invite link for this group chat" @@ -2621,7 +2678,9 @@ msgstr "" #: src/components/dms/AfterReportDialog.tsx:207 #: src/components/dms/AfterReportDialog.tsx:212 #: src/components/dms/EmojiPopup.android.tsx:59 -#: src/components/dms/InitiateChatFlow.tsx:533 +#: src/components/dms/InitiateChatFlow.tsx:534 +#: src/components/intents/GroupChatJoinDialog.tsx:204 +#: src/components/intents/GroupChatJoinDialog.tsx:239 #: src/components/NewskieDialog.tsx:169 #: src/components/NewskieDialog.tsx:175 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:107 @@ -2635,8 +2694,8 @@ msgstr "" #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:45 #: src/features/liveNow/components/EditLiveDialog.tsx:217 #: src/features/liveNow/components/EditLiveDialog.tsx:223 -#: src/screens/Messages/components/InviteLinkDialog.tsx:481 -#: src/screens/Messages/components/InviteLinkDialog.tsx:486 +#: src/screens/Messages/components/InviteLinkDialog.tsx:492 +#: src/screens/Messages/components/InviteLinkDialog.tsx:497 #: src/screens/Settings/components/ChangePasswordDialog.tsx:288 #: src/screens/Settings/components/ChangePasswordDialog.tsx:293 #: src/view/com/feeds/MissingFeed.tsx:211 @@ -2679,7 +2738,7 @@ msgid "Close image" msgstr "" #: src/components/Lightbox/Lightbox.web.tsx:73 -#: src/components/Lightbox/Lightbox.web.tsx:326 +#: src/components/Lightbox/Lightbox.web.tsx:330 msgid "Close image viewer" msgstr "" @@ -2690,6 +2749,10 @@ msgstr "" msgid "Close menu" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:197 +#: src/components/intents/GroupChatJoinDialog.tsx:198 +#: src/components/intents/GroupChatJoinDialog.tsx:234 +#: src/components/intents/GroupChatJoinDialog.tsx:235 #: src/components/Menu/index.tsx:361 msgid "Close this dialog" msgstr "" @@ -2702,7 +2765,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:1627 +#: src/view/com/composer/Composer.tsx:1609 msgid "Closes post composer and discards post draft" msgstr "" @@ -2732,7 +2795,7 @@ msgid "Comics" msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:46 -#: src/Navigation.tsx:338 +#: src/Navigation.tsx:340 #: src/view/screens/CommunityGuidelines.tsx:28 msgid "Community Guidelines" msgstr "" @@ -2752,7 +2815,7 @@ msgid "Compose new post" msgstr "" #. placeholder {0}: MAX_GRAPHEME_LENGTH || 0 -#: src/view/com/composer/Composer.tsx:1503 +#: src/view/com/composer/Composer.tsx:1485 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -2760,11 +2823,11 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:2464 +#: src/view/com/composer/Composer.tsx:2446 msgid "Compressing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2466 +#: src/view/com/composer/Composer.tsx:2448 msgid "Compressing video..." msgstr "" @@ -2849,7 +2912,7 @@ msgstr "" msgid "Content and media" msgstr "" -#: src/Navigation.tsx:441 +#: src/Navigation.tsx:443 msgid "Content and Media" msgstr "" @@ -2916,7 +2979,7 @@ msgid "Continue thread..." msgstr "" #: src/components/dms/AddMembersFlow.tsx:307 -#: src/components/dms/InitiateChatFlow.tsx:460 +#: src/components/dms/InitiateChatFlow.tsx:461 msgid "Continue to group name" msgstr "Continue to group name" @@ -2948,6 +3011,11 @@ msgctxt "toast" msgid "Conversation left" msgstr "Conversation left" +#: src/screens/Messages/JoinRequests.tsx:192 +#: src/screens/Messages/JoinRequests.tsx:224 +msgid "Conversation not found." +msgstr "Conversation not found." + #: src/screens/Settings/AboutSettings.tsx:162 msgid "Copied build version to clipboard" msgstr "" @@ -3059,7 +3127,7 @@ msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:41 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:108 -#: src/Navigation.tsx:343 +#: src/Navigation.tsx:345 #: src/view/screens/CopyrightPolicy.tsx:25 msgid "Copyright Policy" msgstr "" @@ -3146,7 +3214,7 @@ msgstr "" #. Text on button to create a new starter pack #: src/components/dialogs/StarterPackDialog.tsx:113 #: src/components/dialogs/StarterPackDialog.tsx:210 -#: src/components/dms/InitiateChatFlow.tsx:469 +#: src/components/dms/InitiateChatFlow.tsx:470 #: src/components/StarterPack/ProfileStarterPacks.tsx:329 msgid "Create" msgstr "" @@ -3166,7 +3234,7 @@ msgstr "" #: src/components/StarterPack/ProfileStarterPacks.tsx:207 #: src/components/StarterPack/ProfileStarterPacks.tsx:316 -#: src/Navigation.tsx:529 +#: src/Navigation.tsx:536 msgid "Create a starter pack" msgstr "" @@ -3181,6 +3249,7 @@ msgstr "" #: src/components/WelcomeModal.tsx:158 #: src/components/WelcomeModal.tsx:166 +#: src/screens/Messages/JoinRequest.tsx:300 #: src/screens/Signup/index.tsx:135 #: src/view/com/auth/SplashScreen.tsx:107 #: src/view/com/auth/SplashScreen.web.tsx:116 @@ -3213,7 +3282,7 @@ msgstr "" msgid "Create another" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:468 +#: src/components/dms/InitiateChatFlow.tsx:469 msgid "Create group chat" msgstr "Create group chat" @@ -3235,12 +3304,13 @@ msgstr "" msgid "Create moderation list" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:294 #: src/view/com/auth/SplashScreen.tsx:100 #: src/view/com/auth/SplashScreen.web.tsx:108 msgid "Create new account" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:488 +#: src/screens/Messages/ConversationSettings/index.tsx:487 msgid "Create or modify an invite link for this group chat" msgstr "Create or modify an invite link for this group chat" @@ -3263,8 +3333,8 @@ msgstr "" #. placeholder {0}: i18n.date(appPassword.createdAt, { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', }) #. placeholder {0}: i18n.date(createdAt, { dateStyle: 'long', timeStyle: 'short', }) #. placeholder {0}: i18n.date(createdAt, { month: 'long', day: 'numeric', year: 'numeric', }) -#: src/screens/Messages/components/InviteLinkDialog.tsx:312 -#: src/screens/Messages/ConversationSettings/index.tsx:441 +#: src/screens/Messages/components/InviteLinkDialog.tsx:329 +#: src/screens/Messages/ConversationSettings/index.tsx:440 #: src/screens/Settings/AppPasswords.tsx:174 msgid "Created {0}" msgstr "" @@ -3420,7 +3490,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:787 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:789 -#: src/view/com/composer/Composer.tsx:1515 +#: src/view/com/composer/Composer.tsx:1497 msgid "Delete post" msgstr "" @@ -3514,8 +3584,8 @@ msgstr "" msgid "Dim" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:344 -#: src/screens/Messages/components/InviteLinkDialog.tsx:349 +#: src/screens/Messages/components/InviteLinkDialog.tsx:361 +#: src/screens/Messages/components/InviteLinkDialog.tsx:366 msgid "Disable" msgstr "Disable" @@ -3541,8 +3611,8 @@ msgstr "" msgid "Disable haptic feedback" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:446 -#: src/screens/Messages/components/InviteLinkDialog.tsx:452 +#: src/screens/Messages/components/InviteLinkDialog.tsx:456 +#: src/screens/Messages/components/InviteLinkDialog.tsx:462 msgid "Disable link" msgstr "Disable link" @@ -3554,7 +3624,7 @@ msgstr "" msgid "Disable replies entirely" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:426 +#: src/screens/Messages/components/InviteLinkDialog.tsx:443 msgid "Disable this invite link?" msgstr "Disable this invite link?" @@ -3567,9 +3637,9 @@ msgstr "" #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:101 #: src/screens/Profile/Header/EditProfileDialog.tsx:79 -#: src/view/com/composer/Composer.tsx:1294 -#: src/view/com/composer/Composer.tsx:1338 -#: src/view/com/composer/Composer.tsx:1548 +#: src/view/com/composer/Composer.tsx:1276 +#: src/view/com/composer/Composer.tsx:1320 +#: src/view/com/composer/Composer.tsx:1530 #: src/view/com/composer/drafts/DraftItem.tsx:242 #: src/view/com/composer/drafts/DraftsButton.tsx:131 msgid "Discard" @@ -3580,14 +3650,14 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1292 +#: src/view/com/composer/Composer.tsx:1274 #: src/view/com/composer/drafts/DraftItem.tsx:239 #: src/view/com/composer/drafts/DraftsButton.tsx:98 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:1309 -#: src/view/com/composer/Composer.tsx:1540 +#: src/view/com/composer/Composer.tsx:1291 +#: src/view/com/composer/Composer.tsx:1522 msgid "Discard post?" msgstr "" @@ -3624,7 +3694,7 @@ msgstr "" msgid "Dismiss banner" msgstr "" -#: src/view/com/composer/Composer.tsx:2385 +#: src/view/com/composer/Composer.tsx:2367 msgid "Dismiss error" msgstr "" @@ -3760,8 +3830,8 @@ msgctxt "button" msgid "Download chat data" msgstr "Download chat data" -#: src/components/Lightbox/Lightbox.web.tsx:304 -#: src/components/Lightbox/Lightbox.web.tsx:316 +#: src/components/Lightbox/Lightbox.web.tsx:308 +#: src/components/Lightbox/Lightbox.web.tsx:320 msgid "Download image" msgstr "Download image" @@ -3833,7 +3903,7 @@ msgstr "" msgid "Eating disorders" msgstr "" -#: src/screens/Messages/components/EditTextButton.tsx:51 +#: src/screens/Messages/components/EditTextButton.tsx:52 #: src/screens/Settings/AccountSettings.tsx:150 #: src/screens/StarterPack/StarterPackScreen.tsx:604 #: src/screens/StarterPack/Wizard/index.tsx:331 @@ -3877,7 +3947,16 @@ msgstr "" msgid "Edit interests" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:326 +#: src/screens/Messages/JoinRequests.tsx:294 +msgid "Edit invite link" +msgstr "Edit invite link" + +#: src/screens/Messages/JoinRequests.tsx:300 +msgctxt "button" +msgid "Edit invite link" +msgstr "Edit invite link" + +#: src/screens/Messages/components/InviteLinkDialog.tsx:343 msgid "Edit link settings" msgstr "Edit link settings" @@ -3895,12 +3974,12 @@ msgstr "" msgid "Edit moderation list" msgstr "" -#: src/Navigation.tsx:353 +#: src/Navigation.tsx:355 #: src/view/screens/Feeds.tsx:511 msgid "Edit My Feeds" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:475 +#: src/screens/Messages/ConversationSettings/index.tsx:474 msgid "Edit name" msgstr "Edit name" @@ -3929,7 +4008,7 @@ msgstr "" msgid "Edit starter pack" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:474 +#: src/screens/Messages/ConversationSettings/index.tsx:473 msgid "Edit this group chat’s name" msgstr "Edit this group chat’s name" @@ -3941,7 +4020,7 @@ msgstr "" msgid "Edit who can reply" msgstr "" -#: src/Navigation.tsx:534 +#: src/Navigation.tsx:541 msgid "Edit your starter pack" msgstr "" @@ -4146,7 +4225,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:2484 +#: src/view/com/composer/Composer.tsx:2466 #: src/view/com/util/error/ErrorScreen.tsx:40 msgid "Error" msgstr "" @@ -4220,8 +4299,8 @@ msgstr "" msgid "Exit fullscreen" msgstr "" -#: src/components/Lightbox/chrome/Footer.tsx:51 -#: src/components/Lightbox/Lightbox.web.tsx:237 +#: src/components/Lightbox/chrome/Footer.tsx:65 +#: src/components/Lightbox/Lightbox.web.tsx:241 msgid "Expand alt text" msgstr "" @@ -4241,7 +4320,7 @@ msgstr "" msgid "Expands or collapses post text" msgstr "" -#: src/lib/api/index.ts:460 +#: src/lib/api/index.ts:441 msgid "Expected uri to resolve to a record" msgstr "" @@ -4280,7 +4359,7 @@ msgstr "" msgid "Explicit sexual images." msgstr "" -#: src/Navigation.tsx:738 +#: src/Navigation.tsx:745 #: src/screens/Search/Shell.tsx:362 #: src/view/shell/desktop/LeftNav.tsx:676 #: src/view/shell/Drawer.tsx:417 @@ -4322,7 +4401,7 @@ msgstr "" msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." msgstr "" -#: src/Navigation.tsx:372 +#: src/Navigation.tsx:374 #: src/screens/Settings/ExternalMediaPreferences.tsx:35 msgid "External Media Preferences" msgstr "" @@ -4336,6 +4415,10 @@ msgctxt "toast" msgid "Failed to accept chat" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:186 +msgid "Failed to accept join request" +msgstr "Failed to accept join request" + #: src/components/dms/ActionsWrapper.web.tsx:77 #: src/components/dms/MessageContextMenu.tsx:119 msgid "Failed to add emoji reaction" @@ -4354,7 +4437,7 @@ msgstr "" msgid "Failed to change handle. Please try again." msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:294 +#: src/components/Lightbox/Lightbox.web.tsx:298 msgid "Failed to copy link" msgstr "Failed to copy link" @@ -4403,7 +4486,7 @@ msgstr "Failed to disable invite link" msgid "Failed to disconnect Germ DM. Error: {0}" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:345 +#: src/screens/Messages/ConversationSettings/index.tsx:344 msgid "Failed to edit group chat name" msgstr "Failed to edit group chat name" @@ -4427,13 +4510,21 @@ msgstr "" msgid "Failed to hide suggestion, please check your internet connection" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:218 +msgid "Failed to ignore join request" +msgstr "Failed to ignore join request" + +#: src/components/intents/GroupChatJoinDialog.tsx:116 +msgid "Failed to join the group chat. Please try again." +msgstr "Failed to join the group chat. Please try again." + #: src/components/contacts/screens/ViewMatches.tsx:582 msgid "Failed to launch SMS app" msgstr "" #: src/screens/Messages/components/ChatEnded.tsx:42 #: src/screens/Messages/components/ChatLocked.tsx:53 -#: src/screens/Messages/ConversationSettings/index.tsx:372 +#: src/screens/Messages/ConversationSettings/index.tsx:371 msgctxt "toast" msgid "Failed to leave group chat" msgstr "Failed to leave group chat" @@ -4484,7 +4575,7 @@ msgstr "" msgid "Failed to load suggested follows" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:393 +#: src/screens/Messages/ConversationSettings/index.tsx:392 msgid "Failed to lock group chat" msgstr "Failed to lock group chat" @@ -4493,7 +4584,7 @@ msgstr "Failed to lock group chat" msgid "Failed to mark all requests as read" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:359 +#: src/screens/Messages/ConversationSettings/index.tsx:358 msgid "Failed to mute group chat" msgstr "Failed to mute group chat" @@ -4533,15 +4624,19 @@ msgstr "Failed to remove group chat member" msgid "Failed to remove verification" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:150 +msgid "Failed to rescind your request. Please try again." +msgstr "Failed to rescind your request. Please try again." + #: src/components/dialogs/DeviceLocationRequestDialog.tsx:81 msgid "Failed to resolve location. Please try again." msgstr "" -#: src/view/com/composer/Composer.tsx:584 +#: src/view/com/composer/Composer.tsx:578 msgid "Failed to save draft" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:311 +#: src/components/Lightbox/Lightbox.web.tsx:315 msgid "Failed to save image" msgstr "Failed to save image" @@ -4580,7 +4675,7 @@ msgid "Failed to toggle thread mute, please try again" msgstr "" #: src/screens/Messages/components/ChatLocked.tsx:42 -#: src/screens/Messages/ConversationSettings/index.tsx:396 +#: src/screens/Messages/ConversationSettings/index.tsx:395 msgid "Failed to unlock group chat" msgstr "Failed to unlock group chat" @@ -4632,7 +4727,7 @@ msgstr "" msgid "False information about elections" msgstr "" -#: src/Navigation.tsx:288 +#: src/Navigation.tsx:290 msgid "Feed" msgstr "" @@ -4677,7 +4772,7 @@ msgctxt "toast" msgid "Feedback sent to feed operator" msgstr "" -#: src/Navigation.tsx:514 +#: src/Navigation.tsx:521 #: src/screens/SavedFeeds.tsx:112 #: src/screens/SavedFeeds.tsx:303 #: src/screens/Search/SearchResults.tsx:80 @@ -4759,8 +4854,8 @@ msgstr "" msgid "Find accounts to follow" msgstr "" -#: src/Navigation.tsx:428 -#: src/Navigation.tsx:556 +#: src/Navigation.tsx:430 +#: src/Navigation.tsx:563 msgid "Find Contacts" msgstr "" @@ -4935,8 +5030,12 @@ msgstr "" msgid "Followed by <0>{0}, <1>{1}, and {2, plural, one {# other} other {# others}}" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:321 +msgid "Followers can join" +msgstr "Followers can join" + #. placeholder {0}: route.params.name -#: src/Navigation.tsx:242 +#: src/Navigation.tsx:244 msgid "Followers of @{0} that you know" msgstr "" @@ -4986,7 +5085,7 @@ msgstr "" msgid "Following feed preferences" msgstr "" -#: src/Navigation.tsx:359 +#: src/Navigation.tsx:361 #: src/screens/Settings/FollowingFeedPreferences.tsx:57 msgid "Following Feed Preferences" msgstr "" @@ -5077,17 +5176,17 @@ msgstr "" msgid "Generate a starter pack" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:206 -#: src/screens/Messages/components/InviteLinkDialog.tsx:244 -#: src/screens/Messages/components/InviteLinkDialog.tsx:268 +#: src/screens/Messages/components/InviteLinkDialog.tsx:217 +#: src/screens/Messages/components/InviteLinkDialog.tsx:257 +#: src/screens/Messages/components/InviteLinkDialog.tsx:285 msgid "Generate invite link" msgstr "Generate invite link" -#: src/screens/Messages/components/InviteLinkDialog.tsx:397 +#: src/screens/Messages/components/InviteLinkDialog.tsx:414 msgid "Generate new invite link" msgstr "Generate new invite link" -#: src/screens/Messages/components/InviteLinkDialog.tsx:402 +#: src/screens/Messages/components/InviteLinkDialog.tsx:419 msgid "Generate new link" msgstr "Generate new link" @@ -5175,17 +5274,17 @@ msgstr "" #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:71 #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:81 -#: src/screens/Messages/components/InviteLinkDialog.tsx:187 -#: src/screens/Messages/components/InviteLinkDialog.tsx:194 +#: src/screens/Messages/components/InviteLinkDialog.tsx:197 +#: src/screens/Messages/components/InviteLinkDialog.tsx:204 msgid "Get started" msgstr "" -#: src/components/MediaPreview.tsx:136 +#: src/components/MediaPreview.tsx:147 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:69 msgid "GIF" msgstr "" -#: src/view/com/composer/Composer.tsx:2489 +#: src/view/com/composer/Composer.tsx:2471 msgid "GIF uploaded" msgstr "" @@ -5208,7 +5307,7 @@ msgstr "" #: src/screens/VideoFeed/components/Header.tsx:163 #: src/screens/VideoFeed/index.tsx:1168 #: src/screens/VideoFeed/index.tsx:1172 -#: src/view/com/auth/LoggedOut.tsx:89 +#: src/view/com/auth/LoggedOut.tsx:103 #: src/view/com/profile/ProfileFollowers.tsx:178 #: src/view/com/profile/ProfileFollowers.tsx:179 #: src/view/screens/NotFound.tsx:51 @@ -5334,37 +5433,42 @@ msgstr "" msgid "Grooming or predatory behavior" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:513 +#: src/components/intents/GroupChatJoinDialog.tsx:297 +#: src/screens/Messages/JoinRequest.tsx:116 +msgid "Group chat" +msgstr "Group chat" + +#: src/screens/Messages/components/InviteLinkDialog.tsx:522 msgid "Group chat invite link dialog" msgstr "Group chat invite link dialog" -#: src/screens/Messages/ConversationSettings/index.tsx:385 +#: src/screens/Messages/ConversationSettings/index.tsx:384 msgctxt "toast" msgid "Group chat locked" msgstr "Group chat locked" -#: src/screens/Messages/ConversationSettings/index.tsx:352 +#: src/screens/Messages/ConversationSettings/index.tsx:351 msgctxt "toast" msgid "Group chat muted" msgstr "Group chat muted" -#: src/screens/Messages/ConversationSettings/index.tsx:340 +#: src/screens/Messages/ConversationSettings/index.tsx:339 msgctxt "toast" msgid "Group chat name updated" msgstr "Group chat name updated" -#: src/Navigation.tsx:488 +#: src/Navigation.tsx:490 #: src/screens/Messages/ConversationSettings/index.tsx:91 msgid "Group chat settings" msgstr "Group chat settings" #: src/screens/Messages/components/ChatLocked.tsx:38 -#: src/screens/Messages/ConversationSettings/index.tsx:387 +#: src/screens/Messages/ConversationSettings/index.tsx:386 msgctxt "toast" msgid "Group chat unlocked" msgstr "Group chat unlocked" -#: src/screens/Messages/ConversationSettings/index.tsx:354 +#: src/screens/Messages/ConversationSettings/index.tsx:353 msgctxt "toast" msgid "Group chat unmuted" msgstr "Group chat unmuted" @@ -5377,12 +5481,17 @@ msgstr "Group chats are not yet available" msgid "Group chats are now available" msgstr "Group chats are now available" +#. placeholder {0}: import {useState} from 'react' import {View} from 'react-native' import { type ChatBskyGroupDefs, moderateProfile, type ModerationOpts, } from '@atproto/api' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {shareUrl} from '#/lib/sharing' import {useCreateJoinLink} from '#/state/queries/messages/create-join-link' import {useDisableJoinLink} from '#/state/queries/messages/disable-join-link' import {useEditJoinLink} from '#/state/queries/messages/edit-join-link' import {useEnableJoinLink} from '#/state/queries/messages/enable-join-link' import {atoms as a, useTheme, web} from '#/alf' import { Button, ButtonIcon, ButtonText, StackedButton, } from '#/components/Button' import * as Dialog from '#/components/Dialog' import { type ConvoWithDetails, type GroupConvoMember, } from '#/components/dms/util' import * as Toggle from '#/components/forms/Toggle' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight' import {ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon} from '#/components/icons/ChainLink' import {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/EditBig' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' import {CopyTextButton} from './CopyTextButton' import {EditTextButton} from './EditTextButton' enum Step { INFO, GENERATE, MANAGE, CONFIRM_DISABLE, } export function InviteLinkDialog({ convo, control, owner, isOwner, moderationOpts, }: { convo: Extract control: Dialog.DialogOuterProps['control'] owner: GroupConvoMember isOwner: boolean moderationOpts: ModerationOpts }) { const t = useTheme() const {t: l, i18n} = useLingui() const ownerName = createSanitizedDisplayName( owner, false, moderateProfile(owner, moderationOpts).ui('displayName'), ) const {joinLink} = convo.details const defaultStep = joinLink ? Step.MANAGE : Step.INFO const defaultWhoCanJoin = joinLink ? joinLinkToKey(joinLink) : 'anyone' const [step, setStep] = useState(defaultStep) const [whoCanJoin, setWhoCanJoin] = useState(defaultWhoCanJoin) // Resync local state when the server-side join link rules change (mutation // success, refetch, or change from another client). Keyed on the rule string // so identity-only refetches don't bump a user mid-edit. const joinLinkRuleKey = joinLink ? joinLinkToKey(joinLink) : null const [prevKey, setPrevKey] = useState(joinLinkRuleKey) if (joinLinkRuleKey !== prevKey) { setStep(joinLinkRuleKey ? Step.MANAGE : Step.INFO) setWhoCanJoin(joinLinkRuleKey ?? 'anyone') setPrevKey(joinLinkRuleKey) } const {openComposer} = useOpenComposer() const {mutate: createJoinLink, isPending: isCreating} = useCreateJoinLink( convo.view.id, { onSuccess: () => { setStep(Step.MANAGE) }, onError: () => { Toast.show(l`Failed to create invite link`, { type: 'error', }) }, }, ) const {mutate: editJoinLink, isPending: isEditing} = useEditJoinLink( convo.view.id, { onSuccess: () => { setStep(Step.MANAGE) }, onError: () => { Toast.show(l`Failed to edit invite link`, { type: 'error', }) }, }, ) const {mutate: disableJoinLink, isPending: isDisabling} = useDisableJoinLink( convo.view.id, { onError: () => { Toast.show(l`Failed to disable invite link`, { type: 'error', }) }, }, ) const {mutate: enableJoinLink, isPending: isEnabling} = useEnableJoinLink( convo.view.id, { onError: () => { Toast.show(l`Failed to enable invite link`, { type: 'error', }) }, }, ) const isSaving = isCreating || isEditing const whoCanJoinOptions = [ { name: 'anyone', owner: l`Anyone can join instantly`, member: l`Anyone can join instantly`, }, { name: 'anyone:requireApproval', owner: l`Anyone can request to join`, member: l`Anyone can request to join`, }, { name: 'followedByOwner', owner: l`People I follow can join instantly`, member: l`People ${ownerName} follows can join instantly`, }, { name: 'followedByOwner:requireApproval', owner: l`People I follow can request to join`, member: l`People ${ownerName} follows can request to join`, }, ] let content: React.ReactNode = null let header: string | null = null switch (step) { case Step.INFO: { header = l`Invite link` content = ( <> An invite link lets people join this group chat without being added directly. You control who can join the chat. You can disable the link at any time. Group chats can only have a maximum of{' '} {plural(convo.details.memberLimit, { one: '# person', other: '# people', })} . Your name, avatar, the name of the group chat, and the number of members will be visible to everyone. ) break } case Step.GENERATE: { const linkEnabled = joinLink?.enabledStatus === 'enabled' const linkHasChanged = linkEnabled && joinLinkRuleKey !== whoCanJoin header = linkEnabled ? l`Update invite link` : l`Generate invite link` content = ( <> Choose who can join this group chat and how. setWhoCanJoin(value)}> {whoCanJoinOptions.map(option => ( {({selected}) => ( )} ))} ) break } case Step.MANAGE: { const linkEnabled = joinLink?.enabledStatus === 'enabled' const linkDisabled = joinLink?.enabledStatus === 'disabled' const joinLinkURI = joinLink?.code ? `https://bsky.app/c/${joinLink.code}` : 'https://bsky.app/' const createdAt = joinLink ? new Date(joinLink.createdAt) : null const currentOption = whoCanJoinOptions.find( o => o.name === (joinLink ? joinLinkToKey(joinLink) : null), ) ?? whoCanJoinOptions[0] const ownerValue = currentOption?.owner ?? whoCanJoinOptions[0].owner const memberValue = currentOption?.member ?? whoCanJoinOptions[0].member header = linkEnabled ? l`Invite link` : l`Invite link disabled` content = ( <> {joinLinkURI} {createdAt ? ( Created{' '} {i18n.date(createdAt, { dateStyle: 'long', timeStyle: 'short', })} ) : null} {linkEnabled ? ( {isOwner ? ( setStep(Step.GENERATE)}> {ownerValue} ) : ( {memberValue} )} ) : null} {linkEnabled ? ( {isOwner ? ( setStep(Step.CONFIRM_DISABLE)}> Disable ) : null} { control.close(() => { openComposer({ text: joinLinkURI, logContext: 'Other', }) }) }}> Post link { void shareUrl(joinLinkURI) }}> Share ) : ( )} ) break } case Step.CONFIRM_DISABLE: { content = ( <> Disable this invite link? Anyone who has it will no longer be able to join or request to join. You can always create a new one. ) break } } if (!isOwner && (!joinLink || joinLink.enabledStatus === 'disabled')) { header = l`Invite link` content = ( <> There is no invite link for this group chat. ) } return ( { setStep(defaultStep) setWhoCanJoin(defaultWhoCanJoin) }}> {header} } label={l`Group chat invite link dialog`} style={web({maxWidth: 400})}> {content} ) } function joinLinkToKey(joinLink: ChatBskyGroupDefs.JoinLinkView): string { return `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}` } function keyToJoinLink( key: string, ): Pick { const [joinRule, requireApproval] = key.split(':') return { joinRule, requireApproval: requireApproval === 'requireApproval', } } +#: src/screens/Messages/components/InviteLinkDialog.tsx:179 +msgid "Group chats can only have a maximum of {0}." +msgstr "Group chats can only have a maximum of {0}." + #: src/components/dialogs/SearchablePeopleList.tsx:569 msgid "Group is locked" msgstr "Group is locked" #: src/components/dms/InitiateChatFlow.tsx:237 -#: src/components/dms/InitiateChatFlow.tsx:568 +#: src/components/dms/InitiateChatFlow.tsx:569 #: src/screens/Messages/ConversationSettings/prompts.tsx:33 msgid "Group name" msgstr "Group name" @@ -5440,7 +5549,7 @@ msgstr "" msgid "Harming or endangering minors" msgstr "" -#: src/Navigation.tsx:472 +#: src/Navigation.tsx:474 msgid "Hashtag" msgstr "" @@ -5646,7 +5755,7 @@ msgstr "" msgid "Hmmmm, we couldn't load that moderation service." msgstr "" -#: src/view/com/composer/state/video.ts:430 +#: src/view/com/composer/state/video.ts:415 msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!" msgstr "" @@ -5654,8 +5763,8 @@ msgstr "" msgid "Hold your horses! This feature isn't available to you yet. Please check back later." msgstr "Hold your horses! This feature isn't available to you yet. Please check back later." -#: src/Navigation.tsx:733 -#: src/Navigation.tsx:754 +#: src/Navigation.tsx:740 +#: src/Navigation.tsx:761 #: src/view/shell/bottom-bar/BottomBar.tsx:184 #: src/view/shell/desktop/LeftNav.tsx:666 #: src/view/shell/Drawer.tsx:443 @@ -5715,7 +5824,7 @@ msgstr "" msgid "I'm on Bluesky as {0} - come find me! https://bsky.app/download" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:238 +#: src/components/Lightbox/Lightbox.web.tsx:242 msgid "If alt text is long, toggles alt text expanded state" msgstr "" @@ -5779,23 +5888,33 @@ msgstr "" msgid "If you're trying to change your handle or email, do so before you deactivate." msgstr "" -#: src/components/images/ImageLayoutGridItem.tsx:76 +#. Ignore a request to join a chat +#: src/screens/Messages/JoinRequests.tsx:485 +msgctxt "button" +msgid "Ignore" +msgstr "Ignore" + +#: src/screens/Messages/JoinRequests.tsx:479 +msgid "Ignore join request" +msgstr "Ignore join request" + +#: src/components/images/ImageLayoutGridItem.tsx:93 msgid "Image" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:427 +#: src/components/images/Gallery/index.tsx:436 msgid "Image {0}" msgstr "Image {0}" #. placeholder {0}: index + 1 #. placeholder {1}: imgs.length -#: src/components/Lightbox/Lightbox.web.tsx:253 +#: src/components/Lightbox/Lightbox.web.tsx:257 msgid "Image {0} of {1}" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:418 +#: src/components/images/Gallery/index.tsx:421 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5810,7 +5929,7 @@ msgid "Image cache cleared, freed {0}" msgstr "" #. placeholder {0}: images.length -#: src/components/images/Gallery/index.tsx:256 +#: src/components/images/Gallery/index.tsx:257 msgid "Image gallery, {0} images" msgstr "Image gallery, {0} images" @@ -5825,12 +5944,12 @@ msgid "Image is unavailable." msgstr "Image is unavailable." #: src/components/Lightbox/chrome/ImageMenu.tsx:73 -#: src/components/Lightbox/Lightbox.web.tsx:257 -#: src/components/Lightbox/Lightbox.web.tsx:265 +#: src/components/Lightbox/Lightbox.web.tsx:261 +#: src/components/Lightbox/Lightbox.web.tsx:269 msgid "Image options" msgstr "Image options" -#: src/components/Lightbox/Lightbox.web.tsx:308 +#: src/components/Lightbox/Lightbox.web.tsx:312 #: src/lib/media/save-image.ios.ts:25 #: src/lib/media/save-image.ts:29 msgid "Image saved" @@ -5956,6 +6075,10 @@ msgstr "" msgid "Invalid 2FA confirmation code." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:126 +msgid "Invalid group chat code." +msgstr "Invalid group chat code." + #: src/screens/Settings/components/ChangeHandleDialog.tsx:615 msgid "Invalid handle. Please try a different one." msgstr "" @@ -5974,6 +6097,10 @@ msgstr "" msgid "Invalid report subject" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:157 +msgid "Invalid rescind request." +msgstr "Invalid rescind request." + #: src/components/intents/VerifyEmailIntentDialog.tsx:86 msgid "Invalid Verification Code" msgstr "" @@ -6004,11 +6131,11 @@ msgid "Invite friends <0/>" msgstr "" #: src/screens/Messages/components/InviteLinkDialog.tsx:167 -#: src/screens/Messages/components/InviteLinkDialog.tsx:290 -#: src/screens/Messages/components/InviteLinkDialog.tsx:296 -#: src/screens/Messages/components/InviteLinkDialog.tsx:471 -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:151 -#: src/screens/Messages/ConversationSettings/index.tsx:491 +#: src/screens/Messages/components/InviteLinkDialog.tsx:309 +#: src/screens/Messages/components/InviteLinkDialog.tsx:315 +#: src/screens/Messages/components/InviteLinkDialog.tsx:482 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:148 +#: src/screens/Messages/ConversationSettings/index.tsx:490 msgid "Invite link" msgstr "Invite link" @@ -6017,7 +6144,7 @@ msgid "Invite link created" msgstr "Invite link created" #: src/components/dms/getSystemMessageInfo.ts:138 -#: src/screens/Messages/components/InviteLinkDialog.tsx:290 +#: src/screens/Messages/components/InviteLinkDialog.tsx:309 msgid "Invite link disabled" msgstr "Invite link disabled" @@ -6067,7 +6194,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:2404 +#: src/view/com/composer/Composer.tsx:2386 msgid "Job ID: {0}" msgstr "" @@ -6076,6 +6203,10 @@ msgstr "" msgid "Jobs" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:253 +msgid "Join" +msgstr "Join" + #: src/screens/StarterPack/StarterPackLandingScreen.tsx:210 #: src/screens/StarterPack/StarterPackLandingScreen.tsx:215 #: src/screens/StarterPack/StarterPackScreen.tsx:478 @@ -6083,17 +6214,30 @@ msgstr "" msgid "Join Bluesky" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:63 +#: src/components/intents/GroupChatJoinDialog.tsx:415 +msgid "Join group chat" +msgstr "Join group chat" + +#: src/components/intents/GroupChatJoinDialog.tsx:146 +msgid "Join request rescinded." +msgstr "Join request rescinded." + #: src/components/StarterPack/QrCode.tsx:72 #: src/view/shell/NavSignupCard.tsx:41 msgid "Join the conversation" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:420 +msgid "Join this group chat" +msgstr "Join this group chat" + #: src/lib/interests.ts:63 msgid "Journalism" msgstr "" -#: src/view/com/composer/Composer.tsx:1342 -#: src/view/com/composer/Composer.tsx:1352 +#: src/view/com/composer/Composer.tsx:1324 +#: src/view/com/composer/Composer.tsx:1334 #: src/view/com/composer/drafts/DraftsButton.tsx:135 msgid "Keep editing" msgstr "" @@ -6136,7 +6280,7 @@ msgstr "" msgid "Labels on your content" msgstr "" -#: src/Navigation.tsx:215 +#: src/Navigation.tsx:217 msgid "Language Settings" msgstr "" @@ -6246,7 +6390,7 @@ msgid "Learn more." msgstr "" #: src/components/dms/LeaveConvoPrompt.tsx:49 -#: src/screens/Messages/ConversationSettings/index.tsx:527 +#: src/screens/Messages/ConversationSettings/index.tsx:526 msgid "Leave" msgstr "" @@ -6283,7 +6427,7 @@ msgstr "Leave conversation" msgid "Leave group chat" msgstr "Leave group chat" -#: src/screens/Messages/ConversationSettings/index.tsx:526 +#: src/screens/Messages/ConversationSettings/index.tsx:525 msgid "Leave this group chat" msgstr "Leave this group chat" @@ -6347,8 +6491,8 @@ msgstr "" msgid "Like this labeler" msgstr "" -#: src/Navigation.tsx:293 -#: src/Navigation.tsx:298 +#: src/Navigation.tsx:295 +#: src/Navigation.tsx:300 msgid "Liked by" msgstr "" @@ -6394,11 +6538,11 @@ msgstr "" msgid "Linear" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:292 +#: src/components/Lightbox/Lightbox.web.tsx:296 msgid "Link copied to clipboard" msgstr "Link copied to clipboard" -#: src/Navigation.tsx:248 +#: src/Navigation.tsx:250 msgid "List" msgstr "" @@ -6484,7 +6628,7 @@ msgctxt "toast" msgid "List unmuted" msgstr "" -#: src/Navigation.tsx:169 +#: src/Navigation.tsx:171 #: src/view/screens/Lists.tsx:60 #: src/view/screens/Profile.tsx:233 #: src/view/screens/Profile.tsx:241 @@ -6580,7 +6724,7 @@ msgstr "" msgid "Loading..." msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:505 +#: src/screens/Messages/ConversationSettings/index.tsx:504 msgid "Lock" msgstr "Lock" @@ -6592,15 +6736,15 @@ msgstr "Lock group chat" msgid "Lock group chat?" msgstr "Lock group chat?" -#: src/screens/Messages/ConversationSettings/index.tsx:503 +#: src/screens/Messages/ConversationSettings/index.tsx:502 msgid "Lock this group chat" msgstr "Lock this group chat" -#: src/screens/Messages/ConversationSettings/index.tsx:505 +#: src/screens/Messages/ConversationSettings/index.tsx:504 msgid "Locked" msgstr "Locked" -#: src/Navigation.tsx:318 +#: src/Navigation.tsx:320 msgid "Log" msgstr "" @@ -6712,7 +6856,7 @@ msgid "Media that may be disturbing or inappropriate for some audiences." msgstr "" #. The heading above the list of chat members. -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:29 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:34 msgid "Members" msgstr "Members" @@ -6798,7 +6942,7 @@ msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" msgid "Message options" msgstr "" -#: src/Navigation.tsx:748 +#: src/Navigation.tsx:755 msgid "Messages" msgstr "" @@ -6827,7 +6971,7 @@ msgstr "" msgid "Missing media" msgstr "" -#: src/Navigation.tsx:174 +#: src/Navigation.tsx:176 #: src/screens/Moderation/index.tsx:100 msgid "Moderation" msgstr "" @@ -6871,7 +7015,7 @@ msgstr "" msgid "Moderation lists" msgstr "" -#: src/Navigation.tsx:179 +#: src/Navigation.tsx:181 #: src/view/screens/ModerationModlists.tsx:60 msgid "Moderation Lists" msgstr "" @@ -6880,7 +7024,7 @@ msgstr "" msgid "moderation settings" msgstr "" -#: src/Navigation.tsx:308 +#: src/Navigation.tsx:310 msgid "Moderation states" msgstr "" @@ -6926,7 +7070,7 @@ msgstr "" msgid "Music" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:467 +#: src/screens/Messages/ConversationSettings/index.tsx:466 msgid "Mute" msgstr "Mute" @@ -6971,7 +7115,7 @@ msgstr "" msgid "Mute these accounts?" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:465 +#: src/screens/Messages/ConversationSettings/index.tsx:464 msgid "Mute this group chat" msgstr "Mute this group chat" @@ -7009,7 +7153,7 @@ msgstr "" msgid "Mute words & tags" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:467 +#: src/screens/Messages/ConversationSettings/index.tsx:466 msgid "Muted" msgstr "Muted" @@ -7017,7 +7161,7 @@ msgstr "Muted" msgid "Muted accounts" msgstr "" -#: src/Navigation.tsx:184 +#: src/Navigation.tsx:186 #: src/view/screens/ModerationMutedAccounts.tsx:107 msgid "Muted Accounts" msgstr "" @@ -7124,19 +7268,19 @@ msgid "New chat" msgstr "" #. placeholder {0}: createSanitizedDisplayName(members[0]) -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:61 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:58 msgid "New chat with {0}" msgstr "New chat with {0}" #. placeholder {0}: createSanitizedDisplayName(members[0]) #. placeholder {1}: createSanitizedDisplayName(members[1]) -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:65 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:62 msgid "New chat with {0} and {1}" msgstr "New chat with {0} and {1}" #. placeholder {0}: createSanitizedDisplayName(members[0]) #. placeholder {1}: createSanitizedDisplayName(members[1]) -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:73 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:70 msgid "New chat with {0}, {1}, and {memberCount, plural, one {{memberCount} more} other {{memberCount} more}}." msgstr "New chat with {0}, {1}, and {memberCount, plural, one {{memberCount} more} other {{memberCount} more}}." @@ -7163,8 +7307,8 @@ msgstr "New group chat" #. Button used to create a new group chat. #. Button used to create a new group chat. -#: src/components/dms/InitiateChatFlow.tsx:743 -#: src/components/dms/InitiateChatFlow.tsx:777 +#: src/components/dms/InitiateChatFlow.tsx:744 +#: src/components/dms/InitiateChatFlow.tsx:778 msgctxt "action" msgid "New group chat" msgstr "New group chat" @@ -7236,7 +7380,7 @@ msgstr "" #: src/components/contacts/screens/ViewMatches.tsx:395 #: src/components/contacts/screens/ViewMatches.tsx:410 #: src/components/dms/AddMembersFlow.tsx:308 -#: src/components/dms/InitiateChatFlow.tsx:461 +#: src/components/dms/InitiateChatFlow.tsx:462 #: src/screens/Login/ForgotPasswordForm.tsx:149 #: src/screens/Login/ForgotPasswordForm.tsx:156 #: src/screens/Login/SetNewPasswordForm.tsx:186 @@ -7409,7 +7553,7 @@ msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:250 #: src/components/dms/AddMembersFlow.tsx:240 -#: src/components/dms/InitiateChatFlow.tsx:346 +#: src/components/dms/InitiateChatFlow.tsx:347 #: src/components/ProgressGuide/FollowDialog.tsx:221 msgid "No results" msgstr "" @@ -7491,7 +7635,7 @@ msgstr "" msgid "Not followed by anyone you’re following" msgstr "Not followed by anyone you’re following" -#: src/Navigation.tsx:164 +#: src/Navigation.tsx:166 #: src/view/screens/Profile.tsx:132 msgid "Not Found" msgstr "" @@ -7518,8 +7662,8 @@ msgid "Nothing saved yet" msgstr "" #: src/components/dialogs/NotificationSettingsDialog.tsx:64 -#: src/Navigation.tsx:435 -#: src/Navigation.tsx:509 +#: src/Navigation.tsx:437 +#: src/Navigation.tsx:516 #: src/view/screens/Notifications.tsx:134 msgid "Notification settings" msgstr "" @@ -7529,8 +7673,8 @@ msgstr "" msgid "Notification sounds" msgstr "" -#: src/Navigation.tsx:504 -#: src/Navigation.tsx:743 +#: src/Navigation.tsx:511 +#: src/Navigation.tsx:750 #: src/screens/Notifications/ActivityList.tsx:31 #: src/screens/Settings/NotificationSettings/index.tsx:104 #: src/screens/Settings/Settings.tsx:197 @@ -7579,7 +7723,7 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 -#: src/components/dms/InitiateChatFlow.tsx:676 +#: src/components/dms/InitiateChatFlow.tsx:677 #: src/components/dms/MessageItem.tsx:659 #: src/screens/Login/PasswordUpdatedForm.tsx:37 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:661 @@ -7613,27 +7757,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:793 +#: src/view/com/composer/Composer.tsx:787 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:790 +#: src/view/com/composer/Composer.tsx:784 msgid "One or more images is missing alt text." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:411 +#: src/view/com/composer/SelectMediaButton.tsx:415 msgid "One or more of your selected files are not supported." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:434 -msgid "One or more of your selected files are too large. Maximum size is 100 MB." -msgstr "" +#: src/view/com/composer/SelectMediaButton.tsx:438 +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:595 +#: src/view/com/composer/Composer.tsx:589 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:800 +#: src/view/com/composer/Composer.tsx:794 msgid "One or more videos is missing alt text." msgstr "" @@ -7642,6 +7786,18 @@ msgstr "" msgid "Only {0} can reply." msgstr "" +#: src/screens/Messages/JoinRequests.tsx:196 +msgid "Only admins can accept join requests." +msgstr "Only admins can accept join requests." + +#: src/screens/Messages/JoinRequests.tsx:228 +msgid "Only admins can ignore join requests." +msgstr "Only admins can ignore join requests." + +#: src/components/intents/GroupChatJoinDialog.tsx:124 +msgid "Only followers can join this group chat." +msgstr "Only followers can join this group chat." + #: src/screens/Settings/ActivityPrivacySettings.tsx:121 #: src/screens/Settings/ActivityPrivacySettings.tsx:126 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:158 @@ -7652,6 +7808,15 @@ msgstr "" msgid "Only image files are supported" msgstr "" +#. placeholder {0}: createSanitizedDisplayName( data.joinLinkPreviews[0].owner, true, moderateProfile( data.joinLinkPreviews[0].owner, moderationOpts, ).ui('displayName'), ) +#: src/screens/Messages/JoinRequest.tsx:207 +msgid "Only people {0} follows can join." +msgstr "Only people {0} follows can join." + +#: src/components/intents/GroupChatJoinDialog.tsx:268 +msgid "Only people the chat owner follows can join" +msgstr "Only people the chat owner follows can join" + #: src/view/com/composer/videos/SubtitleFilePicker.tsx:41 msgid "Only WebVTT (.vtt) files are supported" msgstr "" @@ -7696,7 +7861,7 @@ msgstr "" #: src/screens/Messages/components/MessageComposer.tsx:176 #: src/screens/Messages/components/MessageInput.web.tsx:148 -#: src/view/com/composer/Composer.tsx:2044 +#: src/view/com/composer/Composer.tsx:2026 msgid "Open emoji picker" msgstr "" @@ -7717,6 +7882,10 @@ msgstr "" msgid "Open Germ DM" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:399 +msgid "Open group chat" +msgstr "Open group chat" + #: src/components/dms/MessagesListHeader.tsx:181 #: src/components/dms/MessagesListHeader.tsx:220 msgid "Open group chat settings" @@ -7774,6 +7943,10 @@ msgstr "" msgid "Open system log" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:400 +msgid "Open this group chat" +msgstr "Open this group chat" + #. placeholder {0}: feedInfo.displayName #: src/view/shell/desktop/Feeds.tsx:185 msgid "Opens {0} feed" @@ -7816,21 +7989,23 @@ msgid "Opens device camera" msgstr "" #. Accessibility hint for button in composer to add images, a video, or a GIF to a post. Maximum number of images that can be selected is currently 4 but may change. -#: src/view/com/composer/SelectMediaButton.tsx:505 +#: src/view/com/composer/SelectMediaButton.tsx:509 msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." msgstr "" +#: src/screens/Messages/JoinRequest.tsx:295 #: src/view/com/auth/SplashScreen.tsx:102 #: src/view/com/auth/SplashScreen.web.tsx:110 msgid "Opens flow to create a new Bluesky account" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:281 #: src/view/com/auth/SplashScreen.tsx:120 #: src/view/com/auth/SplashScreen.web.tsx:124 msgid "Opens flow to sign in to your existing Bluesky account" msgstr "" -#: src/components/images/Gallery/index.tsx:428 +#: src/components/images/Gallery/index.tsx:437 msgid "Opens full image" msgstr "Opens full image" @@ -8031,12 +8206,12 @@ msgid "People {ownerName} follows can request to join" msgstr "People {ownerName} follows can request to join" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:235 +#: src/Navigation.tsx:237 msgid "People followed by @{0}" msgstr "" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:228 +#: src/Navigation.tsx:230 msgid "People following @{0}" msgstr "" @@ -8198,7 +8373,7 @@ msgstr "" msgid "Please complete the verification captcha." msgstr "" -#: src/view/com/composer/state/video.ts:454 +#: src/view/com/composer/state/video.ts:439 msgid "Please confirm your email address to upload videos." msgstr "" @@ -8337,7 +8512,7 @@ msgstr "" msgid "Porn" msgstr "" -#: src/view/com/composer/Composer.tsx:1693 +#: src/view/com/composer/Composer.tsx:1675 msgctxt "action" msgid "Post" msgstr "" @@ -8357,12 +8532,12 @@ msgstr "" msgid "Post a video" msgstr "" -#: src/view/com/composer/Composer.tsx:1691 +#: src/view/com/composer/Composer.tsx:1673 msgctxt "action" msgid "Post All" msgstr "" -#: src/view/com/composer/Composer.tsx:1351 +#: src/view/com/composer/Composer.tsx:1333 msgid "Post anyway" msgstr "Post anyway" @@ -8371,10 +8546,10 @@ msgid "Post blocked" msgstr "" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:261 -#: src/Navigation.tsx:268 -#: src/Navigation.tsx:275 -#: src/Navigation.tsx:282 +#: src/Navigation.tsx:263 +#: src/Navigation.tsx:270 +#: src/Navigation.tsx:277 +#: src/Navigation.tsx:284 msgid "Post by @{0}" msgstr "" @@ -8383,7 +8558,7 @@ msgctxt "toast" msgid "Post deleted" msgstr "" -#: src/lib/api/index.ts:193 +#: src/lib/api/index.ts:186 msgid "Post failed to upload. Please check your Internet connection and try again." msgstr "" @@ -8408,7 +8583,7 @@ msgstr "" msgid "Post interaction settings" msgstr "" -#: src/Navigation.tsx:195 +#: src/Navigation.tsx:197 #: src/screens/ModerationInteractionSettings/index.tsx:35 msgid "Post Interaction Settings" msgstr "" @@ -8418,8 +8593,8 @@ msgstr "" msgid "Post language selection" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:354 -#: src/screens/Messages/components/InviteLinkDialog.tsx:366 +#: src/screens/Messages/components/InviteLinkDialog.tsx:371 +#: src/screens/Messages/components/InviteLinkDialog.tsx:383 msgid "Post link" msgstr "Post link" @@ -8477,6 +8652,7 @@ msgstr "" #: src/components/Error.tsx:61 #: src/components/Lists.tsx:104 #: src/screens/Messages/components/MessageListError.tsx:23 +#: src/screens/Messages/JoinRequests.tsx:350 #: src/screens/Signup/BackNextButtons.tsx:48 msgid "Press to retry" msgstr "" @@ -8508,8 +8684,8 @@ msgstr "" msgid "Privacy and security" msgstr "" -#: src/Navigation.tsx:412 -#: src/Navigation.tsx:420 +#: src/Navigation.tsx:414 +#: src/Navigation.tsx:422 #: src/screens/Settings/ActivityPrivacySettings.tsx:41 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:45 msgid "Privacy and Security" @@ -8521,7 +8697,7 @@ msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:36 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:103 -#: src/Navigation.tsx:328 +#: src/Navigation.tsx:330 #: src/screens/Settings/AboutSettings.tsx:95 #: src/screens/Settings/AboutSettings.tsx:98 #: src/view/screens/PrivacyPolicy.tsx:25 @@ -8534,15 +8710,15 @@ msgstr "" msgid "Privacy violation of a minor" msgstr "" -#: src/view/com/composer/Composer.tsx:2478 +#: src/view/com/composer/Composer.tsx:2460 msgid "Processing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2480 +#: src/view/com/composer/Composer.tsx:2462 msgid "Processing video..." msgstr "" -#: src/lib/api/index.ts:66 +#: src/lib/api/index.ts:60 msgid "Processing..." msgstr "" @@ -8583,22 +8759,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:1677 +#: src/view/com/composer/Composer.tsx:1659 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1672 +#: src/view/com/composer/Composer.tsx:1654 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:1661 +#: src/view/com/composer/Composer.tsx:1643 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:1666 +#: src/view/com/composer/Composer.tsx:1648 msgid "Publish reply" msgstr "" @@ -8677,11 +8853,11 @@ msgstr "" msgid "Re-attach quote" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:384 +#: src/screens/Messages/components/InviteLinkDialog.tsx:401 msgid "Re-enable invite link" msgstr "Re-enable invite link" -#: src/screens/Messages/components/InviteLinkDialog.tsx:391 +#: src/screens/Messages/components/InviteLinkDialog.tsx:408 msgid "Re-enable link" msgstr "Re-enable link" @@ -9047,7 +9223,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1689 +#: src/view/com/composer/Composer.tsx:1671 msgctxt "action" msgid "Reply" msgstr "" @@ -9087,7 +9263,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:193 #: src/features/liveNow/components/LiveStatusDialog.tsx:266 -#: src/screens/Messages/ConversationSettings/index.tsx:518 +#: src/screens/Messages/ConversationSettings/index.tsx:517 msgid "Report" msgstr "" @@ -9152,7 +9328,7 @@ msgstr "" msgid "Report this feed" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:517 +#: src/screens/Messages/ConversationSettings/index.tsx:516 msgid "Report this group chat" msgstr "Report this group chat" @@ -9232,16 +9408,42 @@ msgstr "" msgid "Reposts of your reposts" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:414 +msgid "Request access to group chat" +msgstr "Request access to group chat" + +#: src/components/intents/GroupChatJoinDialog.tsx:419 +msgid "Request access to join this group chat" +msgstr "Request access to join this group chat" + +#: src/screens/Messages/JoinRequests.tsx:178 +msgid "Request approved." +msgstr "Request approved." + #: src/screens/Settings/components/ChangePasswordDialog.tsx:226 #: src/screens/Settings/components/ChangePasswordDialog.tsx:232 msgid "Request code" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:210 +msgid "Request ignored." +msgstr "Request ignored." + +#: src/components/intents/GroupChatJoinDialog.tsx:252 +msgid "Request to join" +msgstr "Request to join" + #. Incoming message requests #: src/screens/Messages/components/InboxRequests.tsx:24 msgid "Requests" msgstr "Requests" +#: src/Navigation.tsx:495 +#: src/screens/Messages/JoinRequests.tsx:58 +#: src/screens/Messages/JoinRequests.tsx:421 +msgid "Requests to join" +msgstr "Requests to join" + #: src/screens/Settings/AccessibilitySettings.tsx:59 #: src/screens/Settings/AccessibilitySettings.tsx:64 msgid "Require alt text before posting" @@ -9259,6 +9461,10 @@ msgstr "" msgid "Required in your region" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:272 +msgid "Rescind request" +msgstr "Rescind request" + #: src/components/dialogs/EmailDialog/components/ResendEmailText.tsx:39 msgid "Resend" msgstr "" @@ -9333,6 +9539,7 @@ msgstr "" #: src/screens/Messages/ChatList.tsx:354 #: src/screens/Messages/components/MessageListError.tsx:24 #: src/screens/Messages/Inbox.tsx:220 +#: src/screens/Messages/JoinRequests.tsx:356 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:268 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:271 #: src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx:92 @@ -9410,27 +9617,29 @@ msgstr "" #: src/screens/SavedFeeds.tsx:124 #: src/screens/SavedFeeds.tsx:311 #: src/screens/SavedFeeds.tsx:315 -#: src/view/com/composer/Composer.tsx:1332 +#: src/view/com/composer/Composer.tsx:1314 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save changes" msgstr "" -#: src/view/com/composer/Composer.tsx:1304 +#: src/view/com/composer/Composer.tsx:1286 #: src/view/com/composer/drafts/DraftsButton.tsx:93 msgid "Save changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1332 +#: src/view/com/composer/Composer.tsx:1314 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save draft" msgstr "" -#: src/view/com/composer/Composer.tsx:1306 +#: src/view/com/composer/Composer.tsx:1288 #: src/view/com/composer/drafts/DraftsButton.tsx:95 msgid "Save draft?" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:99 +#: src/components/MediaPreview.tsx:196 +#: src/components/Post/Embed/ImageContextMenu.tsx:70 #: src/components/StarterPack/ShareDialog.tsx:144 #: src/components/StarterPack/ShareDialog.tsx:150 msgid "Save image" @@ -9466,7 +9675,7 @@ msgid "Saved Feeds" msgstr "" #: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:144 -#: src/Navigation.tsx:548 +#: src/Navigation.tsx:555 #: src/screens/Bookmarks/index.tsx:59 msgid "Saved Posts" msgstr "" @@ -9518,7 +9727,7 @@ msgstr "" #. placeholder {0}: profile.handle #. placeholder {0}: route.params.name -#: src/Navigation.tsx:254 +#: src/Navigation.tsx:256 #: src/screens/Profile/ProfileSearch.tsx:37 msgid "Search @{0}'s posts" msgstr "" @@ -9759,7 +9968,7 @@ msgstr "" msgid "Select GIF \"{0}\"" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:668 +#: src/components/dms/InitiateChatFlow.tsx:669 msgid "Select group chat members" msgstr "Select group chat members" @@ -9839,7 +10048,7 @@ msgstr "" msgid "Select your preferred notification channels" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:414 +#: src/view/com/composer/SelectMediaButton.tsx:418 msgid "Selecting multiple media types is not supported." msgstr "" @@ -9946,7 +10155,7 @@ msgstr "" msgid "Sets email for password reset" msgstr "" -#: src/Navigation.tsx:210 +#: src/Navigation.tsx:212 #: src/screens/Settings/Settings.tsx:98 #: src/view/shell/desktop/LeftNav.tsx:754 #: src/view/shell/Drawer.tsx:612 @@ -10010,10 +10219,12 @@ msgstr "" msgid "Sexually Suggestive" msgstr "" +#: src/components/MediaPreview.tsx:202 +#: src/components/Post/Embed/ImageContextMenu.tsx:74 #: src/components/StarterPack/QrCodeDialog.tsx:195 #: src/screens/Hashtag.tsx:130 -#: src/screens/Messages/components/InviteLinkDialog.tsx:370 -#: src/screens/Messages/components/InviteLinkDialog.tsx:377 +#: src/screens/Messages/components/InviteLinkDialog.tsx:387 +#: src/screens/Messages/components/InviteLinkDialog.tsx:394 #: src/screens/StarterPack/StarterPackScreen.tsx:447 #: src/screens/Topic.tsx:90 msgid "Share" @@ -10029,8 +10240,8 @@ msgid "Share author DID" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:94 -#: src/components/Lightbox/Lightbox.web.tsx:273 -#: src/components/Lightbox/Lightbox.web.tsx:299 +#: src/components/Lightbox/Lightbox.web.tsx:277 +#: src/components/Lightbox/Lightbox.web.tsx:303 msgid "Share image" msgstr "Share image" @@ -10082,7 +10293,7 @@ msgstr "" msgid "Share your contacts to find friends" msgstr "" -#: src/Navigation.tsx:313 +#: src/Navigation.tsx:315 msgid "Shared Preferences Tester" msgstr "" @@ -10228,6 +10439,8 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:179 #: src/screens/Login/LoginForm.tsx:350 #: src/screens/Login/LoginForm.tsx:356 +#: src/screens/Messages/JoinRequest.tsx:280 +#: src/screens/Messages/JoinRequest.tsx:286 #: src/screens/Search/SearchResults.tsx:316 #: src/view/com/auth/SplashScreen.tsx:118 #: src/view/com/auth/SplashScreen.tsx:124 @@ -10260,6 +10473,10 @@ msgstr "" msgid "Sign in or create your account to join the conversation!" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:205 +msgid "Sign in to accept invite." +msgstr "Sign in to accept invite." + #: src/components/AccountList.tsx:70 msgid "Sign in to account that is not listed" msgstr "" @@ -10268,6 +10485,10 @@ msgstr "" msgid "Sign in to Bluesky or create a new account" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:204 +msgid "Sign in to request access to this group chat." +msgstr "Sign in to request access to this group chat." + #: src/components/PostControls/PostMenu/PostMenuItems.tsx:571 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:573 msgid "Sign in to view post" @@ -10322,7 +10543,7 @@ msgstr "" msgid "Skip contact sharing and continue to the app" msgstr "" -#: src/view/com/composer/Composer.tsx:1349 +#: src/view/com/composer/Composer.tsx:1331 msgid "Skip empty posts?" msgstr "Skip empty posts?" @@ -10336,7 +10557,7 @@ msgstr "" msgid "Skip to next step" msgstr "" -#: src/components/images/Gallery/index.tsx:417 +#: src/components/images/Gallery/index.tsx:420 msgid "slide" msgstr "slide" @@ -10424,6 +10645,7 @@ msgstr "" #: src/screens/Messages/Conversation.tsx:135 #: src/screens/Messages/ConversationSettings/index.tsx:112 +#: src/screens/Messages/JoinRequests.tsx:87 msgid "Something went wrong" msgstr "" @@ -10513,17 +10735,17 @@ msgstr "" msgid "Start adding people!" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:669 +#: src/components/dms/InitiateChatFlow.tsx:670 msgid "Start chat" msgstr "Start chat" #: src/components/dialogs/SearchablePeopleList.tsx:428 -#: src/components/dms/InitiateChatFlow.tsx:817 +#: src/components/dms/InitiateChatFlow.tsx:818 msgid "Start chat with {displayName}" msgstr "" -#: src/Navigation.tsx:519 -#: src/Navigation.tsx:524 +#: src/Navigation.tsx:526 +#: src/Navigation.tsx:531 #: src/screens/StarterPack/Wizard/index.tsx:197 msgid "Starter Pack" msgstr "" @@ -10585,7 +10807,7 @@ msgstr "" msgid "Stored as part of a secure code for matching with others" msgstr "" -#: src/Navigation.tsx:303 +#: src/Navigation.tsx:305 #: src/screens/Settings/Settings.tsx:456 msgid "Storybook" msgstr "" @@ -10667,12 +10889,16 @@ msgstr "" msgid "Success!" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:100 +msgid "Successfully joined the group chat!" +msgstr "Successfully joined the group chat!" + #: src/components/verification/VerificationCreatePrompt.tsx:37 msgid "Successfully verified" msgstr "" #: src/components/dms/AddMembersFlow.tsx:226 -#: src/components/dms/InitiateChatFlow.tsx:325 +#: src/components/dms/InitiateChatFlow.tsx:326 msgid "Suggested" msgstr "Suggested" @@ -10701,7 +10927,7 @@ msgctxt "Name of app icon variant" msgid "Sunset" msgstr "" -#: src/Navigation.tsx:323 +#: src/Navigation.tsx:325 #: src/view/screens/Support.tsx:25 #: src/view/screens/Support.tsx:28 msgid "Support" @@ -10855,7 +11081,7 @@ msgstr "" #: src/components/dialogs/BirthDateSettings.tsx:181 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:31 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:98 -#: src/Navigation.tsx:333 +#: src/Navigation.tsx:335 #: src/screens/Settings/AboutSettings.tsx:87 #: src/screens/Settings/AboutSettings.tsx:90 #: src/view/screens/TermsOfService.tsx:25 @@ -10980,6 +11206,11 @@ msgstr "" msgid "The laws in your region require you to verify you're an adult to access certain features. Tap to learn more." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:134 +#: src/screens/Messages/JoinRequests.tsx:201 +msgid "The member limit has been reached." +msgstr "The member limit has been reached." + #: src/view/com/composer/select-language/SuggestedLanguage.tsx:400 msgid "The post you’re replying to was marked as being written in {suggestedLanguageName} by its author. Would you like to reply in <0>{suggestedLanguageName}?" msgstr "The post you’re replying to was marked as being written in {suggestedLanguageName} by its author. Would you like to reply in <0>{suggestedLanguageName}?" @@ -10988,10 +11219,10 @@ 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:407 -#: src/view/com/composer/state/video.ts:451 -msgid "The selected video is larger than {videoSize} MB. Please try again with a smaller file." -msgstr "The selected video is larger than {videoSize} MB. Please try again with a smaller file." +#: src/view/com/composer/state/video.ts:397 +#: src/view/com/composer/state/video.ts:436 +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/lib/hooks/useCleanError.ts:41 #: src/lib/strings/errors.ts:19 @@ -11032,7 +11263,7 @@ msgstr "" msgid "There is a limit to how often you can change your birthdate. You may need to wait a day or two before updating it again." msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:476 +#: src/screens/Messages/components/InviteLinkDialog.tsx:487 msgid "There is no invite link for this group chat." msgstr "There is no invite link for this group chat." @@ -11046,6 +11277,8 @@ msgid "There was a problem loading GIFs. Check your connection and try again." msgstr "There was a problem loading GIFs. Check your connection and try again." #: src/components/contacts/screens/GetContacts.tsx:147 +#: src/components/intents/GroupChatJoinDialog.tsx:118 +#: src/components/intents/GroupChatJoinDialog.tsx:152 msgid "There was a problem with your internet connection, please try again" msgstr "" @@ -11202,6 +11435,10 @@ msgstr "" msgid "This chat has ended" msgstr "This chat has ended" +#: src/components/intents/GroupChatJoinDialog.tsx:263 +msgid "This chat is full" +msgstr "This chat is full" + #: src/screens/Messages/components/ChatListItem.tsx:375 #: src/screens/Messages/components/ChatLocked.tsx:61 msgid "This chat is locked" @@ -11241,6 +11478,10 @@ msgstr "" msgid "This content is not viewable without a Bluesky account." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:120 +msgid "This conversation is locked." +msgstr "This conversation is locked." + #: src/screens/Messages/components/ChatListItem.tsx:155 msgid "This conversation is with a deleted or a deactivated account. Press for options" msgstr "" @@ -11299,6 +11540,18 @@ msgstr "This image could not be used. Try a different format like .jpg or .png." msgid "This information is private and not shared with other users." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:130 +msgid "This invite link has been disabled." +msgstr "This invite link has been disabled." + +#: src/screens/Messages/JoinRequest.tsx:83 +msgid "This invite link has expired" +msgstr "This invite link has expired" + +#: src/components/intents/GroupChatJoinDialog.tsx:194 +msgid "This invite link is invalid" +msgstr "This invite link is invalid" + #: src/view/screens/Debug.tsx:327 msgid "This is an empty state" msgstr "" @@ -11385,7 +11638,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:963 +#: src/view/com/composer/Composer.tsx:945 msgid "This post's author has disabled quote posts." msgstr "" @@ -11398,6 +11651,7 @@ msgid "This reply will be sorted into a hidden section at the bottom of your thr msgstr "" #: src/screens/Messages/ConversationSettings/index.tsx:132 +#: src/screens/Messages/JoinRequests.tsx:111 msgid "This screen is only available for group conversations." msgstr "This screen is only available for group conversations." @@ -11505,7 +11759,7 @@ msgstr "" msgid "Threaded" msgstr "" -#: src/Navigation.tsx:366 +#: src/Navigation.tsx:368 msgid "Threads Preferences" msgstr "" @@ -11577,7 +11831,7 @@ msgstr "" msgid "Top replies first" msgstr "" -#: src/Navigation.tsx:477 +#: src/Navigation.tsx:479 msgid "Topic" msgstr "" @@ -11694,6 +11948,10 @@ msgstr "" msgid "Unable to delete" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:346 +msgid "Unable to fetch join requests." +msgstr "Unable to fetch join requests." + #: src/components/dms/dialogs/NewChatDialog.tsx:109 msgid "Unable to find a selected recipient." msgstr "Unable to find a selected recipient." @@ -11849,7 +12107,7 @@ msgstr "" msgid "Unlock chat" msgstr "Unlock chat" -#: src/screens/Messages/ConversationSettings/index.tsx:502 +#: src/screens/Messages/ConversationSettings/index.tsx:501 msgid "Unlock this group chat" msgstr "Unlock this group chat" @@ -11886,7 +12144,7 @@ msgstr "" msgid "Unmute list" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:464 +#: src/screens/Messages/ConversationSettings/index.tsx:463 msgid "Unmute this group chat" msgstr "Unmute this group chat" @@ -11965,7 +12223,7 @@ msgstr "" msgid "Unsupported clipboard content" msgstr "Unsupported clipboard content" -#: src/view/com/composer/Composer.tsx:1442 +#: src/view/com/composer/Composer.tsx:1424 msgid "Unsupported video type: {mimeType}" msgstr "" @@ -11985,9 +12243,9 @@ msgstr "" msgid "Update email" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:205 -#: src/screens/Messages/components/InviteLinkDialog.tsx:243 -#: src/screens/Messages/components/InviteLinkDialog.tsx:267 +#: src/screens/Messages/components/InviteLinkDialog.tsx:217 +#: src/screens/Messages/components/InviteLinkDialog.tsx:255 +#: src/screens/Messages/components/InviteLinkDialog.tsx:283 msgid "Update invite link" msgstr "Update invite link" @@ -12048,20 +12306,20 @@ msgstr "" msgid "Upload from Library" msgstr "" -#: src/view/com/composer/Composer.tsx:2471 +#: src/view/com/composer/Composer.tsx:2453 msgid "Uploading GIF..." msgstr "" -#: src/lib/api/index.ts:338 +#: src/lib/api/index.ts:322 msgid "Uploading images..." msgstr "" -#: src/lib/api/index.ts:409 -#: src/lib/api/index.ts:433 +#: src/lib/api/index.ts:390 +#: src/lib/api/index.ts:414 msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:2473 +#: src/view/com/composer/Composer.tsx:2455 msgid "Uploading video..." msgstr "" @@ -12208,7 +12466,7 @@ msgstr "" msgid "Verification settings" msgstr "" -#: src/Navigation.tsx:203 +#: src/Navigation.tsx:205 #: src/screens/Moderation/VerificationSettings.tsx:34 msgid "Verification Settings" msgstr "" @@ -12311,11 +12569,11 @@ msgstr "" msgid "Video" msgstr "" -#: src/view/com/composer/state/video.ts:364 +#: src/view/com/composer/state/video.ts:359 msgid "Video failed to process" msgstr "" -#: src/Navigation.tsx:540 +#: src/Navigation.tsx:547 msgid "Video Feed" msgstr "" @@ -12350,7 +12608,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:2491 +#: src/view/com/composer/Composer.tsx:2473 msgid "Video uploaded" msgstr "" @@ -12363,11 +12621,11 @@ msgstr "" msgid "Videos" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:428 +#: src/view/com/composer/SelectMediaButton.tsx:432 msgid "Videos must be less than 3 minutes long." msgstr "" -#: src/view/com/composer/Composer.tsx:1055 +#: src/view/com/composer/Composer.tsx:1037 msgctxt "Action to view the post the user just created" msgid "View" msgstr "" @@ -12434,7 +12692,7 @@ msgstr "" msgid "View full thread" msgstr "" -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:43 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:48 msgid "View incoming group chat requests" msgstr "View incoming group chat requests" @@ -12453,7 +12711,7 @@ msgstr "" msgid "View more trending videos" msgstr "" -#: src/view/com/composer/Composer.tsx:1050 +#: src/view/com/composer/Composer.tsx:1032 msgid "View post" msgstr "" @@ -12481,7 +12739,7 @@ msgstr "View publication" msgid "View the avatar" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:489 +#: src/screens/Messages/ConversationSettings/index.tsx:488 msgid "View the invite link for this group chat" msgstr "View the invite link for this group chat" @@ -12527,8 +12785,8 @@ msgstr "" msgid "View your verifications" msgstr "" -#: src/components/images/AutoSizedImage.tsx:207 -#: src/components/images/AutoSizedImage.tsx:239 +#: src/components/images/AutoSizedImage.tsx:220 +#: src/components/images/AutoSizedImage.tsx:252 msgid "Views full image" msgstr "" @@ -12600,6 +12858,10 @@ msgstr "" msgid "We couldn't load this conversation" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:88 +msgid "We couldn’t load this conversation’s join requests" +msgstr "We couldn’t load this conversation’s join requests" + #: src/screens/Messages/ConversationSettings/index.tsx:113 msgid "We couldn’t load this conversation’s settings" msgstr "We couldn’t load this conversation’s settings" @@ -12650,7 +12912,7 @@ msgstr "" msgid "We sent an email to <0>{0} containing a link. Please click on it to complete the email verification process." msgstr "" -#: src/view/com/composer/state/video.ts:434 +#: src/view/com/composer/state/video.ts:419 msgid "We were unable to determine if you are allowed to upload videos. Please try again." msgstr "" @@ -12745,7 +13007,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:961 +#: src/view/com/composer/Composer.tsx:943 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -12796,7 +13058,7 @@ msgid "What do you want to call your starter pack?" msgstr "" #: src/view/com/auth/SplashScreen.web.tsx:98 -#: src/view/com/composer/Composer.tsx:1402 +#: src/view/com/composer/Composer.tsx:1384 #: src/view/com/feeds/ComposerPrompt.tsx:193 msgid "What's up?" msgstr "" @@ -12809,7 +13071,7 @@ msgstr "" msgid "Who can interact with this post?" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:216 +#: src/screens/Messages/components/InviteLinkDialog.tsx:227 msgid "Who can join this group chat and how" msgstr "Who can join this group chat and how" @@ -12882,7 +13144,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:1320 +#: src/view/com/composer/Composer.tsx:1302 msgid "Would you like to save this as a draft to edit later?" msgstr "" @@ -12891,12 +13153,12 @@ msgstr "" msgid "Write a post" msgstr "" -#: src/view/com/composer/Composer.tsx:1502 +#: src/view/com/composer/Composer.tsx:1484 msgid "Write post" msgstr "" #: src/screens/PostThread/components/ThreadComposePrompt.tsx:91 -#: src/view/com/composer/Composer.tsx:1400 +#: src/view/com/composer/Composer.tsx:1382 msgid "Write your reply" msgstr "" @@ -12910,6 +13172,7 @@ msgid "Wrong DID returned from server. Received: {0}" msgstr "" #: src/screens/Messages/ConversationSettings/index.tsx:131 +#: src/screens/Messages/JoinRequests.tsx:110 msgid "Wrong kind of conversation" msgstr "Wrong kind of conversation" @@ -12999,7 +13262,7 @@ msgstr "" msgid "You are no longer live" msgstr "" -#: src/view/com/composer/state/video.ts:427 +#: src/view/com/composer/state/video.ts:412 msgid "You are not allowed to upload videos." msgstr "" @@ -13062,7 +13325,7 @@ msgstr "" msgid "You can now sign in with your new password." msgstr "" -#: src/view/com/composer/Composer.tsx:1325 +#: src/view/com/composer/Composer.tsx:1307 msgid "You can only save drafts up to 1000 characters." msgstr "" @@ -13070,11 +13333,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:431 +#: src/view/com/composer/SelectMediaButton.tsx:435 msgid "You can only select one GIF at a time." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:425 +#: src/view/com/composer/SelectMediaButton.tsx:429 msgid "You can only select one video at a time." msgstr "" @@ -13087,7 +13350,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, currently 4 but may change. -#: src/view/com/composer/SelectMediaButton.tsx:417 +#: src/view/com/composer/SelectMediaButton.tsx:421 msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." msgstr "" @@ -13128,6 +13391,10 @@ msgstr "" msgid "You got here first" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:136 +msgid "You have been removed from this group." +msgstr "You have been removed from this group." + #: src/components/moderation/ModerationDetailsDialog.tsx:77 #: src/lib/moderation/useModerationCauseDescription.ts:58 #: src/lib/moderation/useModerationCauseDescription.ts:66 @@ -13197,7 +13464,7 @@ msgstr "" msgid "You have temporarily reached the limit for video uploads. Please try again later." msgstr "" -#: src/view/com/composer/Composer.tsx:1315 +#: src/view/com/composer/Composer.tsx:1297 msgid "You have unsaved changes to this draft, would you like to save them?" msgstr "" @@ -13267,7 +13534,7 @@ msgstr "" msgid "You must grant access to your photo library to save a QR code" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:460 +#: src/view/com/composer/SelectMediaButton.tsx:464 msgid "You need to allow access to your media library." msgstr "" @@ -13394,7 +13661,7 @@ msgstr "" msgid "You've reached the end of your feed! Find some more accounts to follow." msgstr "" -#: src/view/com/composer/Composer.tsx:582 +#: src/view/com/composer/Composer.tsx:576 msgid "You've reached the maximum number of drafts" msgstr "" @@ -13406,11 +13673,11 @@ msgstr "" msgid "You've reached the start of the active content." msgstr "" -#: src/view/com/composer/state/video.ts:438 +#: src/view/com/composer/state/video.ts:423 msgid "You've reached your daily limit for video uploads (too many bytes)" msgstr "" -#: src/view/com/composer/state/video.ts:442 +#: src/view/com/composer/state/video.ts:427 msgid "You've reached your daily limit for video uploads (too many videos)" msgstr "" @@ -13430,15 +13697,15 @@ msgstr "" msgid "Your account has been suspended" msgstr "" -#: src/view/com/composer/state/video.ts:446 +#: src/view/com/composer/state/video.ts:431 msgid "Your account is not yet old enough to upload videos. Please try again later." msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:674 +#: src/components/dms/InitiateChatFlow.tsx:675 msgid "Your account is too new" msgstr "Your account is too new" -#: src/components/dms/InitiateChatFlow.tsx:675 +#: src/components/dms/InitiateChatFlow.tsx:676 msgid "Your account must be at least 7 days old to create a new group chat." msgstr "Your account must be at least 7 days old to create a new group chat." @@ -13521,7 +13788,7 @@ msgstr "" msgid "Your full handle will be <0>@{0}" msgstr "" -#: src/Navigation.tsx:449 +#: src/Navigation.tsx:451 #: src/screens/Search/modules/ExploreInterestsCard.tsx:68 #: src/screens/Settings/ContentAndMediaSettings.tsx:94 #: src/screens/Settings/ContentAndMediaSettings.tsx:97 @@ -13550,9 +13817,9 @@ msgstr "" msgid "Your muted words" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:179 -msgid "Your name, avatar, and the name of the group chat will be visible to everyone." -msgstr "Your name, avatar, and the name of the group chat will be visible to everyone." +#: src/screens/Messages/components/InviteLinkDialog.tsx:189 +msgid "Your name, avatar, the name of the group chat, and the number of members will be visible to everyone." +msgstr "Your name, avatar, the name of the group chat, and the number of members will be visible to everyone." #: src/screens/Settings/components/ChangePasswordDialog.tsx:72 msgid "Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on." @@ -13562,11 +13829,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:1046 +#: src/view/com/composer/Composer.tsx:1028 msgid "Your post was sent" msgstr "" -#: src/view/com/composer/Composer.tsx:1043 +#: src/view/com/composer/Composer.tsx:1025 msgid "Your posts were sent" msgstr "" @@ -13587,7 +13854,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:1045 +#: src/view/com/composer/Composer.tsx:1027 msgid "Your reply was sent" msgstr "" @@ -13600,7 +13867,7 @@ msgstr "" msgid "Your selected interests help us serve you content you care about." msgstr "" -#: src/view/com/composer/Composer.tsx:1350 +#: src/view/com/composer/Composer.tsx:1332 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." -- 2.51.2 From 00497964c1ab048cc676ce3924dc407ed8ef15cf Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 3 Jun 2026 09:05:46 +0300 Subject: [PATCH 04/61] [ALF] Set text to `leading_snug` by default (#10627) --- src/components/Button.tsx | 6 +++--- src/components/RichText.tsx | 2 +- src/components/Typography.tsx | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index aa705b189..5f7459502 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -757,11 +757,11 @@ export function useSharedButtonTextStyles() { } if (size === 'large') { - baseStyles.push(a.text_md, a.leading_snug, a.font_medium) + baseStyles.push(a.text_md, a.font_medium) } else if (size === 'small') { - baseStyles.push(a.text_sm, a.leading_snug, a.font_medium) + baseStyles.push(a.text_sm, a.font_medium) } else if (size === 'tiny') { - baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold) + baseStyles.push(a.text_xs, a.font_semi_bold) } return flatten(baseStyles) diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 322fb9ebd..25af85172 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -66,7 +66,7 @@ export function RichText({ } }, [value]) - const plainStyles = [a.leading_snug, style] + const plainStyles = style const interactiveStyles = [plainStyles, interactiveStyle] const {text, facets} = richText diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index 04b5e5499..a65889f67 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -31,6 +31,7 @@ export function Text({ [ a.text_sm, t.atoms.text, + a.leading_snug, web(numberOfLines === 1 && numberOfLinesClippingFix), style, ], -- 2.51.2 From a046d8d96a94adf499682d9cb293f8dbf35d6465 Mon Sep 17 00:00:00 2001 From: RetroSunstar <57507616+RetroSunstar@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:47:35 +0100 Subject: [PATCH 05/61] Use square icons for labellers in Automation Label preview card (#10701) --- src/screens/Settings/AutomationLabelSettings.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/screens/Settings/AutomationLabelSettings.tsx b/src/screens/Settings/AutomationLabelSettings.tsx index f4c6823d6..423bc0073 100644 --- a/src/screens/Settings/AutomationLabelSettings.tsx +++ b/src/screens/Settings/AutomationLabelSettings.tsx @@ -123,7 +123,11 @@ export function AutomationLabelSettingsScreen({}: Props) { paddingRight: 20, // helps visually center }, ]}> - + -- 2.51.2 From 21679b66ddeeb0df62173091994d9b0708435cda Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 3 Jun 2026 17:39:31 +0300 Subject: [PATCH 06/61] Mention default `` style in CLAUDE.md (#10705) --- CLAUDE.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c747973c7..37e6a1c2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -206,7 +206,7 @@ function MyComponent() { return ( - Hello + Hello ) } @@ -414,12 +414,16 @@ import {Text, H1, H2, P} from '#/components/Typography'

Heading

Paragraph text with default styling.

-Custom text +Custom text -// For text with emoji, add the emoji prop +// For text with emoji, add the emoji prop. User-generated text (e.g. display names) +// will almost certainly contain emoji, so only omit it when the text is static and +// does not contain an emoji Hello! 👋 ``` +The `Text` component's default style is `[a.text_sm, a.leading_snug, t.atoms.text]`. + ### TextField ```tsx -- 2.51.2 From b6650d8d5c0a9dc2adbe53ce727e8a3901f0edd7 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Wed, 3 Jun 2026 10:58:43 -0400 Subject: [PATCH 07/61] Fix alt text selection in lightbox on Android (and iOS) (#10698) Co-authored-by: Eric Bailey --- src/components/Lightbox/chrome/Footer.tsx | 47 ++++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/components/Lightbox/chrome/Footer.tsx b/src/components/Lightbox/chrome/Footer.tsx index fb190f42f..a20c80df3 100644 --- a/src/components/Lightbox/chrome/Footer.tsx +++ b/src/components/Lightbox/chrome/Footer.tsx @@ -1,11 +1,5 @@ import {useRef} from 'react' -import { - LayoutAnimation, - Pressable, - ScrollView, - StyleSheet, - View, -} from 'react-native' +import {LayoutAnimation, ScrollView, StyleSheet, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {BlurView} from 'expo-blur' import {useLingui} from '@lingui/react/macro' @@ -60,26 +54,41 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) { isMomentumScrolling.current = false }} contentContainerStyle={[a.px_md, a.py_sm]}> - { - if (isMomentumScrolling.current) return - LayoutAnimation.configureNext({ - duration: 450, - update: {type: 'spring', springDamping: 1}, - }) - onToggleAltExpanded() - }}> + accessibilityHint=""> + {/* + * The press handlers must live on the Text itself, not on a + * wrapping Pressable. Text selection is driven by the platform's + * native text view long-press (RN Text on Android, UITextView on + * iOS). A parent touchable consumes that long-press before the + * selectable Text can begin a selection - on Android this prevents + * selection entirely. Keeping onPress/onLongPress on the Text lets + * the same native node own both the tap-to-expand and the + * long-press-to-select. The empty onLongPress is intentional: it + * reserves the long-press for the OS selection gesture instead of + * firing the expand toggle. RN exposes no API to arbitrate tap vs + * native selection on a single node, so this is the supported + * workaround, and it behaves consistently on both platforms. + */} + numberOfLines={isAltExpanded ? undefined : 3} + onPress={() => { + if (isMomentumScrolling.current) return + LayoutAnimation.configureNext({ + duration: 450, + update: {type: 'spring', springDamping: 1}, + }) + onToggleAltExpanded() + }} + onLongPress={() => {}}> {altText} - +
-- 2.51.2 From 1b4c72f5166aec88df7ec8513a02c6e934d86327 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:28:11 -0700 Subject: [PATCH 08/61] Mount message dialogs once at the list level (#10435) Co-authored-by: Samuel Newman --- src/components/Prompt.tsx | 4 +- src/components/dms/AfterReportDialog.tsx | 7 +- src/components/dms/MessageContextMenu.tsx | 176 +++++-------- src/components/dms/MessageItem.tsx | 47 +--- src/components/dms/MessageOverlays.tsx | 175 +++++++++++++ src/components/dms/ReactionsDialog.tsx | 38 ++- .../moderation/ReportDialog/index.tsx | 4 +- .../moderation/ReportDialog/types.ts | 4 + .../Messages/components/MessagesList.tsx | 245 +++++++++--------- 9 files changed, 421 insertions(+), 279 deletions(-) create mode 100644 src/components/dms/MessageOverlays.tsx diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 916a7ec2d..5fdf18e2a 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -224,6 +224,7 @@ export function Basic({ cancelButtonCta, confirmButtonCta, onConfirm, + onClose, confirmButtonColor, showCancel = true, }: React.PropsWithChildren<{ @@ -240,11 +241,12 @@ export function Basic({ * should NOT close the dialog as a side effect of this method. */ onConfirm: (e: GestureResponderEvent) => void + onClose?: () => void confirmButtonColor?: ButtonColor showCancel?: boolean }>) { return ( - + {title} {description && {description}} diff --git a/src/components/dms/AfterReportDialog.tsx b/src/components/dms/AfterReportDialog.tsx index 3df194fc8..060ea1fcf 100644 --- a/src/components/dms/AfterReportDialog.tsx +++ b/src/components/dms/AfterReportDialog.tsx @@ -33,14 +33,19 @@ export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({ control, params, currentScreen, + onClose, }: { control: Dialog.DialogControlProps params: ReportDialogParams currentScreen: 'list' | 'conversation' + onClose?: () => void }): React.ReactNode { const {t: l} = useLingui() return ( - + { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - convo - .deleteMessage(message.id) - .then(() => Toast.show(l({message: 'Message deleted', context: 'toast'}))) - .catch(() => Toast.show(l`Failed to delete message`)) - }, [l, convo, message.id]) - const onEmojiSelect = useCallback( (emoji: string) => { if ( @@ -128,104 +112,72 @@ export let MessageContextMenu = ({ const sender = senderProfile return ( - <> - - {IS_NATIVE && reactionsAvailable && ( - - - - )} - - - {children} - - - + {IS_NATIVE && reactionsAvailable && ( + - {message.text.length > 0 && ( - <> - - - {l`Translate`} - - - - - {l`Copy message text`} - - - - )} + + + )} + + + {children} + + + + {message.text.length > 0 && ( + <> + + + {l`Translate`} + + + + + {l`Copy message text`} + + + + )} + openDeleteMessage(message)}> + + {l`Delete for me`} + + {!isFromSelf && ( deleteControl.open()}> - - {l`Delete for me`} + testID="messageDropdownReportBtn" + label={l`Report message`} + onPress={() => openReportMessage(message, senderProfile)}> + + {l`Report`} - {!isFromSelf && ( - reportControl.open()}> - - {l`Report`} - - )} - - - { - if (sender) { - unstableCacheProfileView(queryClient, sender) - } - blockOrDeleteControl.open() - }} - /> - - - + )} + + ) } MessageContextMenu = memo(MessageContextMenu) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 6f1819ada..ac6acb589 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -40,8 +40,8 @@ import {useSession} from '#/state/session' import {atoms as a, native, platform, useTheme} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' import {Button} from '#/components/Button' -import {useDialogControl} from '#/components/Dialog' import {ActionsWrapper} from '#/components/dms/ActionsWrapper' +import {useMessageDialogs} from '#/components/dms/MessageOverlays' import {InlineLinkText, Link} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' @@ -49,7 +49,7 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' -import {ReactionsDialog} from './ReactionsDialog' +import {groupReactions} from './ReactionsDialog' import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util' const AVATAR_SIZE = 28 @@ -118,7 +118,7 @@ let MessageItem = ({ const {message} = item const profile = useMaybeProfileShadow(relatedProfiles.get(message.sender.did)) - const reactionsControl = useDialogControl() + const {openReactions} = useMessageDialogs() const isPending = item.type === 'pending-message' @@ -243,34 +243,10 @@ let MessageItem = ({ ) - const groupedReactions = useMemo(() => { - const reactions = message.reactions ?? [] - const grouped = new Map< - string, - { - key: string - value: string - senders: ChatBskyConvoDefs.ReactionViewSender[] - count: number - } - >() - for (const reaction of reactions) { - if (!reaction) continue - const existing = grouped.get(reaction.value) - if (existing) { - existing.senders.push(reaction.sender) - existing.count++ - } else { - grouped.set(reaction.value, { - key: reaction.value, - value: reaction.value, - senders: [reaction.sender], - count: 1, - }) - } - } - return Array.from(grouped.values()) - }, [message.reactions]) + const groupedReactions = useMemo( + () => groupReactions(message.reactions), + [message.reactions], + ) const reactions = useMemo(() => message.reactions ?? [], [message.reactions]) @@ -336,7 +312,7 @@ let MessageItem = ({ transform: [{translateY: -8}], }, ]} - onPress={isGroupChat ? reactionsControl.open : undefined}> + onPress={isGroupChat ? () => openReactions(message) : undefined}> {groupedReactions.map(group => (
) : null} - ) diff --git a/src/components/dms/MessageOverlays.tsx b/src/components/dms/MessageOverlays.tsx new file mode 100644 index 000000000..41228f289 --- /dev/null +++ b/src/components/dms/MessageOverlays.tsx @@ -0,0 +1,175 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' +import {LayoutAnimation} from 'react-native' +import {type ChatBskyConvoDefs} from '@atproto/api' +import {useLingui} from '@lingui/react/macro' +import {useQueryClient} from '@tanstack/react-query' + +import {useConvoActive} from '#/state/messages/convo' +import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' +import {useDialogControl} from '#/components/Dialog' +import {AfterReportDialog} from '#/components/dms/AfterReportDialog' +import {ReactionsDialog} from '#/components/dms/ReactionsDialog' +import {ReportDialog} from '#/components/moderation/ReportDialog' +import * as Prompt from '#/components/Prompt' +import {usePromptControl} from '#/components/Prompt' +import * as Toast from '#/components/Toast' +import type * as bsky from '#/types/bsky' + +type MessageDialogsContextType = { + openDeleteMessage: (message: ChatBskyConvoDefs.MessageView) => void + openReportMessage: ( + message: ChatBskyConvoDefs.MessageView, + senderProfile: bsky.profile.AnyProfileView | undefined, + ) => void + openReactions: (message: ChatBskyConvoDefs.MessageView) => void +} + +const Context = createContext(null) + +export function useMessageDialogs() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useMessageDialogs must be used within a MessageOverlays') + } + return ctx +} + +export function MessageOverlays({children}: {children: React.ReactNode}) { + const {t: l} = useLingui() + const queryClient = useQueryClient() + const convo = useConvoActive() + + const deleteControl = usePromptControl() + const reportControl = usePromptControl() + const afterReportControl = usePromptControl() + const reactionsControl = useDialogControl() + + const [deleteTarget, setDeleteTarget] = + useState(null) + const [reportTarget, setReportTarget] = useState<{ + message: ChatBskyConvoDefs.MessageView + senderProfile: bsky.profile.AnyProfileView | undefined + } | null>(null) + const [afterReportTarget, setAfterReportTarget] = + useState(null) + const [reactionsTarget, setReactionsTarget] = + useState(null) + + const openDeleteMessage = useCallback( + (message: ChatBskyConvoDefs.MessageView) => { + setDeleteTarget(message) + deleteControl.open() + }, + [deleteControl], + ) + + const openReportMessage = useCallback( + ( + message: ChatBskyConvoDefs.MessageView, + senderProfile: bsky.profile.AnyProfileView | undefined, + ) => { + setReportTarget({message, senderProfile}) + reportControl.open() + }, + [reportControl], + ) + + const openReactions = useCallback( + (message: ChatBskyConvoDefs.MessageView) => { + setReactionsTarget(message) + }, + [], + ) + + // These dialogs are conditionally mounted, so we can't open them in the same + // tick that we set their targets - the control refs aren't attached yet. Open + // in an effect after the dialog has mounted. + useEffect(() => { + if (reactionsTarget) { + reactionsControl.open() + } + }, [reactionsTarget, reactionsControl]) + + useEffect(() => { + if (afterReportTarget) { + afterReportControl.open() + } + }, [afterReportTarget, afterReportControl]) + + const onConfirmDelete = useCallback(() => { + if (!deleteTarget) return + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + convo + .deleteMessage(deleteTarget.id) + .then(() => Toast.show(l({message: 'Message deleted', context: 'toast'}))) + .catch(() => Toast.show(l`Failed to delete message`)) + }, [l, convo, deleteTarget]) + + const onAfterReportSubmit = useCallback(() => { + if (!reportTarget) return + if (reportTarget.senderProfile) { + unstableCacheProfileView(queryClient, reportTarget.senderProfile) + } + setAfterReportTarget(reportTarget.message) + }, [queryClient, reportTarget]) + + const ctx = useMemo( + () => ({openDeleteMessage, openReportMessage, openReactions}), + [openDeleteMessage, openReportMessage, openReactions], + ) + + const reportSubject = reportTarget + ? ({ + view: 'message', + convoId: convo.convo.view.id, + message: reportTarget.message, + } as const) + : undefined + + return ( + + {children} + setReportTarget(null)} + /> + {afterReportTarget && ( + setAfterReportTarget(null)} + /> + )} + {reactionsTarget && ( + setReactionsTarget(null)} + /> + )} + setDeleteTarget(null)} + /> + + ) +} diff --git a/src/components/dms/ReactionsDialog.tsx b/src/components/dms/ReactionsDialog.tsx index 576ef4cea..8119cc1b7 100644 --- a/src/components/dms/ReactionsDialog.tsx +++ b/src/components/dms/ReactionsDialog.tsx @@ -1,4 +1,4 @@ -import {useRef, useState} from 'react' +import {useMemo, useRef, useState} from 'react' import { LayoutAnimation, Pressable, @@ -37,14 +37,12 @@ export function ReactionsDialog({ control, relatedProfiles, message, - reactions, - groupedReactions, + onClose, }: { control: Dialog.DialogControlProps relatedProfiles: Map message: ChatBskyConvoDefs.MessageView - reactions?: ChatBskyConvoDefs.ReactionView[] - groupedReactions?: Reaction[] + onClose?: () => void }) { const {t: l} = useLingui() @@ -54,6 +52,9 @@ export function ReactionsDialog({ const [selected, setSelected] = useState('all') + const reactions = message.reactions + const groupedReactions = useMemo(() => groupReactions(reactions), [reactions]) + const filteredReactions = reactions?.filter( r => selected === 'all' || r.value === selected, ) @@ -78,7 +79,10 @@ export function ReactionsDialog({ return ( setSelected('all')} + onClose={() => { + setSelected('all') + onClose?.() + }} nativeOptions={{ preventExpansion: true, minHeight: screenHeight / 2, @@ -388,3 +392,25 @@ function ReactionTab({ ) } + +export function groupReactions( + reactions: ChatBskyConvoDefs.ReactionView[] | undefined, +): Reaction[] { + const grouped = new Map() + for (const reaction of reactions ?? []) { + if (!reaction) continue + const existing = grouped.get(reaction.value) + if (existing) { + existing.senders.push(reaction.sender) + existing.count++ + } else { + grouped.set(reaction.value, { + key: reaction.value, + value: reaction.value, + senders: [reaction.sender], + count: 1, + }) + } + } + return Array.from(grouped.values()) +} diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index c24c6feae..d1d3aa558 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -75,9 +75,11 @@ export function ReportDialog( () => (props.subject ? parseReportSubject(props.subject) : undefined), [props.subject], ) + const propsOnClose = props.onClose const onClose = useCallback(() => { ax.metric('reportDialog:close', {}) - }, [ax]) + propsOnClose?.() + }, [ax, propsOnClose]) return ( diff --git a/src/components/moderation/ReportDialog/types.ts b/src/components/moderation/ReportDialog/types.ts index 89089d138..57ed3cd27 100644 --- a/src/components/moderation/ReportDialog/types.ts +++ b/src/components/moderation/ReportDialog/types.ts @@ -88,4 +88,8 @@ export type ReportDialogProps = { * Called if the report was successfully submitted. */ onAfterSubmit?: () => void + /** + * Called after the dialog finishes closing. + */ + onClose?: () => void } diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 99b725267..fbebdc074 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -56,6 +56,7 @@ import {MessageListError} from '#/screens/Messages/components/MessageListError' import {atoms as a, platform, tokens, useTheme, web} from '#/alf' import {DateDivider} from '#/components/dms/DateDivider' import {MessageItem} from '#/components/dms/MessageItem' +import {MessageOverlays} from '#/components/dms/MessageOverlays' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {SystemMessageGroup} from '#/components/dms/SystemMessageGroup' import {SystemMessageItem} from '#/components/dms/SystemMessageItem' @@ -498,127 +499,133 @@ export function MessagesList({ return ( - - {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} - - - - - {convoState.hasAllHistory ? ( - convoState.convo?.kind === 'group' ? ( - - ) : ( - - ) - ) : null} - - } - // native only (prop is not supported on web) - renderScrollComponent={renderScrollComponent} - contentContainerStyle={{ - paddingBottom: platform({ - // ios is slightly larger as the input has no top padding - ios: tokens.space.lg, - android: tokens.space.md, - web: 0, // web uses ListFooterComponent instead for scroll reasons - }), - }} - ListFooterComponent={ - - } - style={[ - web({ - scrollbarWidth: 'thin', - scrollbarColor: `${t.palette.contrast_100} transparent`, - scrollbarGutter: 'stable', - }), - ]} - pointerEvents={!hasScrolled ? 'none' : 'auto'} - contentInset={{top: transparentHeaderHeight}} - scrollIndicatorInsets={{top: transparentHeaderHeight}} - /> - - - - {footer ?? ( - - {({loading}) => - ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? ( - - void onSendMessage(message) - } - hasEmbed={!!embedUri} - setEmbed={setEmbed} - loading={loading}> - + + {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} + + + + + {convoState.hasAllHistory ? ( + convoState.convo?.kind === 'group' ? ( + + ) : ( + + ) + ) : null} + + } + // native only (prop is not supported on web) + renderScrollComponent={renderScrollComponent} + contentContainerStyle={{ + paddingBottom: platform({ + // ios is slightly larger as the input has no top padding + ios: tokens.space.lg, + android: tokens.space.md, + web: 0, // web uses ListFooterComponent instead for scroll reasons + }), + }} + ListFooterComponent={ + + } + style={[ + web({ + scrollbarWidth: 'thin', + scrollbarColor: `${t.palette.contrast_100} transparent`, + scrollbarGutter: 'stable', + }), + ]} + pointerEvents={!hasScrolled ? 'none' : 'auto'} + contentInset={{top: transparentHeaderHeight}} + scrollIndicatorInsets={{top: transparentHeaderHeight}} + /> + + + + {footer ?? ( + + {({loading}) => + ax.features.enabled( + ax.features.DmsNewMessageComposerEnable, + ) ? ( + + void onSendMessage(message) + } + hasEmbed={!!embedUri} setEmbed={setEmbed} - /> - - ) : ( - - + + + ) : ( + - - ) - } - - )} - - - - {newMessagesPill.show && } + loading={loading}> + + + ) + } + + )} + + + + {newMessagesPill.show && ( + + )} + ) } -- 2.51.2 From cdad723ea100f4cee9cae186da2454a9bc1b5c2f Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:23:13 -0700 Subject: [PATCH 09/61] Hide accept button for locked chats (#10696) --- src/screens/Messages/components/RequestListItem.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/screens/Messages/components/RequestListItem.tsx b/src/screens/Messages/components/RequestListItem.tsx index 64dbebb16..79eab480a 100644 --- a/src/screens/Messages/components/RequestListItem.tsx +++ b/src/screens/Messages/components/RequestListItem.tsx @@ -28,6 +28,9 @@ export function RequestListItem({ const isDeletedAccount = !convo.primaryMember || convo.primaryMember.handle === 'missing.invalid' + const canAcceptRequest = + convo.kind === 'direct' || convo.details.lockStatus === 'unlocked' + return ( @@ -65,7 +68,9 @@ export function RequestListItem({ ]}> {convo.primaryMember && !isDeletedAccount ? ( <> - + {canAcceptRequest ? ( + + ) : null} Date: Wed, 3 Jun 2026 11:27:44 -0700 Subject: [PATCH 10/61] Enable ESLint errors and suppress existing violations (#10490) --- .github/workflows/lint.yml | 26 +- eslint-suppressions.json | 2246 +++++++++++++++-- eslint.config.mjs | 38 +- src/components/Toast/Toast.tsx | 6 +- src/lib/hooks/useTimeAgo.ts | 12 +- src/lib/routes/links.ts | 4 +- .../Settings/AppIconSettings/index.tsx | 4 +- src/screens/VideoFeed/components/Scrubber.tsx | 3 +- src/state/queries/notifications/feed.ts | 2 +- src/state/queries/nuxs/util.ts | 2 +- src/state/queries/suggested-follows.ts | 2 +- src/state/queries/usePostThread/traversal.ts | 2 +- src/view/com/composer/drafts/state/api.ts | 6 +- src/view/com/testing/TestCtrls.e2e.tsx | 2 +- src/view/com/util/PressableWithHover.tsx | 2 +- .../createNativeStackNavigatorWithAuth.tsx | 2 +- 16 files changed, 2085 insertions(+), 274 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 93729874b..aa6c92765 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - job: [lint, prettier] + job: [lint, prettier, typecheck] steps: - name: Check out Git repository uses: actions/checkout@v5 @@ -62,6 +62,8 @@ jobs: command: pnpm install --frozen-lockfile attempt_limit: 3 attempt_delay: 2000 + - name: Check & compile i18n + run: pnpm intl:build - name: Lint checks run: pnpm ${{ matrix.job }} # Aggregates the matrix results into a single stable check name so branch @@ -80,28 +82,6 @@ jobs: run: | echo "linting result: $RESULT" test "$RESULT" = "success" - typechecking: - name: Run typecheck - runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@v5 - - uses: pnpm/action-setup@v6 - - name: Install node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - cache: pnpm - - name: pnpm install - uses: Wandalen/wretry.action@master - with: - command: pnpm install --frozen-lockfile - attempt_limit: 3 - attempt_delay: 2000 - - name: Check & compile i18n - run: pnpm intl:build - - name: Type check - run: pnpm typecheck testing: name: Run tests runs-on: ubuntu-latest diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 5b7eef4c9..f2e677dc7 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1,586 +1,2235 @@ { - "src/alf/util/flatten.ts": { - "@typescript-eslint/no-explicit-any": { + "src/Navigation.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 1 } }, - "src/analytics/PassiveAnalytics.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/Splash.android.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 1 } }, - "src/analytics/metadata.ts": { - "@typescript-eslint/no-explicit-any": { + "src/Splash.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 1 } }, - "src/analytics/metrics/client.test.ts": { - "@typescript-eslint/no-explicit-any": { + "src/ageAssurance/components/RedirectOverlay.tsx": { + "@typescript-eslint/require-await": { "count": 1 } }, - "src/analytics/metrics/client.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 + "src/ageAssurance/util.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/components/Composer/index.tsx": { + "src/alf/util/flatten.ts": { "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 } }, - "src/components/Dialog/index.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/alf/util/systemUI.ts": { + "@typescript-eslint/no-floating-promises": { "count": 2 } }, - "src/components/Dialog/index.web.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 4 + "src/alf/util/useGutters.ts": { + "react-hooks/immutability": { + "count": 5 } }, - "src/components/DraggableList/index.web.tsx": { + "src/analytics/PassiveAnalytics.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 + }, + "react-hooks/purity": { + "count": 1 } }, - "src/components/FeedCard.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/analytics/features/index.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { "count": 1 } }, - "src/components/FocusScope/index.tsx": { + "src/analytics/identifiers/session.test.ts": { + "@typescript-eslint/no-unsafe-call": { + "count": 9 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "src/analytics/metadata.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/components/InternationalPhoneCodeSelect.tsx": { + "src/analytics/metrics/client.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 8 + }, + "@typescript-eslint/require-await": { + "count": 5 } }, - "src/components/Lists.tsx": { + "src/analytics/metrics/client.ts": { "@typescript-eslint/no-explicit-any": { + "count": 5 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { "count": 1 } }, - "src/components/Menu/types.ts": { - "@typescript-eslint/no-explicit-any": { + "src/analytics/utils.ts": { + "react-hooks/immutability": { "count": 1 } }, - "src/components/Portal.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/AppLanguageDropdown.tsx": { + "@typescript-eslint/no-unsafe-enum-comparison": { "count": 1 } }, - "src/components/Post/Embed/ImageEmbed.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/components/Autocomplete/Autocomplete.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/components/Autocomplete/useAutocomplete/index.ts": { + "react-hooks/immutability": { + "count": 1 } }, - "src/components/PostControls/BookmarkButton.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/components/BotAccountAlert.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/components/ProfileHoverCard/index.web.tsx": { + "src/components/Button.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/Composer/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 7 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, - "src/components/Select/index.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 4 + "src/components/DebugFieldDisplay.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/components/Select/index.web.tsx": { + "src/components/Dialog/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 } }, - "src/components/Select/types.ts": { + "src/components/Dialog/index.web.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { "count": 3 - } - }, - "src/components/StarterPack/ProfileStarterPacks.tsx": { - "@typescript-eslint/no-explicit-any": { + }, + "@typescript-eslint/require-await": { "count": 1 } }, - "src/components/StarterPack/Wizard/WizardEditListDialog.tsx": { + "src/components/DraggableList/index.web.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 1 + "count": 2 } }, - "src/components/dialogs/DeviceLocationRequestDialog.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 1 + "src/components/EmojiPicker/preload.web.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, - "src/components/dialogs/LanguageSelectDialog.tsx": { + "src/components/FeedCard.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/components/dialogs/MutedWords.tsx": { + "src/components/FocusScope/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, - "src/components/dialogs/PostInteractionSettingsDialog.tsx": { + "src/components/InternationalPhoneCodeSelect.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 }, - "react-hooks/preserve-manual-memoization": { + "@typescript-eslint/no-unsafe-member-access": { "count": 1 } }, - "src/components/dialogs/lists/CreateOrEditListDialog.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/components/KnownFollowers.tsx": { + "react-hooks/refs": { + "count": 6 } }, - "src/components/forms/DateField/index.web.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/Lightbox/Lightbox.web.tsx": { + "@typescript-eslint/no-misused-promises": { "count": 1 } }, - "src/components/forms/SegmentedControl.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx": { + "react-hooks/immutability": { "count": 1 } }, - "src/components/forms/ToggleButton.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx": { + "react-hooks/refs": { "count": 1 } }, - "src/components/hooks/useFollowMethods.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/LikedByList.tsx": { + "@typescript-eslint/no-misused-promises": { "count": 2 } }, - "src/components/images/AutoSizedImage.tsx": { + "src/components/Lists.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, - "src/components/images/Gallery/index.tsx": { + "src/components/Menu/types.ts": { "@typescript-eslint/no-explicit-any": { - "count": 5 + "count": 1 } }, - "src/components/images/Gallery/useKeyboardHandlers.ts": { + "src/components/Portal.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 6 + "count": 1 } }, - "src/components/images/Gallery/usePointerHandlers.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 6 + "src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 } }, - "src/components/images/ImageLayoutGrid.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/components/Post/Embed/ExternalEmbed/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/components/images/ImageLayoutGridItem.tsx": { + "src/components/Post/Embed/ImageEmbed.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/features/liveNow/index.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/Post/Embed/StandardSiteEmbed/index.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 3 } }, - "src/geolocation/service.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 2 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 } }, - "src/lib/ScrollContext.tsx": { + "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { "count": 3 } }, - "src/lib/api/index.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 - } - }, - "src/lib/async/retry.ts": { + "src/components/PostControls/BookmarkButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 - } - }, - "src/lib/async/until.ts": { - "@typescript-eslint/no-explicit-any": { + }, + "@typescript-eslint/no-misused-promises": { "count": 2 } }, - "src/lib/broadcast/stub.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/PostControls/DiscoverDebug.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 1 } }, - "src/lib/functions.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 6 + "src/components/PostControls/ShareMenu/ShareMenuItems.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, - "src/lib/getUserDisplayName.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 + "src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 } }, - "src/lib/hooks/useAccountSwitcher.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/PostControls/ShareMenu/index.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 1 } }, - "src/lib/hooks/useNonReactiveCallback.ts": { + "src/components/ProfileHoverCard/index.web.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/immutability": { "count": 1 + }, + "react-hooks/refs": { + "count": 2 } }, - "src/lib/hooks/useOTAUpdates.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/ProgressGuide/FollowDialog.tsx": { + "react-hooks/refs": { "count": 2 } }, - "src/lib/hooks/useRequireEmailVerification.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/RichTextTag.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 2 } }, - "src/lib/hooks/useToggleMutationQueue.ts": { + "src/components/Select/index.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { "count": 2 } }, - "src/lib/hooks/useWebScrollRestoration.ts": { + "src/components/Select/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, - "src/lib/international-telephone-codes.ts": { + "src/components/Select/types.ts": { "@typescript-eslint/no-explicit-any": { - "count": 1 + "count": 3 } }, - "src/lib/media/manip.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/SendErrorReportDialog.tsx": { + "@typescript-eslint/require-await": { "count": 1 } }, - "src/lib/media/save-image.ios.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/StarterPack/Main/ProfilesList.tsx": { + "@typescript-eslint/no-misused-promises": { "count": 1 } }, - "src/lib/media/save-image.ts": { + "src/components/StarterPack/ProfileStarterPacks.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, - "src/lib/merge-refs.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 + "src/components/StarterPack/QrCodeDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 5 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 4 } }, - "src/lib/react-query.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/StarterPack/ShareDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { "count": 1 } }, - "src/lib/routes/router.ts": { + "src/components/StarterPack/Wizard/WizardEditListDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/lib/routes/types.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/Tooltip/index.tsx": { + "@typescript-eslint/no-base-to-string": { "count": 1 } }, - "src/lib/strings/errors.ts": { - "@typescript-eslint/no-explicit-any": { + "src/components/Tooltip/index.web.tsx": { + "@typescript-eslint/no-base-to-string": { "count": 1 } }, - "src/screens/Bookmarks/index.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/WhoCanReply.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "react-hooks/refs": { "count": 1 } }, - "src/screens/Deactivated.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/ageAssurance/AgeAssuranceErrors.tsx": { + "@typescript-eslint/no-misused-promises": { "count": 1 } }, - "src/screens/Login/ChooseAccountForm.tsx": { + "src/components/ageAssurance/AgeAssuranceInitDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/components/contacts/components/OTPInput.tsx": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/contacts/screens/GetContacts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/contacts/screens/VerifyNumber.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/contacts/screens/ViewMatches.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/DeviceLocationRequestDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/dialogs/EmailDialog/components/ResendEmailText.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/screens/Login/ForgotPasswordForm.tsx": { + "src/components/dialogs/EmailDialog/data/useAccountEmailState.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/screens/Update.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/EmailDialog/screens/Verify.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/InAppBrowserConsent.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/components/dialogs/LanguageSelectDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/screens/Login/LoginForm.tsx": { + "src/components/dialogs/LinkWarning.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/components/dialogs/MutedWords.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, - "src/screens/Login/SetNewPasswordForm.tsx": { + "src/components/dialogs/PostInteractionSettingsDialog.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "react-hooks/preserve-manual-memoization": { "count": 1 } }, - "src/screens/Moderation/index.tsx": { + "src/components/dialogs/ServerInput.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/StarterPackDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/SwitchAccount.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/components/dialogs/lists/CreateOrEditListDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, - "src/screens/ModerationInteractionSettings/index.tsx": { + "src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/nuxs/index.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/dms/MessageItem.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/forms/DateField/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/forms/SearchInput.tsx": { + "react-hooks/refs": { + "count": 1 } }, - "src/screens/Onboarding/StepFinished/index.tsx": { + "src/components/forms/SegmentedControl.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/screens/Onboarding/StepInterests/index.tsx": { + "src/components/forms/TextField.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/forms/ToggleButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": { + "src/components/hooks/useFollowMethods.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, - "src/screens/PostThread/index.tsx": { + "src/components/hooks/useFullscreen.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useLandingEntry.native.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useLandingEntry.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useRefreshOnFocus.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/hooks/useRichText.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/images/AutoSizedImage.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + } + }, + "src/components/images/Gallery/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 5 }, - "react-hooks/preserve-manual-memoization": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { "count": 1 } }, - "src/screens/Profile/Header/EditProfileDialog.tsx": { + "src/components/images/Gallery/useKeyboardHandlers.ts": { "@typescript-eslint/no-explicit-any": { - "count": 3 + "count": 6 } }, - "src/screens/Profile/Header/ProfileHeaderLabeler.tsx": { + "src/components/images/Gallery/usePointerHandlers.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 6 + } + }, + "src/components/images/ImageLayoutGrid.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 } }, - "src/screens/Profile/Header/Shell.tsx": { + "src/components/images/ImageLayoutGridItem.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "src/components/intents/VerifyEmailIntentDialog.tsx": { + "@typescript-eslint/no-misused-promises": { "count": 1 } }, - "src/screens/Profile/Sections/Feed.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/components/interstitials/TrendingVideos.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 1 } }, - "src/screens/Profile/components/GermButton.tsx": { + "src/components/moderation/ContentHider.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/moderation/LabelsOnMeDialog.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/moderation/ModerationDetailsDialog.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/verification/VerificationCreatePrompt.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/verification/VerificationRemovePrompt.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/features/liveEvents/preferences.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/features/liveNow/components/LiveStatusDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/features/liveNow/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "src/geolocation/service.ts": { "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, - "src/screens/Settings/FindContactsSettings.tsx": { + "src/lib/ScrollContext.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 3 + } + }, + "src/lib/api/feed/demo.ts": { + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/lib/api/feed/posts.ts": { + "@typescript-eslint/require-await": { "count": 1 } }, - "src/screens/Settings/components/ChangePasswordDialog.tsx": { + "src/lib/api/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 5 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + } + }, + "src/lib/async/retry.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 } }, - "src/screens/Settings/components/DeactivateAccountDialog.tsx": { + "src/lib/async/until.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "src/lib/broadcast/stub.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/screens/Settings/components/DeleteAccountDialog.tsx": { + "src/lib/custom-animations/CountWheel.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/functions.ts": { "@typescript-eslint/no-explicit-any": { + "count": 6 + }, + "@typescript-eslint/no-unsafe-call": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 25 } }, - "src/screens/Signup/StepInfo/Policies.tsx": { + "src/lib/getUserDisplayName.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "src/screens/SignupQueued.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 1 + "src/lib/haptics.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/lib/hooks/useAccountSwitcher.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/lib/hooks/useAnimatedValue.ts": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/hooks/useDraggableScrollView.ts": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/hooks/useIntentHandler.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/lib/hooks/useNonReactiveCallback.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + } + }, + "src/lib/hooks/useNotificationHandler.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 16 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/lib/hooks/useOTAUpdates.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "src/lib/hooks/useOpenLink.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/lib/hooks/usePermissions.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 5 + } + }, + "src/lib/hooks/usePermissions.web.ts": { + "@typescript-eslint/require-await": { + "count": 3 + } + }, + "src/lib/hooks/useRequireEmailVerification.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "src/lib/hooks/useTLDs.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/lib/hooks/useTabFocusEffect.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/hooks/useToggleMutationQueue.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + } + }, + "src/lib/hooks/useWebScrollRestoration.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/lib/international-telephone-codes.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/lib/link-meta/link-meta.ts": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "src/lib/media/manip.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 5 + } + }, + "src/lib/media/manip.web.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 3 + } + }, + "src/lib/media/picker.web.tsx": { + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/lib/media/save-image.ios.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/lib/media/save-image.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/lib/merge-refs.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/lib/notifications/notifications.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 5 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/lib/react-query.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/routes/links.ts": { + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 + } + }, + "src/lib/routes/router.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/lib/routes/types.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/lib/sharing.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/lib/strings/errors.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 14 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 14 + } + }, + "src/lib/translation/index.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/useGetEmojis/getEmojis.ts": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/logger/transports/sentry.ts": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 3 + } + }, + "src/logger/types.ts": { + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 + } + }, + "src/screens/Bookmarks/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/screens/Deactivated.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/E2E/SharedPreferencesTesterScreen.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 6 + }, + "@typescript-eslint/require-await": { + "count": 6 + } + }, + "src/screens/Feeds/NoFollowingFeed.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Feeds/NoSavedFeedsOfAnyType.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Hashtag.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Home/NoFeedsPinned.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/List/ListHiddenScreen.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Log.tsx": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 2 + } + }, + "src/screens/Login/ChooseAccountForm.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Login/ForgotPasswordForm.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Login/LoginForm.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/screens/Login/SetNewPasswordForm.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Login/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/screens/Messages/ChatList.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/screens/Moderation/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/screens/ModerationInteractionSettings/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx": { + "react-hooks/refs": { + "count": 4 + } + }, + "src/screens/Onboarding/StepFinished/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Onboarding/StepInterests/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Onboarding/StepProfile/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/screens/PostThread/index.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "react-hooks/preserve-manual-memoization": { + "count": 1 + }, + "react-hooks/refs": { + "count": 4 + } + }, + "src/screens/Profile/Header/EditProfileDialog.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Profile/Header/ProfileHeaderLabeler.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + } + }, + "src/screens/Profile/Header/Shell.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/screens/Profile/KnownFollowers.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/screens/Profile/Sections/Feed.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Profile/components/GermButton.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Profile/components/ProfileFeedHeader.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 5 + } + }, + "src/screens/ProfileList/FeedSection.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/ProfileList/components/Header.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/screens/ProfileList/components/MoreOptionsMenu.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 5 + } + }, + "src/screens/ProfileList/components/SubscribeMenu.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/screens/ProfileList/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/SavedFeeds.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/screens/Search/Shell.tsx": { + "react-hooks/refs": { + "count": 4 + } + }, + "src/screens/Search/modules/ExploreSuggestedAccounts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Search/modules/ExploreTrendingTopics.tsx": { + "react-hooks/purity": { + "count": 2 + } + }, + "src/screens/Search/modules/ExploreTrendingVideos.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Settings/AboutSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Settings/AppPasswords.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Settings/AutomationLabelSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/screens/Settings/FindContactsSettings.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Settings/InterestsSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Settings/components/ChangeHandleDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Settings/components/ChangePasswordDialog.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/screens/Settings/components/CopyButton.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Settings/components/DeactivateAccountDialog.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Settings/components/DeleteAccountDialog.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + } + }, + "src/screens/Settings/components/DisableEmail2FADialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "src/screens/Settings/components/OTAInfo.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Signup/StepCaptcha/CaptchaWebView.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/screens/Signup/StepHandle/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Signup/StepInfo/Policies.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/screens/Signup/StepInfo/index.tsx": { + "react-hooks/refs": { + "count": 3 + } + }, + "src/screens/SignupQueued.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/screens/StarterPack/StarterPackLandingScreen.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/StarterPack/StarterPackScreen.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/StepFeeds.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/StepProfiles.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Topic.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/state/a11y.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/cache/profile-shadow.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/state/cache/thread-mutes.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/cache/types.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/state/feed-feedback.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + }, + "react-hooks/refs": { + "count": 2 + } + }, + "src/state/gallery.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + } + }, + "src/state/messages/events/agent.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/messages/message-drafts.tsx": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/state/persisted/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/state/persisted/index.web.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/state/persisted/util.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/state/preferences/alt-text-required.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/autoplay.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/disable-haptics.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/external-embeds-prefs.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/hidden-posts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/in-app-browser.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/kawaii.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/state/preferences/languages.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/large-alt-badge.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/subtitles.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/trending.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/used-starter-packs.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/activity-subscriptions.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/actor-autocomplete.ts": { + "react-hooks/immutability": { + "count": 2 + } + }, + "src/state/queries/app-passwords.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/bookmarks/useBookmarkMutation.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/bookmarks/useBookmarksQuery.ts": { + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/state/queries/explore-feed-previews.tsx": { + "react-hooks/refs": { + "count": 4 + } + }, + "src/state/queries/feed.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/handle.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/list-memberships.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/list.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 7 + } + }, + "src/state/queries/messages/accept-conversation.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/messages/update-all-read.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/state/queries/my-lists.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/notifications/feed.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/queries/notifications/unread.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/state/queries/nuxs/definitions.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + } + }, + "src/state/queries/pinned-post.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/post-feed.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + } + }, + "src/state/queries/postgate/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/state/queries/preferences/index.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/state/queries/preferences/useThreadPreferences.ts": { + "react-hooks/refs": { + "count": 2 + } + }, + "src/state/queries/search-posts.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/queries/starter-packs.ts": { + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/state/queries/suggested-follows.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 + } + }, + "src/state/queries/threadgate/index.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 3 + } + }, + "src/state/queries/util.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/state/session/agent.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/state/shell/color-mode.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/shell/composer/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/shell/onboarding.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 5 } }, - "src/state/cache/types.ts": { + "src/state/shell/progress-guide.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 } }, - "src/state/feed-feedback.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 3 + "src/state/shell/reminders.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, - "src/state/messages/events/agent.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/state/shell/tick-every-minute.tsx": { + "react-hooks/purity": { + "count": 1 } }, - "src/state/persisted/index.ts": { - "@typescript-eslint/no-explicit-any": { + "src/storage/archive/index.ts": { + "@typescript-eslint/no-unsafe-member-access": { "count": 1 } }, - "src/state/persisted/index.web.ts": { + "src/storage/index.ts": { "@typescript-eslint/no-explicit-any": { + "count": 8 + }, + "@typescript-eslint/no-unsafe-member-access": { "count": 1 } }, - "src/state/persisted/util.ts": { - "@typescript-eslint/no-explicit-any": { + "src/view/com/auth/SplashScreen.web.tsx": { + "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/state/queries/notifications/feed.ts": { + "src/view/com/composer/Composer.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 - } - }, - "src/state/queries/nuxs/definitions.ts": { - "@typescript-eslint/no-explicit-any": { + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "react-hooks/immutability": { + "count": 2 + }, + "react-hooks/preserve-manual-memoization": { + "count": 3 + }, + "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/state/queries/pinned-post.ts": { - "@typescript-eslint/no-explicit-any": { + "src/view/com/composer/SelectMediaButton.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { "count": 1 } }, - "src/state/queries/post-feed.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 3 + "src/view/com/composer/drafts/DraftsButton.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, - "src/state/queries/postgate/index.ts": { + "src/view/com/composer/drafts/state/queries.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, - "src/state/queries/search-posts.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/view/com/composer/drafts/state/storage.ts": { + "@typescript-eslint/require-await": { + "count": 3 } }, - "src/state/queries/threadgate/index.ts": { - "@typescript-eslint/no-explicit-any": { + "src/view/com/composer/photos/EditImageDialog.web.tsx": { + "@typescript-eslint/no-misused-promises": { "count": 1 } }, - "src/state/queries/util.ts": { - "@typescript-eslint/no-explicit-any": { + "src/view/com/composer/photos/Gallery.tsx": { + "@typescript-eslint/no-floating-promises": { "count": 1 } }, - "src/state/session/agent.ts": { + "src/view/com/composer/photos/OpenCameraBtn.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, - "src/state/shell/progress-guide.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/view/com/composer/select-language/SuggestedLanguage.tsx": { + "react-hooks/refs": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/storage/index.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 8 + "src/view/com/composer/text-input/TextInput.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, - "src/view/com/composer/Composer.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 2 + "src/view/com/composer/text-input/TextInput.web.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 }, - "react-hooks/preserve-manual-memoization": { - "count": 3 + "react-hooks/refs": { + "count": 1 } }, - "src/view/com/composer/drafts/state/queries.ts": { - "@typescript-eslint/no-explicit-any": { + "src/view/com/composer/text-input/web/Autocomplete.tsx": { + "@typescript-eslint/no-unsafe-member-access": { "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 } }, - "src/view/com/composer/photos/OpenCameraBtn.tsx": { - "@typescript-eslint/no-explicit-any": { + "src/view/com/composer/videos/pickVideo.web.ts": { + "@typescript-eslint/no-misused-promises": { "count": 1 } }, "src/view/com/feeds/ComposerPrompt.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/view/com/feeds/FeedPage.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/view/com/feeds/ProfileFeedgens.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/lists/ListMembers.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/lists/MyLists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 6 } }, "src/view/com/lists/ProfileLists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/com/modals/Modal.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/notifications/NotificationFeed.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 6 + }, + "react-hooks/set-state-in-effect": { + "count": 1 } }, "src/view/com/notifications/NotificationFeedItem.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/pager/Pager.tsx": { "@typescript-eslint/no-explicit-any": { "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/pager/Pager.web.tsx": { + "react-hooks/immutability": { + "count": 1 + }, "react-hooks/preserve-manual-memoization": { "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/pager/PagerWithHeader.tsx": { @@ -593,14 +2242,46 @@ "count": 2 } }, + "src/view/com/pager/TabBar.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, "src/view/com/pager/TabBar.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/view/com/post-thread/PostLikedBy.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/post-thread/PostQuotes.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/post-thread/PostRepostedBy.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/posts/FeedShutdownMsg.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/posts/PostFeed.tsx": { @@ -611,16 +2292,47 @@ "src/view/com/posts/PostFeedErrorMessage.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 8 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "src/view/com/profile/ProfileFollowers.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/profile/ProfileFollows.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/profile/ProfileMenu.tsx": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 6 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 12 } }, "src/view/com/testing/TestCtrls.e2e.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 9 } }, "src/view/com/util/EmptyState.tsx": { @@ -631,16 +2343,36 @@ "src/view/com/util/ErrorBoundary.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/EventStopper.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/Link.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/util/LoadingPlaceholder.tsx": { + "react-hooks/purity": { + "count": 1 } }, "src/view/com/util/MainScrollProvider.tsx": { @@ -648,9 +2380,22 @@ "count": 2 } }, + "src/view/com/util/UserAvatar.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/view/com/util/UserBanner.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, "src/view/com/util/ViewSelector.tsx": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/Views.tsx": { @@ -658,14 +2403,101 @@ "count": 1 } }, + "src/view/com/util/Views.web.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/util/forms/Button.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/view/screens/Debug.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/view/screens/Feeds.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/screens/Home.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/view/screens/ModerationBlockedAccounts.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/view/screens/ModerationMutedAccounts.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/screens/Notifications.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/view/screens/Storybook/ListContained.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/screens/Storybook/Storybook.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/shell/Drawer.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/view/shell/createNativeStackNavigatorWithAuth.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "react-hooks/refs": { + "count": 13 + } + }, + "src/view/shell/desktop/Search.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "react-hooks/refs": { + "count": 3 + } + }, + "src/view/shell/index.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 } } } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 50ab8e5da..c8addf66a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -134,11 +134,10 @@ export default defineConfig( 'react-native/no-inline-styles': 'off', ...reactNativeA11y.configs.all.rules, 'react-compiler/react-compiler': 'warn', - // TODO: Fix these and set to error - 'react-hooks/set-state-in-effect': 'warn', - 'react-hooks/purity': 'warn', - 'react-hooks/refs': 'warn', - 'react-hooks/immutability': 'warn', + 'react-hooks/set-state-in-effect': 'error', + 'react-hooks/purity': 'error', + 'react-hooks/refs': 'error', + 'react-hooks/immutability': 'error', /** * Import sorting @@ -235,9 +234,8 @@ export default defineConfig( }, ], /** - * Maintain previous behavior - these are stricter in typescript-eslint - * v8 `warn` ones are probably worth fixing. `off` ones are a bit too - * nit-picky + * Maintain previous behavior via eslint-suppressions.json - these are + * stricter in typescript-eslint v8. `off` ones are a bit too nit-picky. */ '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/ban-ts-comment': 'off', @@ -247,18 +245,18 @@ export default defineConfig( '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-member-access': 'warn', - '@typescript-eslint/no-unsafe-call': 'warn', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-misused-promises': 'warn', - '@typescript-eslint/require-await': 'warn', - '@typescript-eslint/no-unsafe-enum-comparison': 'warn', - '@typescript-eslint/no-unnecessary-type-assertion': 'warn', - '@typescript-eslint/no-redundant-type-constituents': 'warn', - '@typescript-eslint/no-duplicate-type-constituents': 'warn', - '@typescript-eslint/no-base-to-string': 'warn', - '@typescript-eslint/prefer-promise-reject-errors': 'warn', - '@typescript-eslint/await-thenable': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/require-await': 'error', + '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'error', + '@typescript-eslint/no-duplicate-type-constituents': 'error', + '@typescript-eslint/no-base-to-string': 'error', + '@typescript-eslint/prefer-promise-reject-errors': 'error', + '@typescript-eslint/await-thenable': 'error', 'no-restricted-imports': [ 'error', diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index ac5bc4889..3e11cf051 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -8,8 +8,10 @@ import { type UninheritableButtonProps, } from '#/components/Button' import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' +import { + CircleInfo_Stroke2_Corner0_Rounded as CircleInfo, + CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon, +} from '#/components/icons/CircleInfo' import {type Props as SVGIconProps} from '#/components/icons/common' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {dismiss} from '#/components/Toast/sonner' diff --git a/src/lib/hooks/useTimeAgo.ts b/src/lib/hooks/useTimeAgo.ts index 67398ec54..ffc808a91 100644 --- a/src/lib/hooks/useTimeAgo.ts +++ b/src/lib/hooks/useTimeAgo.ts @@ -58,12 +58,12 @@ export function dateDiff( if (diffSeconds < NOW) { diff = { value: 0, - unit: 'now' as DateDiff['unit'], + unit: 'now', } } else if (diffSeconds < MINUTE) { diff = { value: diffSeconds, - unit: 'second' as DateDiff['unit'], + unit: 'second', } } else if (diffSeconds < HOUR) { const value = @@ -72,7 +72,7 @@ export function dateDiff( : Math.floor(diffSeconds / MINUTE) diff = { value, - unit: 'minute' as DateDiff['unit'], + unit: 'minute', } } else if (diffSeconds < DAY) { const value = @@ -81,7 +81,7 @@ export function dateDiff( : Math.floor(diffSeconds / HOUR) diff = { value, - unit: 'hour' as DateDiff['unit'], + unit: 'hour', } } else if (diffSeconds < MONTH_30) { const value = @@ -90,7 +90,7 @@ export function dateDiff( : Math.floor(diffSeconds / DAY) diff = { value, - unit: 'day' as DateDiff['unit'], + unit: 'day', } } else { const value = @@ -99,7 +99,7 @@ export function dateDiff( : Math.floor(diffSeconds / MONTH_30) diff = { value, - unit: 'month' as DateDiff['unit'], + unit: 'month', } } diff --git a/src/lib/routes/links.ts b/src/lib/routes/links.ts index 908db7cba..c87ccb5a3 100644 --- a/src/lib/routes/links.ts +++ b/src/lib/routes/links.ts @@ -19,8 +19,8 @@ export function makeProfileLink( export function makeCustomFeedLink( did: string, rkey: string, - segment?: string | undefined, - feedCacheKey?: 'discover' | 'explore' | undefined, + segment?: string, + feedCacheKey?: 'discover' | 'explore', ) { return ( [`/profile`, did, 'feed', rkey, ...(segment ? [segment] : [])].join('/') + diff --git a/src/screens/Settings/AppIconSettings/index.tsx b/src/screens/Settings/AppIconSettings/index.tsx index 183e161a8..2e716a219 100644 --- a/src/screens/Settings/AppIconSettings/index.tsx +++ b/src/screens/Settings/AppIconSettings/index.tsx @@ -129,7 +129,7 @@ function getAppIconName(icon: string | false): DynamicAppIcon.IconName { if (!icon || icon === 'DEFAULT') { return 'default_light' } else { - return icon as DynamicAppIcon.IconName + return icon } } @@ -151,7 +151,7 @@ function Group({ values={[value]} maxSelections={1} onChange={vals => { - if (vals[0]) onChange(vals[0] as DynamicAppIcon.IconName) + if (vals[0]) onChange(vals[0]) }}> {children} diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx index 85bd4fddf..acaf02453 100644 --- a/src/screens/VideoFeed/components/Scrubber.tsx +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -23,8 +23,7 @@ import { import {useEventListener} from 'expo' import {type VideoPlayer} from 'expo-video' -import {tokens} from '#/alf' -import {atoms as a} from '#/alf' +import {atoms as a, tokens} from '#/alf' import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils' import {Text} from '#/components/Typography' diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 57d83cb5b..ca885107d 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -298,7 +298,7 @@ export function* findAllPostsInQueryData( if (AppBskyFeedDefs.isPostView(item.subject)) { const quotedPost = getEmbeddedPost(item.subject?.embed) if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) { - yield embedViewRecordToPostView(quotedPost!) + yield embedViewRecordToPostView(quotedPost) } } } diff --git a/src/state/queries/nuxs/util.ts b/src/state/queries/nuxs/util.ts index ef323b422..dea35a916 100644 --- a/src/state/queries/nuxs/util.ts +++ b/src/state/queries/nuxs/util.ts @@ -34,7 +34,7 @@ export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined { export function serializeAppNux(nux: AppNux): AppBskyActorDefs.Nux { const {data, ...rest} = nux - const schema = NuxSchemas[nux.id as Nux] + const schema = NuxSchemas[nux.id] const result: AppBskyActorDefs.Nux = { ...rest, diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index c1ec4a12a..7197eb298 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -85,7 +85,7 @@ export function useSuggestedFollowsByActorWithDismiss({ const profiles = useMemo(() => { return (data?.suggestions ?? []).map(profile => ({ - actor: profile as bsky.profile.AnyProfileView, + actor: profile, recId: data?.recId, })) }, [data?.suggestions, data?.recId]) diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 889c208c3..81abd958f 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -196,7 +196,7 @@ export function sortAndAnnotateThreadItems( * `repliesSeenCounter` later on, since `repliesSeenCounter` * is 1-indexed and `replyIndex` is 0-indexed. */ - childMetadata!.replyIndex = + childMetadata.replyIndex = childParentMetadata.repliesSeenCounter } diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index 1099c1a0f..8f178d528 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -470,11 +470,11 @@ export async function draftToComposerPosts( height, mime: 'image/jpeg', }, - } as ComposerImage + } satisfies ComposerImage }) const images = (await Promise.all(imagePromises)).filter( - (img): img is ComposerImage => img !== null, + (img): img is NonNullable => img !== null, ) if (images.length > 0) { embed.media = {type: 'images', images} @@ -511,7 +511,7 @@ export async function draftToComposerPosts( tinygif: mediaObject, preview: mediaObject, }, - } as Gif, + }, alt: gifData.alt, } break diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index b01651d7b..2110c9540 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -55,7 +55,7 @@ export function TestCtrls() { accessibilityLabel="Text input field" accessibilityHint="Enter proxy header" testID="e2eProxyHeaderInput" - onChangeText={val => setProxyHeader(val as any)} + onChangeText={val => setProxyHeader(val)} autoComplete="off" autoCorrect={false} autoCapitalize="none" diff --git a/src/view/com/util/PressableWithHover.tsx b/src/view/com/util/PressableWithHover.tsx index f4006a526..a4a2d194f 100644 --- a/src/view/com/util/PressableWithHover.tsx +++ b/src/view/com/util/PressableWithHover.tsx @@ -3,9 +3,9 @@ import { Pressable, type PressableProps, type StyleProp, + type View, type ViewStyle, } from 'react-native' -import {type View} from 'react-native' import {addStyle} from '#/lib/styles' import {useInteractionState} from '#/components/hooks/useInteractionState' diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 4191f9417..a23ed693e 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -171,7 +171,7 @@ function NativeStackNavigator({ } // Evicted screens get a lightweight placeholder instead of their full tree - finalDescriptors = {} as typeof descriptors + finalDescriptors = {} for (const key in descriptors) { if (mountSet.has(key)) { finalDescriptors[key] = descriptors[key] -- 2.51.2 From 4367b9d047c8a1175aa889b7295a4591ee89c8b2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 3 Jun 2026 23:01:54 +0300 Subject: [PATCH 11/61] [Android] Fix horizontal swipes being cancelled when leaving bounds (#10712) --- src/view/shell/BlockDrawerGesture.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/view/shell/BlockDrawerGesture.tsx b/src/view/shell/BlockDrawerGesture.tsx index bae9a8db8..93ef18cfa 100644 --- a/src/view/shell/BlockDrawerGesture.tsx +++ b/src/view/shell/BlockDrawerGesture.tsx @@ -4,6 +4,8 @@ import {Gesture, GestureDetector} from 'react-native-gesture-handler' export function BlockDrawerGesture({children}: {children: React.ReactNode}) { const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web - const scrollGesture = Gesture.Native().blocksExternalGesture(drawerGesture) + let scrollGesture = Gesture.Native() + .shouldCancelWhenOutside(false) // for some reason defaults to true on Android + .blocksExternalGesture(drawerGesture) return {children} } -- 2.51.2 From f99837040e0eddecb22c54c855d30297d8e0e6c4 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:21:13 -0700 Subject: [PATCH 12/61] Compile i18n before running lint (#10711) --- .github/workflows/bundle-deploy-eas-update.yml | 12 ++++++------ .github/workflows/pull-request-comment.yml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index 983494dd1..5f1f0a4e2 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -71,18 +71,18 @@ jobs: profile: ${{ inputs.channel || 'testflight' }} previous-commit-tag: ${{ inputs.runtimeVersion }} - - name: Lint check - run: pnpm lint - - - name: Prettier check - run: pnpm prettier --check . - - name: 🔤 Compile translations run: pnpm intl:build 2>&1 | tee i18n.log - name: Check for i18n compilation errors run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + - name: Lint check + run: pnpm lint + + - name: Prettier check + run: pnpm prettier --check . + - name: Type check run: pnpm typecheck diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml index a497a5625..8ede46239 100644 --- a/.github/workflows/pull-request-comment.yml +++ b/.github/workflows/pull-request-comment.yml @@ -128,15 +128,15 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Lint check - run: pnpm lint - - name: 🔤 Compile translations run: pnpm intl:build 2>&1 | tee i18n.log - name: Check for i18n compilation errors run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + - name: Lint check + run: pnpm lint + - name: Type check run: pnpm typecheck -- 2.51.2 From 0a5a9941705ddf1255ffaa93c5c1409c4873902e Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Wed, 3 Jun 2026 16:38:36 -0400 Subject: [PATCH 13/61] Fix image peek in carousels for quote posts (#10688) Co-authored-by: Eric Bailey --- src/components/Post/Embed/ImageEmbed.tsx | 9 +++------ src/components/images/Gallery/index.tsx | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index ccd1ceb27..2c6bb6c5d 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -87,8 +87,7 @@ export function ImageEmbed({ crop={ rest.viewContext === PostEmbedViewContext.ThreadHighlighted ? 'none' - : rest.viewContext === - PostEmbedViewContext.FeedEmbedRecordWithMedia + : rest.isWithinQuote ? 'square' : 'constrained' } @@ -103,10 +102,7 @@ export function ImageEmbed({ onPress(0, [containerRef], [dims]) } onPressIn={() => onPressIn(0)} - hideBadge={ - rest.viewContext === - PostEmbedViewContext.FeedEmbedRecordWithMedia - } + hideBadge={rest.isWithinQuote} /> @@ -121,6 +117,7 @@ export function ImageEmbed({ onPress={onPress} onPressIn={onPressIn} viewContext={rest.viewContext} + isWithinQuote={rest.isWithinQuote} /> ) diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index f4ffcd715..fe2b63e60 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -54,6 +54,7 @@ interface GalleryProps { ) => void onPressIn?: (index: number) => void viewContext?: PostEmbedViewContext + isWithinQuote?: boolean } const Context = createContext<{ @@ -97,6 +98,7 @@ export function Gallery({ onPress, onPressIn, viewContext, + isWithinQuote, }: GalleryProps) { const {t: l} = useLingui() const ax = useAnalytics() @@ -104,14 +106,21 @@ export function Gallery({ const largeAltBadge = useLargeAltBadgeEnabled() const bps = useBreakpoints() const window = useWindowDimensions() - const isWithinQuote = - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia const isWithinChat = viewContext === PostEmbedViewContext.ChatMessage const hideBadges = isWithinQuote const contentHeight = useMemo(() => { if (isWithinChat) { return 120 } + if (isWithinQuote) { + if (bps.gtMobile) { + return 220 + } else if (bps.gtPhone) { + return 190 + } else { + return 150 + } + } if (bps.gtMobile) { return 300 } else if (bps.gtPhone) { @@ -119,7 +128,7 @@ export function Gallery({ } else { return 200 } - }, [bps, isWithinChat]) + }, [bps, isWithinChat, isWithinQuote]) /* * Container overflow styles @@ -220,7 +229,7 @@ export function Gallery({ crop={ viewContext === PostEmbedViewContext.ThreadHighlighted ? 'none' - : viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + : isWithinQuote ? 'square' : 'constrained' } @@ -229,9 +238,7 @@ export function Gallery({ onPress?.(index, [containerRef], [dims]) } onPressIn={() => onPressIn?.(index)} - hideBadge={ - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - } + hideBadge={isWithinQuote} /> ))}
-- 2.51.2 From 57ae26fcaabfb3e9bcbb75c74ca3726975cb82fd Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 3 Jun 2026 23:57:45 +0300 Subject: [PATCH 14/61] Make extra sure the drawer gesture doesn't accidentally trigger on Android (#10713) --- src/view/shell/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 827ab092c..9b215d9f1 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -174,8 +174,15 @@ function DrawerLayout({children}: {children: React.ReactNode}) { // so fail the drawer gesture immediately. .failOffsetX(-1) // Don't rush declaring that a movement to the right - // is a drawer swipe. It could be a vertical scroll. - .activeOffsetX(5) + // is a drawer swipe. It could be a vertical scroll, or a + // slow horizontal carousel swipe. On Android a child + // `blocksExternalGesture` only holds the drawer off once the + // native scroll has activated, which on a slow swipe doesn't + // happen until movement crosses the native touch slop + // (~8-16px). Activating the drawer below that lets a slow + // carousel swipe pop the drawer open (APP-2119), so require + // more travel before claiming on Android. + .activeOffsetX(IS_ANDROID ? 20 : 5) ) } } else { -- 2.51.2 From 153772b760ef1f67220efe40ca6b4c66d33af71b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 4 Jun 2026 01:03:11 +0300 Subject: [PATCH 15/61] `expo-image` tweaks (#10710) --- src/components/ContextMenu/index.tsx | 1 + .../pager/ImageItem/ImageItem.ios.tsx | 1 + src/components/Lightbox/pager/ImagePager.tsx | 4 ++++ src/components/MediaPreview.tsx | 1 + .../Post/Embed/ExternalEmbed/index.tsx | 1 + .../Post/Embed/StandardSiteEmbed/index.tsx | 1 + .../contacts/FindContactsBannerNUX.tsx | 1 + .../contacts/components/HeroImage.tsx | 1 + .../dialogs/nuxs/ActivitySubscriptions.tsx | 1 + .../dialogs/nuxs/BookmarksAnnouncement.tsx | 1 + .../dialogs/nuxs/DraftsAnnouncement.tsx | 1 + .../dialogs/nuxs/FindContactsAnnouncement.tsx | 1 + .../nuxs/InitialVerificationAnnouncement.tsx | 2 ++ .../dialogs/nuxs/LiveNowBetaDialog.tsx | 1 + src/components/images/AutoSizedImage.tsx | 1 + src/components/images/Gallery/index.tsx | 1 + src/components/images/ImageLayoutGridItem.tsx | 1 + .../verification/VerifierDialog.tsx | 1 + .../components/LiveEventFeedCardCompact.tsx | 1 + .../components/LiveEventFeedCardWide.tsx | 1 + .../liveNow/components/LinkPreview.tsx | 1 + .../liveNow/components/LiveStatusDialog.tsx | 1 + .../StepFinished/ValuePropositionPager.tsx | 2 ++ src/screens/Profile/components/GermButton.tsx | 1 + src/view/com/composer/ComposerReplyTo.tsx | 20 +++++++++---------- src/view/com/composer/photos/Gallery.tsx | 1 + src/view/com/util/UserAvatar.tsx | 1 + src/view/com/util/UserBanner.tsx | 2 ++ 28 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index 8d3d39a74..51bf83309 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -499,6 +499,7 @@ function TriggerClone({ accessibilityLabel={label} accessibilityHint={_(msg`The subject of the context menu`)} accessibilityIgnoresInvertColors={false} + cachePolicy="none" /> ) diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx index c0bb99c89..19373cbe1 100644 --- a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx +++ b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx @@ -246,6 +246,7 @@ const ImageItem = ({ } } cachePolicy="memory" + useAppleWebpCodec /> diff --git a/src/components/Lightbox/pager/ImagePager.tsx b/src/components/Lightbox/pager/ImagePager.tsx index 1c8836a0f..7b8815f27 100644 --- a/src/components/Lightbox/pager/ImagePager.tsx +++ b/src/components/Lightbox/pager/ImagePager.tsx @@ -32,6 +32,7 @@ import Animated, { withSpring, type WithSpringConfig, } from 'react-native-reanimated' +import {Image} from 'expo-image' import * as ScreenOrientation from 'expo-screen-orientation' import {type Dimensions} from '#/lib/media/types' @@ -136,6 +137,9 @@ export default function ImageViewRoot({ 'worklet' thumbRects.set({}) })() + requestIdleCallback(() => { + void Image.clearMemoryCache() + }) }, [thumbRects]) useAnimatedReaction( diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index 9aad6da86..b36705d84 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -129,6 +129,7 @@ export function ImageItem({ contentFit="cover" accessible={true} accessibilityIgnoresInvertColors + useAppleWebpCodec /> {children} diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index 43bfbb474..fe0be2bd2 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -108,6 +108,7 @@ export const ExternalEmbed = ({ source={{uri: imageUri}} accessibilityIgnoresInvertColors loading="lazy" + useAppleWebpCodec /> ) : undefined} diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 70fce8ad7..5f7fc1f5e 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -166,6 +166,7 @@ export const StandardSiteEmbed = ({ source={{uri: imageUri}} accessibilityIgnoresInvertColors loading="lazy" + useAppleWebpCodec /> ) : undefined} diff --git a/src/components/contacts/FindContactsBannerNUX.tsx b/src/components/contacts/FindContactsBannerNUX.tsx index 2997e00d4..417379fa2 100644 --- a/src/components/contacts/FindContactsBannerNUX.tsx +++ b/src/components/contacts/FindContactsBannerNUX.tsx @@ -60,6 +60,7 @@ export function FindContactsBannerNUX() { a.self_end, a.mt_sm, ]} + useAppleWebpCodec /> ) diff --git a/src/components/dialogs/nuxs/ActivitySubscriptions.tsx b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx index ed4306459..318b13041 100644 --- a/src/components/dialogs/nuxs/ActivitySubscriptions.tsx +++ b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx @@ -113,6 +113,7 @@ export function ActivitySubscriptionsNUX() { alt={_( msg`A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature.`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx index f7e4bf04c..153130f8e 100644 --- a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx +++ b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx @@ -124,6 +124,7 @@ export function BookmarksAnnouncement() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/DraftsAnnouncement.tsx b/src/components/dialogs/nuxs/DraftsAnnouncement.tsx index 79488fe4d..37b93bda7 100644 --- a/src/components/dialogs/nuxs/DraftsAnnouncement.tsx +++ b/src/components/dialogs/nuxs/DraftsAnnouncement.tsx @@ -101,6 +101,7 @@ export function DraftsAnnouncement() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx index 8fd7d2d2c..70f29ef15 100644 --- a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx +++ b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx @@ -78,6 +78,7 @@ export function FindContactsAnnouncement() { alt={_( msg`An illustration depicting user avatars flowing from a contact book into the Bluesky app`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx index 85bc29a48..cbe2555aa 100644 --- a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx +++ b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx @@ -85,6 +85,7 @@ export function InitialVerificationAnnouncement() { alt={_( msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, )} + useAppleWebpCodec /> @@ -119,6 +120,7 @@ export function InitialVerificationAnnouncement() { alt={_( msg`An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name.`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx index 49c43a1e4..e389e1117 100644 --- a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx +++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx @@ -150,6 +150,7 @@ export function LiveNowBetaDialog() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/images/AutoSizedImage.tsx b/src/components/images/AutoSizedImage.tsx index 79b41311c..182bfe661 100644 --- a/src/components/images/AutoSizedImage.tsx +++ b/src/components/images/AutoSizedImage.tsx @@ -142,6 +142,7 @@ export function AutoSizedImage({ } }} loading="lazy" + useAppleWebpCodec /> diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index fe2b63e60..9ae677c2d 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -480,6 +480,7 @@ function GalleryImage({ height: e.source.height, }) }} + useAppleWebpCodec /> {(hasAlt || isCropped) && !hideBadges ? ( diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index 0c349b661..d6cbc0ade 100644 --- a/src/components/images/ImageLayoutGridItem.tsx +++ b/src/components/images/ImageLayoutGridItem.tsx @@ -106,6 +106,7 @@ export function GalleryItem({ } }} loading="lazy" + useAppleWebpCodec /> diff --git a/src/components/verification/VerifierDialog.tsx b/src/components/verification/VerifierDialog.tsx index 570441ecd..e8944024c 100644 --- a/src/components/verification/VerifierDialog.tsx +++ b/src/components/verification/VerifierDialog.tsx @@ -87,6 +87,7 @@ function Inner({ alt={_( msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, )} + useAppleWebpCodec /> diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx index df2bb17f8..9d2f948eb 100644 --- a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx @@ -69,6 +69,7 @@ export function LiveEventFeedCardCompact({ style={[a.absolute, a.inset_0, a.w_full, a.h_full]} contentFit="cover" placeholderContentFit="cover" + useAppleWebpCodec /> setImageLoadError(false)} onError={() => setImageLoadError(true)} + useAppleWebpCodec /> )} {linkMeta && (!linkMeta.image || imageLoadError) && ( diff --git a/src/features/liveNow/components/LiveStatusDialog.tsx b/src/features/liveNow/components/LiveStatusDialog.tsx index 545e8ab94..9e916678f 100644 --- a/src/features/liveNow/components/LiveStatusDialog.tsx +++ b/src/features/liveNow/components/LiveStatusDialog.tsx @@ -147,6 +147,7 @@ export function LiveStatus({ contentFit="cover" style={[a.absolute, a.inset_0]} accessibilityIgnoresInvertColors + useAppleWebpCodec /> {page === 1 && ( {_(msg`Your )} diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx index 0bef3a22c..e37e57edb 100644 --- a/src/screens/Profile/components/GermButton.tsx +++ b/src/screens/Profile/components/GermButton.tsx @@ -105,6 +105,7 @@ function GermLogo({size}: {size: 'small' | 'large'}) { source={require('../../../../assets/images/germ_logo.webp')} accessibilityIgnoresInvertColors={false} contentFit="cover" + useAppleWebpCodec style={[ a.rounded_full, size === 'large' ? {width: 32, height: 32} : {width: 16, height: 16}, diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index a6ef1a344..bac3e74d3 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -151,8 +151,8 @@ function ComposerReplyToImages({ )) || (images.length === 2 && ( @@ -160,14 +160,14 @@ function ComposerReplyToImages({ )) || @@ -176,21 +176,21 @@ function ComposerReplyToImages({ @@ -201,28 +201,28 @@ function ComposerReplyToImages({ diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 7624b585f..2cc000019 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -245,6 +245,7 @@ const GalleryItem = ({ }} accessible={true} accessibilityIgnoresInvertColors + enforceEarlyResizing cachePolicy="none" autoplay={false} contentFit="cover" diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 420fd0270..9db4914b2 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -330,6 +330,7 @@ let UserAvatar = ({ }} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} onLoad={onLoad} + useAppleWebpCodec /> )} {!noBorder && } diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 8146b7ad9..033e8c14f 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -129,6 +129,7 @@ export function UserBanner({ source={{uri: banner}} accessible={true} accessibilityIgnoresInvertColors + useAppleWebpCodec /> ) : ( ) : ( Date: Thu, 4 Jun 2026 03:21:17 +0000 Subject: [PATCH 16/61] Nightly source-language update --- src/locale/locales/en/messages.po | 424 +++++++++++++++--------------- 1 file changed, 212 insertions(+), 212 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index b1fd0337d..5830797c5 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -93,7 +93,7 @@ msgstr "{0, plural, one {# person} other {# people}}" #. placeholder {0}: reactions.length #. placeholder {1}: groupedReactions.map(g => g.value).join(' ') -#: src/components/dms/MessageItem.tsx:297 +#: src/components/dms/MessageItem.tsx:273 msgid "{0, plural, one {# person} other {# people}} reacted – {1}" msgstr "{0, plural, one {# person} other {# people}} reacted – {1}" @@ -122,7 +122,7 @@ msgstr "" #. How long it takes to read an article, in minutes. Displayed in a short form, e.g. "5m" for 5 minutes. #. placeholder {0}: view.readingTime -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:232 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:233 msgid "{0, plural, one {#m} other {#m}}" msgstr "{0, plural, one {#m} other {#m}}" @@ -211,7 +211,7 @@ msgid "{0} following" msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:640 +#: src/components/dms/MessageItem.tsx:609 msgid "{0} is blocking you" msgstr "{0} is blocking you" @@ -269,7 +269,7 @@ msgstr "" #. placeholder {0}: createSanitizedDisplayName(memberSender) #. placeholder {1}: reaction.value -#: src/components/dms/MessageItem.tsx:292 +#: src/components/dms/MessageItem.tsx:268 msgid "{0} reacted {1}" msgstr "" @@ -314,8 +314,8 @@ msgstr "" #. placeholder {0}: sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), ) #. placeholder {0}: sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), ) -#: src/view/com/util/UserAvatar.tsx:595 -#: src/view/com/util/UserAvatar.tsx:613 +#: src/view/com/util/UserAvatar.tsx:596 +#: src/view/com/util/UserAvatar.tsx:614 msgid "{0}'s avatar" msgstr "" @@ -839,7 +839,7 @@ msgstr "" msgid "A new code has been sent" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:93 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:94 msgid "A new form of verification" msgstr "" @@ -897,7 +897,7 @@ msgid "Accept join request" msgstr "Accept join request" #. Accept a chat request -#: src/screens/Messages/components/RequestListItem.tsx:48 +#: src/screens/Messages/components/RequestListItem.tsx:51 msgid "Accept Request" msgstr "" @@ -999,7 +999,7 @@ msgctxt "toast" msgid "Account unmuted" msgstr "" -#: src/components/verification/VerifierDialog.tsx:99 +#: src/components/verification/VerifierDialog.tsx:100 msgid "Accounts with a scalloped blue check mark <0><1/> can verify others. These trusted verifiers are selected by Bluesky." msgstr "" @@ -1085,7 +1085,7 @@ msgstr "" msgid "Add App Password" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:166 +#: src/screens/Settings/AutomationLabelSettings.tsx:170 msgid "Add automation label to account" msgstr "Add automation label to account" @@ -1112,8 +1112,8 @@ msgstr "" msgid "Add members" msgstr "Add members" -#: src/components/moderation/ReportDialog/index.tsx:526 -#: src/components/moderation/ReportDialog/index.tsx:530 +#: src/components/moderation/ReportDialog/index.tsx:528 +#: src/components/moderation/ReportDialog/index.tsx:532 msgid "Add more details (optional)" msgstr "" @@ -1215,7 +1215,7 @@ msgstr "" msgid "Additional details (limit 1000 characters)" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:544 +#: src/components/moderation/ReportDialog/index.tsx:546 msgid "Additional details (limit 300 characters)" msgstr "" @@ -1283,7 +1283,7 @@ msgid "alice@example.com" msgstr "" #. the default tab in the interests tab bar -#: src/components/dms/ReactionsDialog.tsx:284 +#: src/components/dms/ReactionsDialog.tsx:288 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:201 #: src/view/screens/Notifications.tsx:86 msgid "All" @@ -1291,7 +1291,7 @@ msgstr "" #. Tab label showing the total count of reactions on a chat message. #. placeholder {0}: tab.count -#: src/components/dms/ReactionsDialog.tsx:381 +#: src/components/dms/ReactionsDialog.tsx:385 msgid "All {0}" msgstr "All {0}" @@ -1388,9 +1388,9 @@ msgstr "" msgid "Already signed in as @{0}" msgstr "" -#: src/components/images/AutoSizedImage.tsx:203 -#: src/components/images/Gallery/index.tsx:531 -#: src/components/images/ImageLayoutGridItem.tsx:138 +#: src/components/images/AutoSizedImage.tsx:204 +#: src/components/images/Gallery/index.tsx:539 +#: src/components/images/ImageLayoutGridItem.tsx:139 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94 #: src/view/com/composer/GifAltText.tsx:100 #: src/view/com/composer/photos/Gallery.tsx:209 @@ -1540,7 +1540,7 @@ msgstr "" msgid "An issue occurred, please try again." msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:120 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:121 msgid "An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name." msgstr "" @@ -1729,7 +1729,7 @@ msgstr "" msgid "Are you sure you want to delete the app password \"{0}\"?" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:223 +#: src/components/dms/MessageOverlays.tsx:167 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." @@ -1968,7 +1968,7 @@ msgstr "" msgid "Block accounts" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:143 +#: src/components/dms/AfterReportDialog.tsx:148 msgid "Block and delete" msgstr "Block and delete" @@ -1992,9 +1992,9 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:221 #: src/components/dms/AfterReportConversationDialog.tsx:224 -#: src/components/dms/AfterReportDialog.tsx:149 -#: src/components/dms/AfterReportDialog.tsx:184 -#: src/components/dms/AfterReportDialog.tsx:187 +#: src/components/dms/AfterReportDialog.tsx:154 +#: src/components/dms/AfterReportDialog.tsx:189 +#: src/components/dms/AfterReportDialog.tsx:192 msgid "Block user" msgstr "" @@ -2004,7 +2004,7 @@ msgctxt "button" msgid "Block user" msgstr "Block user" -#: src/components/dms/AfterReportDialog.tsx:180 +#: src/components/dms/AfterReportDialog.tsx:185 msgid "Block user and/or delete this conversation" msgstr "" @@ -2085,7 +2085,7 @@ msgstr "" msgid "Bluesky is more fun with friends" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:107 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:108 msgid "Bluesky is more fun with friends! Import your contacts to see who’s already here." msgstr "" @@ -2109,7 +2109,7 @@ msgstr "" msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:134 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:136 msgid "Bluesky will proactively verify notable and authentic accounts." msgstr "" @@ -2198,7 +2198,7 @@ msgstr "" #. placeholder {0}: sanitizeHandle(handle, '@') #. placeholder {0}: sanitizeHandle(item.feed.creator.handle, '@') #: src/components/LabelingServiceCard/index.tsx:62 -#: src/components/moderation/ReportDialog/index.tsx:845 +#: src/components/moderation/ReportDialog/index.tsx:847 #: src/screens/Messages/JoinRequest.tsx:166 #: src/screens/Search/components/StarterPackCard.tsx:107 #: src/screens/Search/Explore.tsx:971 @@ -2322,7 +2322,7 @@ msgstr "" msgid "Captions & alt text" msgstr "" -#: src/components/images/Gallery/index.tsx:256 +#: src/components/images/Gallery/index.tsx:263 msgid "carousel" msgstr "carousel" @@ -2355,7 +2355,7 @@ msgstr "" msgid "Change Handle" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:443 +#: src/components/moderation/ReportDialog/index.tsx:445 msgid "Change moderation service" msgstr "" @@ -2368,11 +2368,11 @@ msgstr "" msgid "Change password dialog" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:310 +#: src/components/moderation/ReportDialog/index.tsx:312 msgid "Change report category" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:388 +#: src/components/moderation/ReportDialog/index.tsx:390 msgid "Change report reason" msgstr "" @@ -2643,7 +2643,7 @@ msgstr "Click here to view the invite link for this group chat" msgid "Click to open tag menu for {0}" msgstr "" -#: src/components/dms/MessageItem.tsx:559 +#: src/components/dms/MessageItem.tsx:528 msgid "Click to retry failed message" msgstr "" @@ -2657,15 +2657,15 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:233 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:239 #: src/components/dialogs/LanguageSelectDialog.tsx:359 -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:159 -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:168 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:164 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:172 -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:139 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:176 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:185 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:191 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:199 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:160 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:169 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:165 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:173 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:140 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:187 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:192 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:200 #: src/components/dialogs/SearchablePeopleList.tsx:340 #: src/components/dialogs/StarterPackDialog.tsx:187 #: src/components/dms/AddMembersFlow.tsx:367 @@ -2673,10 +2673,10 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:96 #: src/components/dms/AfterReportConversationDialog.tsx:247 #: src/components/dms/AfterReportConversationDialog.tsx:252 -#: src/components/dms/AfterReportDialog.tsx:89 #: src/components/dms/AfterReportDialog.tsx:94 -#: src/components/dms/AfterReportDialog.tsx:207 +#: src/components/dms/AfterReportDialog.tsx:99 #: src/components/dms/AfterReportDialog.tsx:212 +#: src/components/dms/AfterReportDialog.tsx:217 #: src/components/dms/EmojiPopup.android.tsx:59 #: src/components/dms/InitiateChatFlow.tsx:534 #: src/components/intents/GroupChatJoinDialog.tsx:204 @@ -2688,7 +2688,7 @@ msgstr "" #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:122 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:128 #: src/components/verification/VerificationsDialog.tsx:146 -#: src/components/verification/VerifierDialog.tsx:146 +#: src/components/verification/VerifierDialog.tsx:147 #: src/components/WhoCanReply.tsx:236 #: src/components/WhoCanReply.tsx:243 #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:45 @@ -2724,7 +2724,7 @@ msgstr "" #: src/components/dialogs/LanguageSelectDialog.tsx:354 #: src/components/dialogs/NotificationSettingsDialog.tsx:94 #: src/components/verification/VerificationsDialog.tsx:138 -#: src/components/verification/VerifierDialog.tsx:139 +#: src/components/verification/VerifierDialog.tsx:140 #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:36 msgid "Close dialog" msgstr "" @@ -2999,8 +2999,8 @@ msgstr "" msgid "Conversation deleted" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:144 -#: src/components/dms/AfterReportDialog.tsx:147 +#: src/components/dms/AfterReportDialog.tsx:149 +#: src/components/dms/AfterReportDialog.tsx:152 msgctxt "toast" msgid "Conversation deleted" msgstr "" @@ -3020,7 +3020,7 @@ msgstr "Conversation not found." msgid "Copied build version to clipboard" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:79 +#: src/components/dms/MessageContextMenu.tsx:71 #: src/components/PostControls/DiscoverDebug.tsx:36 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:272 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:77 @@ -3102,8 +3102,8 @@ msgstr "" msgid "Copy link to starter pack" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:169 -#: src/components/dms/MessageContextMenu.tsx:173 +#: src/components/dms/MessageContextMenu.tsx:152 +#: src/components/dms/MessageContextMenu.tsx:156 msgid "Copy message text" msgstr "" @@ -3160,7 +3160,7 @@ msgid "Could not follow all matches. {0}" msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:158 -#: src/components/dms/AfterReportDialog.tsx:134 +#: src/components/dms/AfterReportDialog.tsx:139 #: src/components/dms/LeaveConvoPrompt.tsx:34 msgid "Could not leave chat" msgstr "" @@ -3316,8 +3316,8 @@ msgstr "Create or modify an invite link for this group chat" #. Accessibility label for button to create a moderation report for the selected option #. placeholder {0}: option.title -#: src/components/moderation/ReportDialog/index.tsx:707 -#: src/components/moderation/ReportDialog/index.tsx:752 +#: src/components/moderation/ReportDialog/index.tsx:709 +#: src/components/moderation/ReportDialog/index.tsx:754 msgid "Create report for {0}" msgstr "" @@ -3418,7 +3418,7 @@ msgstr "" msgid "Default icons" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:224 +#: src/components/dms/MessageOverlays.tsx:168 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:803 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:275 #: src/screens/Settings/AppPasswords.tsx:213 @@ -3459,15 +3459,15 @@ msgstr "" msgid "Delete contacts" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:146 -#: src/components/dms/AfterReportDialog.tsx:190 -#: src/components/dms/AfterReportDialog.tsx:193 +#: src/components/dms/AfterReportDialog.tsx:151 +#: src/components/dms/AfterReportDialog.tsx:195 +#: src/components/dms/AfterReportDialog.tsx:198 #: src/screens/Messages/components/RequestButtons.tsx:142 #: src/screens/Messages/components/RequestButtons.tsx:144 msgid "Delete conversation" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:184 +#: src/components/dms/MessageContextMenu.tsx:167 msgid "Delete for me" msgstr "" @@ -3476,11 +3476,11 @@ msgstr "" msgid "Delete list" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:222 +#: src/components/dms/MessageOverlays.tsx:166 msgid "Delete message" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:181 +#: src/components/dms/MessageContextMenu.tsx:164 msgid "Delete message for me" msgstr "" @@ -3661,8 +3661,8 @@ msgstr "" msgid "Discard post?" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:261 -#: src/screens/Profile/components/GermButton.tsx:268 +#: src/screens/Profile/components/GermButton.tsx:262 +#: src/screens/Profile/components/GermButton.tsx:269 msgid "Disconnect Germ DM" msgstr "" @@ -3690,7 +3690,7 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/components/contacts/FindContactsBannerNUX.tsx:77 +#: src/components/contacts/FindContactsBannerNUX.tsx:78 msgid "Dismiss banner" msgstr "" @@ -3784,7 +3784,7 @@ msgstr "" #: src/components/dialogs/ServerInput.tsx:237 #: src/components/dialogs/ServerInput.tsx:239 #: src/components/dms/AfterReportConversationDialog.tsx:164 -#: src/components/dms/AfterReportDialog.tsx:140 +#: src/components/dms/AfterReportDialog.tsx:145 #: src/components/forms/DateField/index.tsx:104 #: src/components/forms/DateField/index.tsx:110 #: src/lib/media/picker.tsx:37 @@ -3805,7 +3805,7 @@ msgctxt "action" msgid "Done" msgstr "" -#: src/components/dms/MessageItem.tsx:463 +#: src/components/dms/MessageItem.tsx:432 msgid "Double tap or long press the message to add a reaction" msgstr "" @@ -3848,7 +3848,7 @@ msgstr "Download profile data" msgid "Doxxing" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:119 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:120 #: src/view/com/composer/drafts/DraftsButton.tsx:70 #: src/view/com/composer/drafts/DraftsButton.tsx:79 #: src/view/com/composer/drafts/DraftsListDialog.tsx:119 @@ -3917,7 +3917,7 @@ msgctxt "action" msgid "Edit" msgstr "" -#: src/view/com/util/UserAvatar.tsx:460 +#: src/view/com/util/UserAvatar.tsx:461 #: src/view/com/util/UserBanner.tsx:122 msgid "Edit avatar" msgstr "" @@ -4299,7 +4299,7 @@ msgstr "" msgid "Exit fullscreen" msgstr "" -#: src/components/Lightbox/chrome/Footer.tsx:65 +#: src/components/Lightbox/chrome/Footer.tsx:59 #: src/components/Lightbox/Lightbox.web.tsx:241 msgid "Expand alt text" msgstr "" @@ -4420,7 +4420,7 @@ msgid "Failed to accept join request" msgstr "Failed to accept join request" #: src/components/dms/ActionsWrapper.web.tsx:77 -#: src/components/dms/MessageContextMenu.tsx:119 +#: src/components/dms/MessageContextMenu.tsx:103 msgid "Failed to add emoji reaction" msgstr "" @@ -4465,7 +4465,7 @@ msgctxt "toast" msgid "Failed to delete chat" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:101 +#: src/components/dms/MessageOverlays.tsx:112 msgid "Failed to delete message" msgstr "" @@ -4482,7 +4482,7 @@ msgid "Failed to disable invite link" msgstr "Failed to disable invite link" #. placeholder {0}: error?.message -#: src/screens/Profile/components/GermButton.tsx:195 +#: src/screens/Profile/components/GermButton.tsx:196 msgid "Failed to disconnect Germ DM. Error: {0}" msgstr "" @@ -4593,7 +4593,7 @@ msgid "Failed to pin post" msgstr "" #. placeholder {0}: e?.message -#: src/screens/Profile/components/GermButton.tsx:163 +#: src/screens/Profile/components/GermButton.tsx:164 msgid "Failed to reconnect Germ DM. Error: {0}" msgstr "" @@ -4607,8 +4607,8 @@ msgid "Failed to remove data. {0}" msgstr "" #: src/components/dms/ActionsWrapper.web.tsx:73 -#: src/components/dms/MessageContextMenu.tsx:115 -#: src/components/dms/ReactionsDialog.tsx:171 +#: src/components/dms/MessageContextMenu.tsx:99 +#: src/components/dms/ReactionsDialog.tsx:175 msgid "Failed to remove emoji reaction" msgstr "" @@ -4833,11 +4833,11 @@ msgstr "" msgid "Finalizing" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:145 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:146 msgid "Finally!" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:155 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:156 msgid "Finally! Keep track of posts that matter to you. Save them to revisit anytime." msgstr "" @@ -4885,7 +4885,7 @@ msgstr "" msgid "Find posts, users, and feeds on Bluesky" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:97 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:98 msgid "Find your friends" msgstr "" @@ -5191,20 +5191,20 @@ msgid "Generate new link" msgstr "Generate new link" #: src/screens/Profile/components/GermButton.tsx:86 -#: src/screens/Profile/components/GermButton.tsx:224 +#: src/screens/Profile/components/GermButton.tsx:225 msgid "Germ DM" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:182 +#: src/screens/Profile/components/GermButton.tsx:183 msgid "Germ DM disconnected" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:233 -#: src/screens/Profile/components/GermButton.tsx:238 +#: src/screens/Profile/components/GermButton.tsx:234 +#: src/screens/Profile/components/GermButton.tsx:239 msgid "Germ DM Link" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:159 +#: src/screens/Profile/components/GermButton.tsx:160 msgid "Germ DM reconnected" msgstr "" @@ -5268,7 +5268,7 @@ msgstr "" msgid "Get notified when {name} posts" msgstr "" -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:138 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:139 msgid "Get notified when someone posts" msgstr "" @@ -5279,7 +5279,7 @@ msgstr "" msgid "Get started" msgstr "" -#: src/components/MediaPreview.tsx:147 +#: src/components/MediaPreview.tsx:148 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:69 msgid "GIF" msgstr "" @@ -5413,8 +5413,8 @@ msgstr "Go to user’s profile" msgid "Going live is currently disabled for your account" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:252 -#: src/screens/Profile/components/GermButton.tsx:257 +#: src/screens/Profile/components/GermButton.tsx:253 +#: src/screens/Profile/components/GermButton.tsx:258 msgid "Got it" msgstr "" @@ -5524,7 +5524,7 @@ msgid "Handle too long. Please try a shorter one." msgstr "" #: src/components/FeedCard.tsx:156 -#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:122 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:123 msgid "Happening now" msgstr "" @@ -5903,7 +5903,7 @@ msgid "Image" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:436 +#: src/components/images/Gallery/index.tsx:443 msgid "Image {0}" msgstr "Image {0}" @@ -5914,7 +5914,7 @@ msgid "Image {0} of {1}" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:421 +#: src/components/images/Gallery/index.tsx:428 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5929,17 +5929,17 @@ msgid "Image cache cleared, freed {0}" msgstr "" #. placeholder {0}: images.length -#: src/components/images/Gallery/index.tsx:257 +#: src/components/images/Gallery/index.tsx:264 msgid "Image gallery, {0} images" msgstr "Image gallery, {0} images" #. Image has been moderated and user has the option of showing it temporarily -#: src/features/liveNow/components/LiveStatusDialog.tsx:299 +#: src/features/liveNow/components/LiveStatusDialog.tsx:300 msgid "Image is hidden due to your moderation settings." msgstr "Image is hidden due to your moderation settings." #. Image has been moderated and is not visible to the user -#: src/features/liveNow/components/LiveStatusDialog.tsx:309 +#: src/features/liveNow/components/LiveStatusDialog.tsx:310 msgid "Image is unavailable." msgstr "Image is unavailable." @@ -5974,13 +5974,13 @@ msgstr "" msgid "Import contacts" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:115 -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:126 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:116 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:127 msgid "Import Contacts" msgstr "" #: src/components/contacts/FindContactsBannerNUX.tsx:33 -#: src/components/contacts/FindContactsBannerNUX.tsx:71 +#: src/components/contacts/FindContactsBannerNUX.tsx:72 msgid "Import contacts to find your friends" msgstr "" @@ -6093,7 +6093,7 @@ msgstr "" msgid "Invalid phone number" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:98 +#: src/components/moderation/ReportDialog/index.tsx:100 msgid "Invalid report subject" msgstr "" @@ -6311,7 +6311,7 @@ msgid "Latest" msgstr "" #: src/components/verification/VerificationsDialog.tsx:168 -#: src/components/verification/VerifierDialog.tsx:135 +#: src/components/verification/VerifierDialog.tsx:136 #: src/screens/Moderation/VerificationSettings.tsx:50 #: src/screens/Profile/Header/EditProfileDialog.tsx:346 #: src/screens/Settings/components/ChangeHandleDialog.tsx:215 @@ -6366,7 +6366,7 @@ msgid "Learn more about this warning" msgstr "" #: src/components/verification/VerificationsDialog.tsx:153 -#: src/components/verification/VerifierDialog.tsx:121 +#: src/components/verification/VerifierDialog.tsx:122 msgctxt "english-only-resource" msgid "Learn more about verification on Bluesky" msgstr "" @@ -6376,7 +6376,7 @@ msgstr "" msgid "Learn more about what is public on Bluesky." msgstr "" -#: src/screens/Profile/components/GermButton.tsx:211 +#: src/screens/Profile/components/GermButton.tsx:212 msgid "Learn more about your Germ DM link" msgstr "" @@ -6674,7 +6674,7 @@ msgstr "" msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface." msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:244 +#: src/features/liveNow/components/LiveStatusDialog.tsx:245 msgid "Live feature is in beta" msgstr "" @@ -6904,18 +6904,18 @@ msgstr "Message {displayName}" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:100 +#: src/components/dms/MessageOverlays.tsx:111 msgctxt "toast" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageItem.tsx:553 +#: src/components/dms/MessageItem.tsx:522 msgid "Message failed to send." msgstr "Message failed to send." #. placeholder {0}: sender?.handle ?? 'unknown' #. placeholder {1}: message.text -#: src/components/dms/MessageContextMenu.tsx:146 +#: src/components/dms/MessageContextMenu.tsx:129 msgid "Message from @{0}: {1}" msgstr "" @@ -6938,7 +6938,7 @@ msgstr "" msgid "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" -#: src/components/dms/MessageContextMenu.tsx:145 +#: src/components/dms/MessageContextMenu.tsx:128 msgid "Message options" msgstr "" @@ -6946,11 +6946,11 @@ msgstr "" msgid "Messages" msgstr "" -#: src/components/dms/MessageItem.tsx:652 +#: src/components/dms/MessageItem.tsx:621 msgid "Messages from this person are hidden while they are blocking you." msgstr "Messages from this person are hidden while they are blocking you." -#: src/components/dms/MessageItem.tsx:647 +#: src/components/dms/MessageItem.tsx:616 msgid "Messages from this person are hidden while you are blocking them." msgstr "Messages from this person are hidden while you are blocking them." @@ -7229,8 +7229,8 @@ msgstr "" msgid "Navigates to your profile" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:340 -#: src/components/moderation/ReportDialog/index.tsx:356 +#: src/components/moderation/ReportDialog/index.tsx:342 +#: src/components/moderation/ReportDialog/index.tsx:358 msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?" msgstr "" @@ -7452,7 +7452,7 @@ msgstr "No GIFs found for \"{query}\"." msgid "No GIFs to show right now. Try again in a moment." msgstr "No GIFs to show right now. Try again in a moment." -#: src/features/liveNow/components/LinkPreview.tsx:63 +#: src/features/liveNow/components/LinkPreview.tsx:64 msgid "No image" msgstr "" @@ -7640,7 +7640,7 @@ msgstr "Not followed by anyone you’re following" msgid "Not Found" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:130 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:131 msgid "Not ready to hit post? Keep your best ideas in Drafts until the timing is just right." msgstr "" @@ -7724,7 +7724,7 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 #: src/components/dms/InitiateChatFlow.tsx:677 -#: src/components/dms/MessageItem.tsx:659 +#: src/components/dms/MessageItem.tsx:628 #: src/screens/Login/PasswordUpdatedForm.tsx:37 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:661 msgid "Okay" @@ -7916,8 +7916,8 @@ msgstr "" msgid "Open post options menu" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:218 -#: src/features/liveNow/components/LiveStatusDialog.tsx:228 +#: src/features/liveNow/components/LiveStatusDialog.tsx:219 +#: src/features/liveNow/components/LiveStatusDialog.tsx:229 msgid "Open profile" msgstr "" @@ -8005,7 +8005,7 @@ msgstr "" msgid "Opens flow to sign in to your existing Bluesky account" msgstr "" -#: src/components/images/Gallery/index.tsx:437 +#: src/components/images/Gallery/index.tsx:444 msgid "Opens full image" msgstr "Opens full image" @@ -8022,7 +8022,7 @@ msgstr "" msgid "Opens link {0}" msgstr "" -#: src/view/com/util/UserAvatar.tsx:599 +#: src/view/com/util/UserAvatar.tsx:600 msgid "Opens live status dialog" msgstr "" @@ -8053,7 +8053,7 @@ msgstr "" #: src/components/dms/MessageItem.tsx:229 #: src/view/com/notifications/NotificationFeedItem.tsx:1019 -#: src/view/com/util/UserAvatar.tsx:617 +#: src/view/com/util/UserAvatar.tsx:618 msgid "Opens this profile" msgstr "" @@ -8129,8 +8129,8 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:86 #: src/components/dms/AfterReportConversationDialog.tsx:213 -#: src/components/dms/AfterReportDialog.tsx:84 -#: src/components/dms/AfterReportDialog.tsx:176 +#: src/components/dms/AfterReportDialog.tsx:89 +#: src/components/dms/AfterReportDialog.tsx:181 msgid "Our moderation team has received your report." msgstr "" @@ -8661,7 +8661,7 @@ msgstr "" msgid "Press to view followers of this account that you also follow" msgstr "" -#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:120 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:121 msgid "Preview" msgstr "" @@ -8865,8 +8865,8 @@ msgstr "Re-enable link" msgid "React with {emoji}" msgstr "" -#: src/components/dms/ReactionsDialog.tsx:65 -#: src/components/dms/ReactionsDialog.tsx:90 +#: src/components/dms/ReactionsDialog.tsx:66 +#: src/components/dms/ReactionsDialog.tsx:94 msgid "Reactions" msgstr "Reactions" @@ -8884,8 +8884,8 @@ msgctxt "english-only-resource" msgid "read about how to use search filters" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:160 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:171 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:162 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:173 msgid "Read blog post" msgstr "" @@ -9020,13 +9020,13 @@ msgstr "" msgid "Remove attachment" msgstr "" -#: src/view/com/util/UserAvatar.tsx:519 -#: src/view/com/util/UserAvatar.tsx:522 +#: src/view/com/util/UserAvatar.tsx:520 +#: src/view/com/util/UserAvatar.tsx:523 msgid "Remove Avatar" msgstr "" -#: src/view/com/util/UserBanner.tsx:189 -#: src/view/com/util/UserBanner.tsx:192 +#: src/view/com/util/UserBanner.tsx:190 +#: src/view/com/util/UserBanner.tsx:193 msgid "Remove Banner" msgstr "" @@ -9261,8 +9261,8 @@ msgstr "" msgid "Reply was successfully hidden" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:193 -#: src/features/liveNow/components/LiveStatusDialog.tsx:266 +#: src/components/dms/MessageContextMenu.tsx:176 +#: src/features/liveNow/components/LiveStatusDialog.tsx:267 #: src/screens/Messages/ConversationSettings/index.tsx:517 msgid "Report" msgstr "" @@ -9279,8 +9279,8 @@ msgstr "" msgid "Report conversation" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:96 -#: src/components/moderation/ReportDialog/index.tsx:261 +#: src/components/moderation/ReportDialog/index.tsx:98 +#: src/components/moderation/ReportDialog/index.tsx:263 msgid "Report dialog" msgstr "" @@ -9294,7 +9294,7 @@ msgstr "" msgid "Report list" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:190 +#: src/components/dms/MessageContextMenu.tsx:173 msgid "Report message" msgstr "" @@ -9314,8 +9314,8 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:83 #: src/components/dms/AfterReportConversationDialog.tsx:210 -#: src/components/dms/AfterReportDialog.tsx:81 -#: src/components/dms/AfterReportDialog.tsx:173 +#: src/components/dms/AfterReportDialog.tsx:86 +#: src/components/dms/AfterReportDialog.tsx:178 msgid "Report submitted" msgstr "" @@ -9337,7 +9337,7 @@ msgid "Report this list" msgstr "" #: src/components/moderation/ReportDialog/copy.ts:19 -#: src/features/liveNow/components/LiveStatusDialog.tsx:249 +#: src/features/liveNow/components/LiveStatusDialog.tsx:250 msgid "Report this livestream" msgstr "" @@ -9530,7 +9530,7 @@ msgstr "" #: src/components/contacts/screens/VerifyNumber.tsx:355 #: src/components/Error.tsx:65 #: src/components/Lists.tsx:115 -#: src/components/moderation/ReportDialog/index.tsx:295 +#: src/components/moderation/ReportDialog/index.tsx:297 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:59 #: src/components/StarterPack/ProfileStarterPacks.tsx:377 @@ -9553,7 +9553,7 @@ msgstr "" msgid "Retry" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:292 +#: src/components/moderation/ReportDialog/index.tsx:294 #: src/view/screens/Storybook/Admonitions.tsx:61 msgid "Retry loading report options" msgstr "" @@ -9638,7 +9638,7 @@ msgid "Save draft?" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:99 -#: src/components/MediaPreview.tsx:196 +#: src/components/MediaPreview.tsx:197 #: src/components/Post/Embed/ImageContextMenu.tsx:70 #: src/components/StarterPack/ShareDialog.tsx:144 #: src/components/StarterPack/ShareDialog.tsx:150 @@ -9674,7 +9674,7 @@ msgstr "" msgid "Saved Feeds" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:144 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:145 #: src/Navigation.tsx:555 #: src/screens/Bookmarks/index.tsx:59 msgid "Saved Posts" @@ -9899,7 +9899,7 @@ msgstr "" msgid "Select a color" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:378 +#: src/components/moderation/ReportDialog/index.tsx:380 msgid "Select a reason" msgstr "" @@ -9990,7 +9990,7 @@ msgstr "" msgid "Select languages" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:428 +#: src/components/moderation/ReportDialog/index.tsx:430 msgid "Select moderation service" msgstr "" @@ -10092,7 +10092,7 @@ msgstr "" msgid "Send post to..." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:816 +#: src/components/moderation/ReportDialog/index.tsx:818 msgid "Send report to {title}" msgstr "" @@ -10114,7 +10114,7 @@ msgid "Send via direct message" msgstr "" #. placeholder {0}: i18n.date(new Date(message.sentAt), { timeStyle: 'short', }) -#: src/components/dms/MessageContextMenu.tsx:154 +#: src/components/dms/MessageContextMenu.tsx:137 msgid "Sent at {0}" msgstr "Sent at {0}" @@ -10219,7 +10219,7 @@ msgstr "" msgid "Sexually Suggestive" msgstr "" -#: src/components/MediaPreview.tsx:202 +#: src/components/MediaPreview.tsx:203 #: src/components/Post/Embed/ImageContextMenu.tsx:74 #: src/components/StarterPack/QrCodeDialog.tsx:195 #: src/screens/Hashtag.tsx:130 @@ -10309,16 +10309,16 @@ msgstr "" #: src/components/moderation/ScreenHider.tsx:179 #: src/components/moderation/ScreenHider.tsx:182 -#: src/features/liveNow/components/LiveStatusDialog.tsx:317 -#: src/features/liveNow/components/LiveStatusDialog.tsx:321 +#: src/features/liveNow/components/LiveStatusDialog.tsx:318 +#: src/features/liveNow/components/LiveStatusDialog.tsx:322 #: src/screens/List/ListHiddenScreen.tsx:194 #: src/screens/VideoFeed/index.tsx:646 #: src/screens/VideoFeed/index.tsx:652 msgid "Show anyway" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:181 -#: src/screens/Settings/AutomationLabelSettings.tsx:194 +#: src/screens/Settings/AutomationLabelSettings.tsx:185 +#: src/screens/Settings/AutomationLabelSettings.tsx:198 msgid "Show automation label" msgstr "Show automation label" @@ -10412,7 +10412,7 @@ msgid "Show warning and filter from feeds" msgstr "" #: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:60 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:170 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:171 msgid "Show when you’re live" msgstr "" @@ -10557,7 +10557,7 @@ msgstr "" msgid "Skip to next step" msgstr "" -#: src/components/images/Gallery/index.tsx:420 +#: src/components/images/Gallery/index.tsx:427 msgid "slide" msgstr "slide" @@ -10614,7 +10614,7 @@ msgid "Someone left the group" msgstr "Someone left the group" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:294 +#: src/components/dms/MessageItem.tsx:270 msgid "Someone reacted {0}" msgstr "" @@ -10639,7 +10639,7 @@ msgstr "Someone was removed" msgid "Someone was removed from the group" msgstr "Someone was removed from the group" -#: src/components/moderation/ReportDialog/index.tsx:101 +#: src/components/moderation/ReportDialog/index.tsx:103 msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" @@ -10650,7 +10650,7 @@ msgid "Something went wrong" msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:139 -#: src/components/moderation/ReportDialog/index.tsx:287 +#: src/components/moderation/ReportDialog/index.tsx:289 #: src/screens/Deactivated.tsx:86 #: src/screens/Settings/components/DeactivateAccountDialog.tsx:59 #: src/view/screens/Storybook/Admonitions.tsx:56 @@ -10670,7 +10670,7 @@ msgstr "" msgid "Something went wrong. Please try again in a moment." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:245 +#: src/components/moderation/ReportDialog/index.tsx:247 msgid "Something went wrong. Please try again." msgstr "" @@ -10812,7 +10812,7 @@ msgstr "" msgid "Storybook" msgstr "" -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:181 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:182 msgid "Streaming on Twitch? Set your live status on Bluesky to add a badge to your avatar. Tapping it takes people straight to your stream." msgstr "" @@ -10837,9 +10837,9 @@ msgstr "" msgid "Submit Appeal" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:506 -#: src/components/moderation/ReportDialog/index.tsx:567 -#: src/components/moderation/ReportDialog/index.tsx:574 +#: src/components/moderation/ReportDialog/index.tsx:508 +#: src/components/moderation/ReportDialog/index.tsx:569 +#: src/components/moderation/ReportDialog/index.tsx:576 msgid "Submit report" msgstr "" @@ -10848,13 +10848,13 @@ msgid "Subscribe" msgstr "" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:425 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:434 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:426 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:435 msgid "Subscribe on {0}" msgstr "Subscribe on {0}" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:433 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:434 msgid "Subscribe to {publicationTitle} on {0}" msgstr "Subscribe to {publicationTitle} on {0}" @@ -10985,7 +10985,7 @@ msgstr "" msgid "Tap below to allow Bluesky to access your GPS location. We will then use that data to more accurately determine the content and features available in your region." msgstr "" -#: src/components/dms/MessageItem.tsx:598 +#: src/components/dms/MessageItem.tsx:567 msgid "Tap for details" msgstr "Tap for details" @@ -11015,29 +11015,29 @@ msgstr "" msgid "Tap to dismiss" msgstr "" -#: src/components/dms/ReactionsDialog.tsx:192 +#: src/components/dms/ReactionsDialog.tsx:196 msgid "Tap to remove" msgstr "Tap to remove" #. placeholder {0}: reaction.value -#: src/components/dms/ReactionsDialog.tsx:208 +#: src/components/dms/ReactionsDialog.tsx:212 msgid "Tap to remove your {0} reaction" msgstr "Tap to remove your {0} reaction" -#: src/components/dms/MessageItem.tsx:563 +#: src/components/dms/MessageItem.tsx:532 msgid "Tap to retry" msgstr "Tap to retry" #. placeholder {0}: tab.value -#: src/components/dms/ReactionsDialog.tsx:354 +#: src/components/dms/ReactionsDialog.tsx:358 msgid "Tap to show {0} reactions" msgstr "Tap to show {0} reactions" -#: src/components/dms/ReactionsDialog.tsx:353 +#: src/components/dms/ReactionsDialog.tsx:357 msgid "Tap to show all reactions" msgstr "Tap to show all reactions" -#: src/components/dms/MessageItem.tsx:317 +#: src/components/dms/MessageItem.tsx:293 msgid "Tap to view reactions" msgstr "Tap to view reactions" @@ -11427,7 +11427,7 @@ msgstr "" msgid "This author has chosen to make their posts visible only to people who are signed in." msgstr "" -#: src/screens/Profile/components/GermButton.tsx:243 +#: src/screens/Profile/components/GermButton.tsx:244 msgid "This button lets others open the Germ DM app to send you a message. You can manage its visibility from the Germ DM app, or you can disconnect your Bluesky account from Germ DM altogether by clicking the button below." msgstr "" @@ -11561,7 +11561,7 @@ msgstr "" msgid "This is not a valid link" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:169 +#: src/screens/Settings/AutomationLabelSettings.tsx:173 msgid "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." msgstr "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." @@ -11594,13 +11594,13 @@ msgstr "" msgid "This list is empty." msgstr "" -#: src/components/dms/MessageItem.tsx:596 -#: src/components/dms/MessageItem.tsx:625 +#: src/components/dms/MessageItem.tsx:565 +#: src/components/dms/MessageItem.tsx:594 msgid "This message is hidden because this user is blocking you." msgstr "This message is hidden because this user is blocking you." -#: src/components/dms/MessageItem.tsx:595 -#: src/components/dms/MessageItem.tsx:621 +#: src/components/dms/MessageItem.tsx:564 +#: src/components/dms/MessageItem.tsx:590 msgid "This message is hidden because you are blocking this user." msgstr "This message is hidden because you are blocking this user." @@ -11835,8 +11835,8 @@ msgstr "" msgid "Topic" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:162 -#: src/components/dms/MessageContextMenu.tsx:165 +#: src/components/dms/MessageContextMenu.tsx:145 +#: src/components/dms/MessageContextMenu.tsx:148 #: src/components/Post/Translated/index.tsx:150 #: src/components/Post/Translated/index.tsx:157 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:553 @@ -11887,7 +11887,7 @@ msgstr "" msgid "Trolling" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:140 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:142 msgid "Trust emerges from relationships, communities, and shared context, so we’re also enabling <0>trusted verifiers: organizations that can directly issue verification." msgstr "" @@ -11980,7 +11980,7 @@ msgstr "" msgid "Unavailable feed information" msgstr "" -#: src/components/dms/MessageItem.tsx:663 +#: src/components/dms/MessageItem.tsx:632 #: src/components/dms/MessagesListBlockedFooter.tsx:97 #: src/components/dms/MessagesListBlockedFooter.tsx:104 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:222 @@ -12026,8 +12026,8 @@ msgstr "" #: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:79 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:40 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:46 -#: src/screens/Profile/components/GermButton.tsx:185 #: src/screens/Profile/components/GermButton.tsx:186 +#: src/screens/Profile/components/GermButton.tsx:187 msgid "Undo" msgstr "" @@ -12061,7 +12061,7 @@ msgstr "" msgid "Unfollows the user" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:490 +#: src/components/moderation/ReportDialog/index.tsx:492 msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" @@ -12287,22 +12287,22 @@ msgstr "" msgid "Upload a text file to:" msgstr "" -#: src/view/com/util/UserAvatar.tsx:490 -#: src/view/com/util/UserAvatar.tsx:493 -#: src/view/com/util/UserBanner.tsx:160 -#: src/view/com/util/UserBanner.tsx:163 +#: src/view/com/util/UserAvatar.tsx:491 +#: src/view/com/util/UserAvatar.tsx:494 +#: src/view/com/util/UserBanner.tsx:161 +#: src/view/com/util/UserBanner.tsx:164 msgid "Upload from Camera" msgstr "" -#: src/view/com/util/UserAvatar.tsx:507 -#: src/view/com/util/UserBanner.tsx:177 +#: src/view/com/util/UserAvatar.tsx:508 +#: src/view/com/util/UserBanner.tsx:178 msgid "Upload from Files" msgstr "" -#: src/view/com/util/UserAvatar.tsx:501 -#: src/view/com/util/UserAvatar.tsx:505 -#: src/view/com/util/UserBanner.tsx:171 -#: src/view/com/util/UserBanner.tsx:175 +#: src/view/com/util/UserAvatar.tsx:502 +#: src/view/com/util/UserAvatar.tsx:506 +#: src/view/com/util/UserBanner.tsx:172 +#: src/view/com/util/UserBanner.tsx:176 msgid "Upload from Library" msgstr "" @@ -12363,7 +12363,7 @@ msgid "user" msgstr "user" #: src/components/dms/AfterReportConversationDialog.tsx:187 -#: src/components/dms/AfterReportDialog.tsx:150 +#: src/components/dms/AfterReportDialog.tsx:155 msgctxt "toast" msgid "User blocked" msgstr "" @@ -12631,8 +12631,8 @@ msgid "View" msgstr "" #. placeholder {0}: view.source.title -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:319 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:588 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 msgid "View {0}" msgstr "View {0}" @@ -12661,7 +12661,7 @@ msgstr "View {0}’s profile" msgid "View {displayName}’s profile" msgstr "View {displayName}’s profile" -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:436 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:437 msgid "View {publicationTitle}" msgstr "View {publicationTitle}" @@ -12728,10 +12728,10 @@ msgstr "" msgid "View profile banner" msgstr "View profile banner" -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:319 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:426 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:437 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:588 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:427 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:438 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 msgid "View publication" msgstr "View publication" @@ -12785,8 +12785,8 @@ msgstr "" msgid "View your verifications" msgstr "" -#: src/components/images/AutoSizedImage.tsx:220 -#: src/components/images/AutoSizedImage.tsx:252 +#: src/components/images/AutoSizedImage.tsx:221 +#: src/components/images/AutoSizedImage.tsx:253 msgid "Views full image" msgstr "" @@ -12829,8 +12829,8 @@ msgstr "" msgid "Warn content and filter from feeds" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:189 -#: src/features/liveNow/components/LiveStatusDialog.tsx:198 +#: src/features/liveNow/components/LiveStatusDialog.tsx:190 +#: src/features/liveNow/components/LiveStatusDialog.tsx:199 msgid "Watch now" msgstr "" @@ -12977,7 +12977,7 @@ msgstr "" msgid "We’re having network issues, try again" msgstr "We’re having network issues, try again" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:96 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:97 msgid "We’re introducing a new layer of verification on Bluesky — an easy-to-see checkmark." msgstr "" @@ -13063,7 +13063,7 @@ msgstr "" msgid "What's up?" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:148 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:150 msgid "When you tap on a check, you’ll see which organizations have granted verification." msgstr "" @@ -13080,7 +13080,7 @@ msgstr "Who can join this group chat and how" msgid "Who can reply" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:129 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:131 msgid "Who can verify?" msgstr "" @@ -13132,7 +13132,7 @@ msgstr "" msgid "Why should this user be reviewed?" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:46 +#: src/components/dms/AfterReportDialog.tsx:51 msgid "Would you like to block this user and/or delete this conversation?" msgstr "" @@ -13224,7 +13224,7 @@ msgid "You are accessing Bluesky from a region that legally requires us to verif msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:636 +#: src/components/dms/MessageItem.tsx:605 msgid "You are blocking {0}" msgstr "You are blocking {0}" @@ -13316,7 +13316,7 @@ msgstr "" msgid "You can continue ongoing conversations regardless of which setting you choose." msgstr "" -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:149 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:150 msgid "You can now choose to be notified when specific people post. If there’s someone you want timely updates from, go to their profile and find the new bell icon near the follow button." msgstr "" @@ -13552,7 +13552,7 @@ msgid "You probably want to restart the app now." msgstr "" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:287 +#: src/components/dms/MessageItem.tsx:263 msgid "You reacted {0}" msgstr "" @@ -13841,7 +13841,7 @@ msgstr "" msgid "Your preferred language" msgstr "" -#: src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx:100 +#: src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx:102 #: src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx:53 msgid "Your profile picture" msgstr "" @@ -13859,7 +13859,7 @@ msgid "Your reply was sent" msgstr "" #. placeholder {0}: state.selectedLabeler?.creator.displayName -#: src/components/moderation/ReportDialog/index.tsx:517 +#: src/components/moderation/ReportDialog/index.tsx:519 msgid "Your report will be sent to <0>{0}." msgstr "" -- 2.51.2 From a19d652a39b4e483a30a409c37a7d74b6552f7c6 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:50:56 +0100 Subject: [PATCH 17/61] Fix invalid `zero` plural category and add plural formatting (#10722) --- src/screens/Messages/JoinRequests.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/screens/Messages/JoinRequests.tsx b/src/screens/Messages/JoinRequests.tsx index 80d31299d..98952aee3 100644 --- a/src/screens/Messages/JoinRequests.tsx +++ b/src/screens/Messages/JoinRequests.tsx @@ -411,7 +411,6 @@ function Header({ count?: number hasMoreRequests?: boolean }) { - const {t: l} = useLingui() return ( @@ -420,15 +419,15 @@ function Header({ {count === undefined ? ( Requests to join ) : hasMoreRequests ? ( - l({ - message: `${count}+ requests to join`, - comment: - 'Displayed when there are more requests to join a group chat than have been loaded', - }) + ) : ( -- 2.51.2 From 9f200aab05e8f71f8e4fb74f59a9b400e89f8964 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:51:51 +0100 Subject: [PATCH 18/61] Use correct `Plural` macro in InviteLinkDialog (#10723) --- src/screens/Messages/components/InviteLinkDialog.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/screens/Messages/components/InviteLinkDialog.tsx b/src/screens/Messages/components/InviteLinkDialog.tsx index ad6d4a08e..6e898a0a9 100644 --- a/src/screens/Messages/components/InviteLinkDialog.tsx +++ b/src/screens/Messages/components/InviteLinkDialog.tsx @@ -5,8 +5,7 @@ import { moderateProfile, type ModerationOpts, } from '@atproto/api' -import {plural} from '@lingui/core/macro' -import {Trans, useLingui} from '@lingui/react/macro' +import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -178,11 +177,7 @@ export function InviteLinkDialog({ Group chats can only have a maximum of{' '} - {plural(convo.details.memberLimit, { - one: '# person', - other: '# people', - })} - . + . -- 2.51.2 From 49f8e5031f42762805b1ab392c68e7bf75812e9d Mon Sep 17 00:00:00 2001 From: heikadog <41552394+heikadog@users.noreply.github.com> Date: Thu, 4 Jun 2026 06:28:40 -0400 Subject: [PATCH 19/61] Fix labeller highlight when selected on bottom bar (#10716) --- src/view/shell/bottom-bar/BottomBar.tsx | 5 +++-- src/view/shell/bottom-bar/BottomBarStyles.tsx | 9 +++++++++ src/view/shell/bottom-bar/BottomBarWeb.tsx | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index e6a1eeaad..605542749 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -144,6 +144,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { const [demoMode] = useDemoMode() const {isActive: live} = useActorStatus(profile) + const isLabeler = profile?.associated?.labeler return ( <> @@ -276,9 +277,9 @@ export function BottomBar({navigation}: BottomTabBarProps) { { closeAllActiveElements() @@ -186,9 +187,13 @@ export function BottomBarWeb() { -- 2.51.2 From c79b5bfa08d6ddeb909dab0c0b392e5123d9c5bb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 4 Jun 2026 09:29:36 -0500 Subject: [PATCH 20/61] Disable overscroll on Android to fix settle discrepancy (#10717) --- src/components/images/Gallery/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 9ae677c2d..fd18cb8a0 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -40,7 +40,7 @@ import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' -import {IS_WEB} from '#/env' +import {IS_ANDROID, IS_WEB} from '#/env' export * from './const' export * from './maybeApplyGalleryOffsetStyles' @@ -264,6 +264,9 @@ export function Gallery({ aria-label={l`Image gallery, ${images.length} images`} horizontal pagingEnabled={false} + // Disable Android's stretch overscroll, which can leave the carousel + // settled just off the left edge instead of aligned to x = 0 + overScrollMode={IS_ANDROID ? 'never' : 'auto'} showsHorizontalScrollIndicator={false} directionalLockEnabled nestedScrollEnabled -- 2.51.2 From 08cd4e58b023e1ffe2b3681f4f8c690a24af84a0 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:11:50 -0700 Subject: [PATCH 21/61] Add group chat join links to supported embed types (#10454) --- __tests__/lib/strings/url-helpers.test.ts | 45 +++ eslint-suppressions.json | 8 - src/components/Post/Embed/ChatInviteEmbed.tsx | 41 +++ .../Post/Embed/JoinRequestEmbed.tsx | 271 ++++++++++++++++++ src/components/Post/Embed/index.tsx | 17 ++ .../intents/GroupChatJoinDialog.tsx | 10 +- src/lib/api/index.ts | 18 +- src/lib/api/resolve.ts | 25 +- src/lib/strings/url-helpers.ts | 5 +- src/state/queries/join-links.ts | 6 +- src/view/com/composer/ExternalEmbed.tsx | 3 + .../com/composer/ExternalEmbedRemoveBtn.tsx | 7 +- 12 files changed, 434 insertions(+), 22 deletions(-) create mode 100644 src/components/Post/Embed/ChatInviteEmbed.tsx create mode 100644 src/components/Post/Embed/JoinRequestEmbed.tsx diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts index 0b1b75028..23ffaa287 100644 --- a/__tests__/lib/strings/url-helpers.test.ts +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -1,6 +1,7 @@ import {describe, expect, it} from '@jest/globals' import { + getChatInviteCodeFromUrl, isPossiblyAUrl, isTrustedUrl, linkRequiresWarning, @@ -178,3 +179,47 @@ describe('isTrustedUrl', () => { expect(output).toEqual(expected) }) }) + +describe('getChatInviteCodeFromUrl', () => { + type Case = [string, string | undefined] + + const cases: Case[] = [ + ['https://bsky.app/c/abcdefg', 'abcdefg'], + ['https://bsky.app/c/abcdefghij', 'abcdefghij'], + // http is not recognized as a bsky.app url + ['http://bsky.app/c/abcdefg', undefined], + ['https://bsky.app/c/abcdefg?utm=foo', 'abcdefg'], + ['https://bsky.app/c/abcdefg#section', 'abcdefg'], + ['/c/abcdefg', 'abcdefg'], + ['/c/abcdefg?utm=foo', 'abcdefg'], + ['/c/abcdefg#section', 'abcdefg'], + + // too short + ['https://bsky.app/c/abcdef', undefined], + ['/c/abcdef', undefined], + // too long + ['https://bsky.app/c/abcdefghijk', undefined], + ['/c/abcdefghijk', undefined], + // invalid characters + ['https://bsky.app/c/abc-def', undefined], + ['/c/abc def', undefined], + // trailing path + ['https://bsky.app/c/abcdefg/extra', undefined], + ['/c/abcdefg/extra', undefined], + // wrong path + ['https://bsky.app/profile/abcdefg', undefined], + ['https://bsky.app/c', undefined], + // wrong host + ['https://example.com/c/abcdefg', undefined], + // not a url, not a path + ['c/abcdefg', undefined], + ['abcdefg', undefined], + ['', undefined], + // malformed url + ['https://[invalid/c/abcdefg', undefined], + ] + + it.each(cases)('given input %p, returns %p', (input, expected) => { + expect(getChatInviteCodeFromUrl(input)).toEqual(expected) + }) +}) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f2e677dc7..d8555ace5 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -811,14 +811,6 @@ "count": 1 } }, - "src/lib/api/index.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 - }, - "@typescript-eslint/no-unsafe-member-access": { - "count": 3 - } - }, "src/lib/async/retry.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 diff --git a/src/components/Post/Embed/ChatInviteEmbed.tsx b/src/components/Post/Embed/ChatInviteEmbed.tsx new file mode 100644 index 000000000..744fcfb7f --- /dev/null +++ b/src/components/Post/Embed/ChatInviteEmbed.tsx @@ -0,0 +1,41 @@ +import {type StyleProp, type ViewStyle} from 'react-native' +import {type AppBskyEmbedExternal} from '@atproto/api' + +import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links' +import {useSession} from '#/state/session' +import {atoms as a} from '#/alf' +import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' +import {JoinRequestEmbed} from '#/components/Post/Embed/JoinRequestEmbed' + +export function ChatInviteEmbed({ + code, + link, + onOpen, + style, +}: { + code: string + link: AppBskyEmbedExternal.ViewExternal + onOpen?: () => void + style?: StyleProp +}) { + const {hasSession} = useSession() + const {data, error, isPending} = useJoinLinkPreviewsQuery({ + codes: [code], + hasSession, + }) + + const preview = data?.joinLinkPreviews[0] + + if (error) { + return + } + + return ( + + ) +} diff --git a/src/components/Post/Embed/JoinRequestEmbed.tsx b/src/components/Post/Embed/JoinRequestEmbed.tsx new file mode 100644 index 000000000..77a0223be --- /dev/null +++ b/src/components/Post/Embed/JoinRequestEmbed.tsx @@ -0,0 +1,271 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' +import {type ChatBskyGroupDefs} from '@atproto/api' +import {Plural, Trans, useLingui} from '@lingui/react/macro' +import {useNavigation} from '@react-navigation/native' + +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import {type NavigationProp} from '#/lib/routes/types' +import {sanitizeHandle} from '#/lib/strings/handles' +import {atoms as a, useTheme} from '#/alf' +import {AvatarBubbles} from '#/components/AvatarBubbles' +import { + Button, + type ButtonColor, + ButtonIcon, + ButtonText, +} from '#/components/Button' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' +import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' +import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {useIntentDialogs} from '#/components/intents/IntentDialogs' +import {Loader} from '#/components/Loader' +import {ProfileBadges} from '#/components/ProfileBadges' +import {Text} from '#/components/Typography' + +const JOIN_REQUEST_EMBED_HEIGHT = 152 + +export function JoinRequestEmbed({ + loading = false, + preview, + style, + onOpen, +}: { + loading?: boolean + preview?: ChatBskyGroupDefs.JoinLinkPreviewView + style?: StyleProp + onOpen?: () => void +}) { + const t = useTheme() + + if (loading) { + return ( + + + + ) + } + + if (!preview) { + return ( + + + + Chat invite link no longer available + + + ) + } + + return ( + + ) +} + +function JoinRequestEmbedInner({ + preview, + style, + onOpen, +}: { + preview: ChatBskyGroupDefs.JoinLinkPreviewView + style?: StyleProp + onOpen?: () => void +}) { + const t = useTheme() + const {t: l} = useLingui() + const navigation = useNavigation() + + const {groupChatJoinDialogControl, setGroupChatJoinState} = useIntentDialogs() + + const ownerDisplayName = createSanitizedDisplayName(preview.owner) + const ownerHandle = sanitizeHandle(preview.owner.handle, '@') + + const avatarProfiles = preview.convo?.members ?? [preview.owner] + + const convoId = preview.convo?.id + const isFollowing = preview.owner.viewer?.following ?? false + const hasRequested = !convoId && preview.viewer?.requestedAt != null + + let canJoin = true + let ButtonIconImage = JoinIcon + let buttonText = preview.requireApproval ? l`Request to join` : l`Join` + let buttonColor: ButtonColor = 'primary' + if (preview.enabledStatus !== 'enabled') { + canJoin = false + ButtonIconImage = WarningIcon + buttonText = l`Chat invite link no longer available` + buttonColor = 'secondary' + } else if (preview.memberCount >= preview.memberLimit) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`This chat is full` + buttonColor = 'secondary' + } else if (preview.joinRule === 'followedByOwner' && !isFollowing) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`Only people the chat owner follows can join` + buttonColor = 'secondary' + } else if (hasRequested) { + ButtonIconImage = CheckIcon + buttonText = l`Requested` + buttonColor = 'secondary' + } + + return ( + + + + + + {preview.name} + + + + Group chat + + + + {preview.memberCount}/{preview.memberLimit}{' '} + + + + + + + + By{' '} + + {ownerDisplayName} + + + + + + {ownerHandle} + + + + + {convoId ? ( + + ) : ( + + )} + + ) +} diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index 054558ad8..ab6f5dc43 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -12,6 +12,7 @@ import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' +import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {unstableCacheProfileView} from '#/state/queries/profile' import {useSession} from '#/state/session' @@ -33,6 +34,7 @@ import { type EmbedType, parseEmbed, } from '#/types/bsky/post' +import {ChatInviteEmbed} from './ChatInviteEmbed' import {ExternalEmbed} from './ExternalEmbed' import {ModeratedFeedEmbed} from './FeedEmbed' import {ImageEmbed} from './ImageEmbed' @@ -110,6 +112,21 @@ function MediaEmbed({ ) } + const chatInviteCode = getChatInviteCodeFromUrl(embed.view.external.uri) + if (chatInviteCode) { + return ( + + + + ) + } return ( - Open chat + + Open chat + ) : ( @@ -416,8 +420,8 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { } accessibilityHint={ joinLinkPreview.requireApproval - ? l`Request access to join this group chat` - : l`Join this group chat` + ? l`Tap to request access to join this group chat` + : l`Tap to join this group chat immediately` } size="large" color={buttonColor} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index d9a026ca8..c5f6cc6eb 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -177,7 +177,8 @@ export async function post( writes: writes, validate: true, }) - } catch (e: any) { + } catch (err) { + const e = err as Error logger.error(`Failed to create post`, { safeMessage: e.message, }) @@ -427,6 +428,16 @@ async function resolveMedia( }, } } + if (resolvedLink.type === 'chat-invite' && resolvedLink.view) { + return { + $type: 'app.bsky.embed.external', + external: { + uri: resolvedLink.uri, + title: resolvedLink.view.name, + description: `${resolvedLink.view.memberCount}/${resolvedLink.view.memberLimit}`, + }, + } + } } return undefined } @@ -470,6 +481,7 @@ async function computeCid(record: AppBskyFeedPost.Record): Promise { } // Returns a transformed version of the object for use in DAG-CBOR. +// eslint-disable-next-line @typescript-eslint/no-explicit-any function prepareForHashing(v: any): any { // IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing, // the API client will convert this for you but we're hashing in the client, @@ -492,9 +504,10 @@ function prepareForHashing(v: any): any { // Walk through plain objects if (isPlainObject(v)) { - const obj: any = {} + const obj: Record = {} let pure = true for (const key in v) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access let value = v[key] // `value` is undefined if (value === undefined) { @@ -513,6 +526,7 @@ function prepareForHashing(v: any): any { return v } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function isPlainObject(v: any): boolean { if (typeof v !== 'object' || v === null) { return false diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index f75cd74db..d411bd820 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -2,11 +2,12 @@ import { type AppBskyFeedDefs, type AppBskyGraphDefs, type BskyAgent, + type ChatBskyGroupDefs, type ComAtprotoRepoStrongRef, } from '@atproto/api' import {AtUri} from '@atproto/api' -import {POST_IMG_MAX} from '#/lib/constants' +import {DM_SERVICE_HEADERS, POST_IMG_MAX} from '#/lib/constants' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' import {downloadAndResize} from '#/lib/media/manip' @@ -16,6 +17,7 @@ import { } from '#/lib/strings/starter-pack' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, isBskyCustomFeedUrl, isBskyListUrl, isBskyPostUrl, @@ -71,12 +73,20 @@ type ResolvedStarterPackRecord = { view: AppBskyGraphDefs.StarterPackView } +type ResolvedChatInvite = { + type: 'chat-invite' + uri: string + code: string + view?: ChatBskyGroupDefs.JoinLinkPreviewView +} + export type ResolvedLink = | ResolvedExternalLink | ResolvedPostRecord | ResolvedFeedRecord | ResolvedListRecord | ResolvedStarterPackRecord + | ResolvedChatInvite export class EmbeddingDisabledError extends Error { constructor() { @@ -141,6 +151,19 @@ export async function resolveLink( view: res.data.list, } } + const chatInviteCode = getChatInviteCodeFromUrl(uri) + if (chatInviteCode) { + const res = await agent.chat.bsky.group.getJoinLinkPreviews( + {codes: [chatInviteCode]}, + {headers: DM_SERVICE_HEADERS}, + ) + return { + type: 'chat-invite', + uri, + code: chatInviteCode, + view: res.data.joinLinkPreviews[0], + } + } if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { const parsed = parseStarterPackUri(uri) if (!parsed) { diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index b5c278dac..fd1a5ef7d 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,5 +1,5 @@ import {AtUri} from '@atproto/api' -import psl from 'psl' +import {parse} from 'psl' import TLDs from 'tlds' import {BSKY_SERVICE} from '#/lib/constants' @@ -178,6 +178,7 @@ export function isBskyStarterPackUrl(url: string): boolean { return false } +// Invite codes are 7 alphanumeric characters long, supporting up to 10 here to future-proof. export const CHAT_INVITE_CODE_REGEX = /^\/c\/([a-zA-Z0-9]{7,10})$/ export function getChatInviteCodeFromUrl(url: string): string | undefined { @@ -328,7 +329,7 @@ export function isPossiblyAUrl(str: string): boolean { } export function splitApexDomain(hostname: string): [string, string] { - const hostnamep = psl.parse(hostname) + const hostnamep = parse(hostname) if (hostnamep.error || !hostnamep.listed || !hostnamep.domain) { return ['', hostname] } diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index 6aa10331e..99a37b6b0 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -20,9 +20,11 @@ export const createJoinLinkPreviewQueryKey = (args: { export function useJoinLinkPreviewsQuery({ codes, hasSession, + staleTime = STALE.MINUTES.ONE, }: { codes?: string[] hasSession: boolean + staleTime?: number }) { const agent = useAgent() @@ -45,7 +47,7 @@ export function useJoinLinkPreviewsQuery({ } }, enabled: codes != null && codes.length > 0, - staleTime: STALE.SECONDS.FIFTEEN, + staleTime, }) } @@ -66,7 +68,7 @@ export function usePrefetchJoinLinkPreviews() { : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) return res.data }, - staleTime: STALE.SECONDS.FIFTEEN, + staleTime: STALE.MINUTES.ONE, }) } } diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index ecd22d7c3..5cc87c63f 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -11,6 +11,7 @@ import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed' +import {JoinRequestEmbed} from '#/components/Post/Embed/JoinRequestEmbed' import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed' import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' @@ -115,6 +116,8 @@ export const ExternalEmbedLink = ({ hideAlt /> ) + } else if (data.type === 'chat-invite') { + return } else if (data.kind === 'feed') { return ( void} & ViewStyleProp) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() return ( - ) : ( - - )} + + ) } diff --git a/src/components/dms/ChatInvite/Card.tsx b/src/components/dms/ChatInvite/Card.tsx new file mode 100644 index 000000000..518371325 --- /dev/null +++ b/src/components/dms/ChatInvite/Card.tsx @@ -0,0 +1,90 @@ +import {View} from 'react-native' +import {Plural, Trans} from '@lingui/react/macro' + +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import {sanitizeHandle} from '#/lib/strings/handles' +import {atoms as a, useTheme} from '#/alf' +import {AvatarBubbles} from '#/components/AvatarBubbles' +import {ProfileBadges} from '#/components/ProfileBadges' +import {Text} from '#/components/Typography' +import {useChatInvite} from './Context' + +/** + * Presentational preview of a chat invite: member avatars, group name, member + * count, and owner. Reads the preview from `ChatInvite.Root` context. Renders + * nothing if there's no preview (use a fallback alongside it for that case). + */ +export function Card({size}: {size: 'large' | 'small'}) { + const t = useTheme() + const {preview} = useChatInvite() + + if (!preview) return null + + const ownerDisplayName = createSanitizedDisplayName(preview.owner) + const ownerHandle = sanitizeHandle(preview.owner.handle, '@') + const avatarProfiles = preview.convo?.members ?? [preview.owner] + + return ( + + + + + {preview.name} + + + + Group chat + + + + {preview.memberCount}/{preview.memberLimit}{' '} + + + + + + + + By {ownerDisplayName} + + + + + {ownerHandle} + + + + + ) +} diff --git a/src/components/dms/ChatInvite/Context.tsx b/src/components/dms/ChatInvite/Context.tsx new file mode 100644 index 000000000..53c7a6cc2 --- /dev/null +++ b/src/components/dms/ChatInvite/Context.tsx @@ -0,0 +1,47 @@ +import {createContext, useContext} from 'react' +import {type ChatBskyGroupDefs} from '@atproto/api' + +import {type ButtonColor} from '#/components/Button' +import {type Props as SVGIconProps} from '#/components/icons/common' + +/** + * The derived state of the join/open action for a chat invite, computed once in + * `Root` and consumed by `JoinButton` (or any custom action UI). + */ +export type ChatInviteAction = { + label: string + accessibilityHint: string + icon: React.ComponentType + color: ButtonColor + /** + * Whether the action can be performed. False when the link is disabled, the + * chat is full, or the viewer doesn't meet the join rule. + */ + disabled: boolean + onPress: () => void + side: 'left' | 'right' +} + +export type ChatInviteContextValue = { + code: string + loading: boolean + error: boolean + preview: ChatBskyGroupDefs.JoinLinkPreviewView | undefined + /** + * The derived action descriptor. Undefined while loading or when there's no + * preview to act on. + */ + action: ChatInviteAction | undefined +} + +const ChatInviteContext = createContext(null) + +export function useChatInvite(): ChatInviteContextValue { + const ctx = useContext(ChatInviteContext) + if (!ctx) { + throw new Error('useChatInvite must be used within a ChatInvite.Root') + } + return ctx +} + +export const ChatInviteProvider = ChatInviteContext.Provider diff --git a/src/components/dms/ChatInvite/JoinButton.tsx b/src/components/dms/ChatInvite/JoinButton.tsx new file mode 100644 index 000000000..0036391b0 --- /dev/null +++ b/src/components/dms/ChatInvite/JoinButton.tsx @@ -0,0 +1,42 @@ +import {type StyleProp, type ViewStyle} from 'react-native' + +import {atoms as a} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useChatInvite} from './Context' + +/** + * The join/open action button for a chat invite. Reads the derived action from + * `ChatInvite.Root` context. Pass `onPress` to intercept (e.g. to close a + * surface before navigating); it runs before the default action. Renders + * nothing while loading or when there's no preview to act on. + */ +export function JoinButton({ + onPress, + style, +}: { + onPress?: () => void + style?: StyleProp +}) { + const {action} = useChatInvite() + + if (!action) return null + + return ( + + ) +} diff --git a/src/components/dms/ChatInvite/Root.tsx b/src/components/dms/ChatInvite/Root.tsx new file mode 100644 index 000000000..a9f2c54e3 --- /dev/null +++ b/src/components/dms/ChatInvite/Root.tsx @@ -0,0 +1,144 @@ +import {setStringAsync} from 'expo-clipboard' +import {type ChatBskyGroupDefs} from '@atproto/api' +import {useLingui} from '@lingui/react/macro' +import {useNavigation} from '@react-navigation/native' + +import {type NavigationProp} from '#/lib/routes/types' +import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links' +import {useSession} from '#/state/session' +import {type ButtonColor} from '#/components/Button' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' +import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' +import {ChainLink_Stroke2_Corner0_Rounded as LinkIcon} from '#/components/icons/ChainLink' +import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' +import {type Props as SVGIconProps} from '#/components/icons/common' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {useIntentDialogs} from '#/components/intents/IntentDialogs' +import * as Toast from '#/components/Toast' +import {type ChatInviteAction, ChatInviteProvider} from './Context' + +/** + * Headless data + state owner for a chat invite. Fetches the join link preview + * by code and derives the join/open action, exposing both via context for the + * composable parts (`Card`, `JoinButton`) or any custom UI to consume. + * + * Pass `initialPreview` when the preview is already known (e.g. a DM message + * embed already carries the resolved view) to avoid a loading flash. + */ +export function Root({ + code, + initialPreview, + currentConvoId, + children, +}: { + code: string + initialPreview?: ChatBskyGroupDefs.JoinLinkPreviewView + /** + * The convo this invite is being viewed within, if any. When the invite + * links to the same chat, the action becomes "Copy link" instead of + * open/join (you're already here). + */ + currentConvoId?: string + children: React.ReactNode +}) { + const {hasSession} = useSession() + const {t: l} = useLingui() + const navigation = useNavigation() + const {groupChatJoinDialogControl, setGroupChatJoinState} = useIntentDialogs() + + const {data, error, isPending} = useJoinLinkPreviewsQuery({ + codes: [code], + hasSession, + // Seed the cache with the already-resolved preview so we don't refetch. + initialData: initialPreview + ? {joinLinkPreviews: [initialPreview]} + : undefined, + }) + + const preview = data?.joinLinkPreviews[0] + const loading = isPending && !preview + + let action: ChatInviteAction | undefined + if (preview) { + const convoId = preview.convo?.id + const isFollowing = preview.owner.viewer?.following ?? false + const hasRequested = !convoId && preview.viewer?.requestedAt != null + + if (convoId && convoId === currentConvoId) { + // You're already in the chat this invite links to - offer to copy the + // link rather than open/join. + action = { + label: l`Copy link`, + accessibilityHint: l`Tap to copy this invite link`, + icon: LinkIcon, + side: 'left', + color: 'primary', + disabled: false, + onPress: () => { + void setStringAsync(`https://bsky.app/c/${preview.code}`) + Toast.show(l`Copied to clipboard`, {type: 'success'}) + }, + } + } else if (convoId) { + action = { + label: l`Open chat`, + accessibilityHint: l`Tap to open this group chat`, + icon: ArrowRightIcon, + side: 'right', + color: 'primary', + disabled: false, + onPress: () => { + navigation.push('MessagesConversation', {conversation: convoId}) + }, + } + } else { + let canJoin = true + let icon: React.ComponentType = JoinIcon + let label = preview.requireApproval ? l`Request to join` : l`Join` + let color: ButtonColor = 'primary' + if (preview.enabledStatus !== 'enabled') { + canJoin = false + icon = WarningIcon + label = l`Chat invite link no longer available` + color = 'secondary' + } else if (preview.memberCount >= preview.memberLimit) { + canJoin = false + icon = HandIcon + label = l`This chat is full` + color = 'secondary' + } else if (preview.joinRule === 'followedByOwner' && !isFollowing) { + canJoin = false + icon = HandIcon + label = l`Only people the chat owner follows can join` + color = 'secondary' + } else if (hasRequested) { + icon = CheckIcon + label = l`Requested` + color = 'secondary' + } + + action = { + label, + side: 'left', + accessibilityHint: preview.requireApproval + ? l`Tap to request access to join this group chat` + : l`Tap to join this group chat immediately`, + icon, + color, + disabled: !canJoin, + onPress: () => { + setGroupChatJoinState({code: preview.code}) + groupChatJoinDialogControl.open() + }, + } + } + } + + return ( + + {children} + + ) +} diff --git a/src/components/dms/ChatInvite/index.tsx b/src/components/dms/ChatInvite/index.tsx new file mode 100644 index 000000000..bcea3f765 --- /dev/null +++ b/src/components/dms/ChatInvite/index.tsx @@ -0,0 +1,8 @@ +export {Card} from './Card' +export { + type ChatInviteAction, + type ChatInviteContextValue, + useChatInvite, +} from './Context' +export {JoinButton} from './JoinButton' +export {Root} from './Root' diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index ac6acb589..f4f3cdacc 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -20,6 +20,7 @@ import { AppBskyEmbedRecord, type ChatBskyActorDefs, ChatBskyConvoDefs, + ChatBskyEmbedJoinLink, RichText as RichTextAPI, } from '@atproto/api' import {plural} from '@lingui/core/macro' @@ -49,6 +50,7 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' +import {MessageItemInviteEmbed} from './MessageItemInviteEmbed' import {groupReactions} from './ReactionsDialog' import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util' @@ -185,8 +187,10 @@ let MessageItem = ({ const rt = new RichTextAPI({text: message.text, facets: message.facets}) - const hasEmbedAndText = - AppBskyEmbedRecord.isView(message.embed) && rt.text.length > 0 + const hasEmbed = + AppBskyEmbedRecord.isView(message.embed) || + ChatBskyEmbedJoinLink.isView(message.embed) + const hasEmbedAndText = hasEmbed && rt.text.length > 0 const targetBottomRadius = squaredBottomCorner ? SQUARED_BORDER_RADIUS @@ -427,6 +431,15 @@ let MessageItem = ({ squaredTopCorner={squaredTopCorner} /> )} + {ChatBskyEmbedJoinLink.isView(message.embed) && ( + + )} {rt.text.length > 0 && ( + isFromSelf: boolean + isGroupChat: boolean + squaredTopCorner: boolean + squaredBottomCorner: boolean +}): React.ReactNode => { + const t = useTheme() + const screen = useWindowDimensions() + const convo = useConvoActive() + + return ( + + + + + + + + + + + ) +} +MessageItemInviteEmbed = memo(MessageItemInviteEmbed) +export {MessageItemInviteEmbed} diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index 0aa132e23..c6899fb9d 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -20,7 +20,7 @@ import {countGraphemes} from 'unicode-segmenter/grapheme' import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {isBskyPostUrl} from '#/lib/strings/url-helpers' +import {isBskyChatInviteUrl, isBskyPostUrl} from '#/lib/strings/url-helpers' import {useEmail} from '#/state/email-verification' import { useMessageDraft, @@ -233,7 +233,11 @@ export function MessageComposer({ }} onChange={handleChange} onFacetCommitted={facet => { - if (facet.type === 'url' && isBskyPostUrl(facet.value)) { + if ( + facet.type === 'url' && + (isBskyPostUrl(facet.value) || + isBskyChatInviteUrl(facet.value)) + ) { setEmbed(facet.value) } }} diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index 5bc2f38a0..a73a6564f 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -18,6 +18,8 @@ import { } from '#/lib/routes/types' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, + isBskyChatInviteUrl, isBskyPostUrl, makeRecordUri, } from '#/lib/strings/url-helpers' @@ -26,6 +28,7 @@ import {usePostQuery} from '#/state/queries/post' import {PostMeta} from '#/view/com/util/PostMeta' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' +import * as ChatInvite from '#/components/dms/ChatInvite' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Loader} from '#/components/Loader' import * as MediaPreview from '#/components/MediaPreview' @@ -35,35 +38,56 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' +/** + * The embed staged in the message composer. A message can carry at most one + * embed: either a quoted post or a group chat invite link. + */ +export type MessageEmbedState = + | {type: 'post'; uri: string} + | {type: 'invite'; code: string} + export function useMessageEmbed() { const route = useRoute>() const navigation = useNavigation() const embedFromParams = route.params.embed - const [embedUri, setEmbedUri] = useState(embedFromParams) + const [embed, setEmbedState] = useState( + embedFromParams ? {type: 'post', uri: embedFromParams} : undefined, + ) - if (embedFromParams && embedUri !== embedFromParams) { - setEmbedUri(embedFromParams) + if (embedFromParams && embed?.type !== 'post') { + setEmbedState({type: 'post', uri: embedFromParams}) } return { - embedUri, + embed, setEmbed: useCallback( (embedUrl: string | undefined) => { if (!embedUrl) { + // Only the post embed is reflected in the route param (used by the + // share-to-DM intent flow); invites are local-only. navigation.setParams({embed: ''}) - setEmbedUri(undefined) + setEmbedState(undefined) return } if (embedFromParams) return - const url = convertBskyAppUrlIfNeeded(embedUrl) - const [_0, user, _1, rkey] = url.split('/').filter(Boolean) - const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey) + if (isBskyChatInviteUrl(embedUrl)) { + const code = getChatInviteCodeFromUrl(embedUrl) + if (code) { + setEmbedState({type: 'invite', code}) + } + return + } - setEmbedUri(uri) + if (isBskyPostUrl(embedUrl)) { + const url = convertBskyAppUrlIfNeeded(embedUrl) + const [_0, user, _1, rkey] = url.split('/').filter(Boolean) + const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey) + setEmbedState({type: 'post', uri}) + } }, [embedFromParams, navigation], ), @@ -81,7 +105,10 @@ export function useExtractEmbedFromFacets( for (const facet of rt.facets ?? []) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) { + if ( + AppBskyRichtextFacet.isLink(feature) && + (isBskyPostUrl(feature.uri) || isBskyChatInviteUrl(feature.uri)) + ) { uriFromFacet = feature.uri break } @@ -96,16 +123,40 @@ export function useExtractEmbedFromFacets( } export function MessageInputEmbed({ - embedUri, + embed, setEmbed, }: { - embedUri: string | undefined + embed: MessageEmbedState | undefined setEmbed: (embedUrl: string | undefined) => void +}) { + const onRemove = useCallback(() => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + setEmbed(undefined) + }, [setEmbed]) + + if (!embed) { + return null + } + + switch (embed.type) { + case 'post': + return + case 'invite': + return + } +} + +function MessageInputPostEmbed({ + uri, + onRemove, +}: { + uri: string + onRemove: () => void }) { const t = useTheme() const {t: l} = useLingui() - const {data: post, status} = usePostQuery(embedUri) + const {data: post, status} = usePostQuery(uri) const moderationOpts = useModerationOpts() const moderation = useMemo( @@ -134,15 +185,6 @@ export function MessageInputEmbed({ return {rt: undefined, record: undefined} }, [post]) - if (!embedUri) { - return null - } - - const onRemove = () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setEmbed(undefined) - } - switch (status) { case 'pending': { return ( @@ -220,6 +262,71 @@ export function MessageInputEmbed({ } } +function MessageInputInviteEmbed({ + code, + onRemove, +}: { + code: string + onRemove: () => void +}) { + const t = useTheme() + const {t: l} = useLingui() + + return ( + + + + + + + ) +} + +function MessageInputInviteEmbedBody() { + const t = useTheme() + const {loading, preview} = ChatInvite.useChatInvite() + + if (loading) { + return ( + + + + ) + } + + if (!preview) { + return ( + + + Could not load invite + + + ) + } + + return +} + function SimpleContainer({ children, onRemove, diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index fbebdc074..6f56c6a89 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -28,6 +28,7 @@ import { type AppBskyEmbedRecord, AppBskyRichtextFacet, ChatBskyConvoDefs, + type ChatBskyEmbedJoinLink, RichText, } from '@atproto/api' import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect' @@ -37,6 +38,7 @@ import {ScrollProvider} from '#/lib/ScrollContext' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, isBskyPostUrl, } from '#/lib/strings/url-helpers' import {logger} from '#/logger' @@ -46,9 +48,10 @@ import { useConvoActive, } from '#/state/messages/convo' import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' +import {useGetJoinLinkPreview} from '#/state/queries/join-links' import {useGetPost} from '#/state/queries/post' import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' -import {useAgent} from '#/state/session' +import {useAgent, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageInput} from '#/screens/Messages/components/MessageInput' @@ -131,8 +134,10 @@ export function MessagesList({ const ax = useAnalytics() const convoState = useConvoActive() const agent = useAgent() + const {hasSession} = useSession() const getPost = useGetPost() - const {embedUri, setEmbed} = useMessageEmbed() + const getJoinLinkPreview = useGetJoinLinkPreview() + const {embed: messageEmbed, setEmbed} = useMessageEmbed() const t = useTheme() const textInputId = 'chat-input-' + useId() @@ -348,12 +353,38 @@ export function MessagesList({ // we want to remove the post link from the text, re-trim, then detect facets rt.detectFacetsWithoutResolution() - let embed: $Typed | undefined - let embedView: $Typed | undefined + let embed: + | $Typed + | $Typed + | undefined + let embedView: + | $Typed + | $Typed + | undefined + + // Find the embedded link facet and, if it's at the start or end of the + // message, remove it from the text (the embed card replaces it). + const stripLinkFacet = (predicate: (uri: string) => boolean) => { + const linkFacet = rt.facets?.find(facet => + facet.features.find( + feature => + AppBskyRichtextFacet.isLink(feature) && predicate(feature.uri), + ), + ) + if (linkFacet) { + const isAtStart = linkFacet.index.byteStart === 0 + const isAtEnd = + linkFacet.index.byteEnd === rt.unicodeText.graphemeLength + if (isAtStart || isAtEnd) { + rt.delete(linkFacet.index.byteStart, linkFacet.index.byteEnd) + } + rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true}) + } + } - if (embedUri) { + if (messageEmbed?.type === 'post') { try { - const post = await getPost({uri: embedUri}) + const post = await getPost({uri: messageEmbed.uri}) if (post) { embed = { $type: 'app.bsky.embed.record', @@ -368,42 +399,34 @@ export function MessagesList({ record: createEmbedViewRecordFromPost(post), } - // look for the embed uri in the facets, so we can remove it from the text - const postLinkFacet = rt.facets?.find(facet => { - return facet.features.find(feature => { - if (AppBskyRichtextFacet.isLink(feature)) { - if (isBskyPostUrl(feature.uri)) { - const url = convertBskyAppUrlIfNeeded(feature.uri) - const [_0, _1, _2, rkey] = url.split('/').filter(Boolean) - - // this might have a handle instead of a DID - // so just compare the rkey - not particularly dangerous - return post.uri.endsWith(rkey) - } - } - return false - }) + stripLinkFacet(uri => { + if (!isBskyPostUrl(uri)) return false + const url = convertBskyAppUrlIfNeeded(uri) + const [_0, _1, _2, rkey] = url.split('/').filter(Boolean) + // this might have a handle instead of a DID + // so just compare the rkey - not particularly dangerous + return post.uri.endsWith(rkey) }) - - if (postLinkFacet) { - const isAtStart = postLinkFacet.index.byteStart === 0 - const isAtEnd = - postLinkFacet.index.byteEnd === rt.unicodeText.graphemeLength - - // remove the post link from the text - if (isAtStart || isAtEnd) { - rt.delete( - postLinkFacet.index.byteStart, - postLinkFacet.index.byteEnd, - ) - } - - rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true}) - } } } catch (error) { logger.error('Failed to get post as quote for DM', {error}) } + } else if (messageEmbed?.type === 'invite') { + const code = messageEmbed.code + embed = { + $type: 'chat.bsky.embed.joinLink', + code, + } + + const joinLinkPreview = await getJoinLinkPreview({code, hasSession}) + if (joinLinkPreview) { + embedView = { + $type: 'chat.bsky.embed.joinLink#view', + joinLinkPreview, + } + } + + stripLinkFacet(uri => getChatInviteCodeFromUrl(uri) === code) } await rt.detectFacets(agent) @@ -424,7 +447,16 @@ export function MessagesList({ embedView, ) }, - [agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled], + [ + agent, + convoState, + messageEmbed, + getPost, + getJoinLinkPreview, + hasSession, + hasScrolled, + setHasScrolled, + ], ) const scrollToEndOnPress = useCallback(() => { @@ -595,11 +627,11 @@ export function MessagesList({ onSendMessage={(message: string) => void onSendMessage(message) } - hasEmbed={!!embedUri} + hasEmbed={!!messageEmbed} setEmbed={setEmbed} loading={loading}> @@ -607,11 +639,11 @@ export function MessagesList({ diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 08b609f71..e89965eb4 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -6,6 +6,7 @@ import { ChatBskyConvoDefs, type ChatBskyConvoGetLog, type ChatBskyConvoSendMessage, + type ChatBskyEmbedJoinLink, type ChatBskyGroupDefs, } from '@atproto/api' import {XRPCError} from '@atproto/api' @@ -109,7 +110,9 @@ export class Convo { { id: string message: ChatBskyConvoSendMessage.InputSchema['message'] - optimisticEmbedView?: $Typed + optimisticEmbedView?: + | $Typed + | $Typed } > = new Map() private deletedMessages: Set = new Set() @@ -942,7 +945,9 @@ export class Convo { sendMessage( message: ChatBskyConvoSendMessage.InputSchema['message'], - optimisticEmbedView?: $Typed, + optimisticEmbedView?: + | $Typed + | $Typed, ) { // Ignore empty messages for now since they have no other purpose atm if (!message.text.trim() && !message.embed) return diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index 51d111356..269f32c51 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -5,6 +5,7 @@ import { type ChatBskyActorDefs, type ChatBskyConvoDefs, type ChatBskyConvoSendMessage, + type ChatBskyEmbedJoinLink, } from '@atproto/api' import {type MessagesEventBus} from '#/state/messages/events/agent' @@ -108,7 +109,10 @@ export type ConvoItem = type DeleteMessage = (messageId: string) => Promise type SendMessage = ( message: ChatBskyConvoSendMessage.InputSchema['message'], - optimisticEmbedView: $Typed | undefined, + optimisticEmbedView: + | $Typed + | $Typed + | undefined, ) => void type FetchMessageHistory = () => Promise type MarkConvoAccepted = () => void diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index 99a37b6b0..df87ec0a2 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -1,4 +1,9 @@ -import {AtpAgent} from '@atproto/api' +import {useCallback} from 'react' +import { + AtpAgent, + type ChatBskyGroupDefs, + type ChatBskyGroupGetJoinLinkPreviews, +} from '@atproto/api' import {useQuery, useQueryClient} from '@tanstack/react-query' import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants' @@ -17,14 +22,39 @@ export const createJoinLinkPreviewQueryKey = (args: { persistedVersion: 1, }) +async function fetchJoinLinkPreviews({ + agent, + codes, + hasSession, +}: { + agent: AtpAgent + codes: string[] + hasSession: boolean +}) { + const previewAgent = new AtpAgent({service: CHAT_SERVICE}) + const res = hasSession + ? await agent.chat.bsky.group.getJoinLinkPreviews( + {codes}, + {headers: DM_SERVICE_HEADERS}, + ) + : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) + return res.data +} + export function useJoinLinkPreviewsQuery({ codes, hasSession, staleTime = STALE.MINUTES.ONE, + initialData, }: { codes?: string[] hasSession: boolean staleTime?: number + /** + * Seed the query with an already-known preview (e.g. a DM message embed + * already carries the resolved view), avoiding a duplicate fetch. + */ + initialData?: ChatBskyGroupGetJoinLinkPreviews.OutputSchema }) { const agent = useAgent() @@ -33,14 +63,7 @@ export function useJoinLinkPreviewsQuery({ queryFn: async () => { if (!codes) throw new Error('No invite code') try { - const previewAgent = new AtpAgent({service: CHAT_SERVICE}) - const res = hasSession - ? await agent.chat.bsky.group.getJoinLinkPreviews( - {codes}, - {headers: DM_SERVICE_HEADERS}, - ) - : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) - return res.data + return await fetchJoinLinkPreviews({agent, codes, hasSession}) } catch (error) { logger.error('Failed to fetch join link preview', {safeMessage: error}) throw error @@ -48,6 +71,7 @@ export function useJoinLinkPreviewsQuery({ }, enabled: codes != null && codes.length > 0, staleTime, + initialData, }) } @@ -58,17 +82,42 @@ export function usePrefetchJoinLinkPreviews() { return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => { return queryClient.prefetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}), - queryFn: async () => { - const previewAgent = new AtpAgent({service: CHAT_SERVICE}) - const res = hasSession - ? await agent.chat.bsky.group.getJoinLinkPreviews( - {codes}, - {headers: DM_SERVICE_HEADERS}, - ) - : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) - return res.data - }, + queryFn: () => fetchJoinLinkPreviews({agent, codes, hasSession}), staleTime: STALE.MINUTES.ONE, }) } } + +/** + * Imperatively fetch (or read from cache) a single join link preview by code. + * Used when sending a DM invite embed so we can build an optimistic view. + * Returns undefined if the preview can't be resolved. + */ +export function useGetJoinLinkPreview() { + const agent = useAgent() + const queryClient = useQueryClient() + + return useCallback( + async ({ + code, + hasSession, + }: { + code: string + hasSession: boolean + }): Promise => { + try { + const data = await queryClient.fetchQuery({ + queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}), + queryFn: () => + fetchJoinLinkPreviews({agent, codes: [code], hasSession}), + staleTime: STALE.MINUTES.ONE, + }) + return data.joinLinkPreviews[0] + } catch (error) { + logger.error('Failed to fetch join link preview', {safeMessage: error}) + return undefined + } + }, + [agent, queryClient], + ) +} diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 5cc87c63f..0b14f16ed 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -117,7 +117,7 @@ export const ExternalEmbedLink = ({ /> ) } else if (data.type === 'chat-invite') { - return + return } else if (data.kind === 'feed') { return ( Date: Thu, 4 Jun 2026 15:56:37 -0500 Subject: [PATCH 30/61] V smol log cleanups (#10731) --- eslint-suppressions.json | 3 -- src/analytics/PassiveAnalytics.tsx | 28 +++++++++---------- src/analytics/metrics/client.ts | 4 --- .../Post/Embed/StandardSiteEmbed/index.tsx | 4 ++- .../additional-moderation-authorities.ts | 8 ------ src/view/shell/desktop/LeftNav.tsx | 1 + 6 files changed, 17 insertions(+), 31 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 483ed83e7..b8e65eb26 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -43,9 +43,6 @@ } }, "src/analytics/PassiveAnalytics.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - }, "react-hooks/purity": { "count": 1 } diff --git a/src/analytics/PassiveAnalytics.tsx b/src/analytics/PassiveAnalytics.tsx index 34b5f2d27..c772ed09b 100644 --- a/src/analytics/PassiveAnalytics.tsx +++ b/src/analytics/PassiveAnalytics.tsx @@ -2,8 +2,6 @@ import {useEffect, useRef} from 'react' import {getCurrentState, onAppStateChange} from '#/lib/appState' import {useAnalytics} from '#/analytics' -import {Features, features} from '#/analytics/features' -import {IS_DEV, IS_TESTFLIGHT} from '#/env' /** * Tracks passive analytics like app foreground/background time. @@ -27,19 +25,19 @@ export function PassiveAnalytics() { }) } - if (IS_DEV || IS_TESTFLIGHT) { - const feats = Object.values(Features).reduce( - (acc, feat) => { - acc[feat] = features.evalFeature(feat) - return acc - }, - {} as Record, - ) - ax.logger.info('FEATURES', { - features: feats, - definitions: features.getFeatures(), - }) - } + // if (IS_DEV || IS_TESTFLIGHT) { + // const feats = Object.values(Features).reduce( + // (acc, feat) => { + // acc[feat] = features.evalFeature(feat) + // return acc + // }, + // {} as Record, + // ) + // ax.logger.info('FEATURES', { + // features: feats, + // definitions: features.getFeatures(), + // }) + // } }) return () => sub.remove() }, [ax]) diff --git a/src/analytics/metrics/client.ts b/src/analytics/metrics/client.ts index 2aab90f26..1ea0573dc 100644 --- a/src/analytics/metrics/client.ts +++ b/src/analytics/metrics/client.ts @@ -67,10 +67,6 @@ export class MetricsClient> { } private async sendBatch(events: Event[], isRetry: boolean = false) { - logger.debug(`sendBatch: ${events.length}`, { - isRetry, - }) - try { const body = JSON.stringify({events}) if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) { diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 5f7fc1f5e..c7b8abf88 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -356,6 +356,7 @@ export function PublicationCard({ /> {view.description && ( - + {view.description} @@ -617,6 +618,7 @@ export function PublicationFooter({ /> {sanitizeDisplayName( -- 2.51.2 From 45bd5b45e876e8ebd61b846ad886c7e93e9300dd Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:11:50 -0700 Subject: [PATCH 31/61] Reduce stale time for join link preview queries (#10733) --- src/state/queries/join-links.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index df87ec0a2..628f5e910 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -83,7 +83,7 @@ export function usePrefetchJoinLinkPreviews() { return queryClient.prefetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}), queryFn: () => fetchJoinLinkPreviews({agent, codes, hasSession}), - staleTime: STALE.MINUTES.ONE, + staleTime: STALE.SECONDS.FIFTEEN, }) } } @@ -110,7 +110,7 @@ export function useGetJoinLinkPreview() { queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}), queryFn: () => fetchJoinLinkPreviews({agent, codes: [code], hasSession}), - staleTime: STALE.MINUTES.ONE, + staleTime: STALE.SECONDS.FIFTEEN, }) return data.joinLinkPreviews[0] } catch (error) { -- 2.51.2 From de926d1838185ce08a6fbad5b6e9edb71dc02314 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:22:29 -0700 Subject: [PATCH 32/61] Show a banner when there are incoming chat requests (#10498) Co-authored-by: Samuel Newman --- src/screens/Messages/Conversation.tsx | 29 +++++- .../Messages/components/ChatListItem.tsx | 32 +++++-- .../Messages/components/RequestStatus.tsx | 92 +++++++++++++++++++ .../queries/messages/list-join-requests.ts | 4 +- .../messages/mark-join-request-read.ts | 85 +++++++++++++++++ 5 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 src/screens/Messages/components/RequestStatus.tsx create mode 100644 src/state/queries/messages/mark-join-request-read.ts diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index 6233c467f..b26904653 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -1,7 +1,7 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {type LayoutChangeEvent, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {moderateProfile} from '@atproto/api' +import {ChatBskyConvoDefs, moderateProfile} from '@atproto/api' import { ScrollEdgeEffect, ScrollEdgeEffectProvider, @@ -29,6 +29,7 @@ import {ConvoStatus} from '#/state/messages/convo/types' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useConvoQuery} from '#/state/queries/messages/conversation' +import {useMarkJoinRequestsRead} from '#/state/queries/messages/mark-join-request-read' import {useSession} from '#/state/session' import {MessagesList} from '#/screens/Messages/components/MessagesList' import {atoms as a, web} from '#/alf' @@ -51,6 +52,7 @@ import {IS_INTERNAL, IS_LIQUID_GLASS} from '#/env' import {ChatDisabled} from './components/ChatDisabled' import {ChatEnded} from './components/ChatEnded' import {ChatLocked} from './components/ChatLocked' +import {RequestStatus} from './components/RequestStatus' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -180,6 +182,12 @@ function InnerReady({ const {needsEmailVerification} = useEmail() const emailDialogControl = useEmailDialogControl() + const unreadRequestCount = + convo?.kind === 'group' && ChatBskyConvoDefs.isGroupConvo(convo.view.kind) + ? (convo.view.kind.unreadJoinRequestCount ?? 0) + : 0 + const {mutate: markJoinRequestsRead} = useMarkJoinRequestsRead(convo?.view.id) + /** * Must be non-reactive, otherwise the update to open the global dialog will * cause a re-render loop. @@ -264,8 +272,25 @@ function InnerReady({ {header} ) : ( - header + {header} )} + + {isActive && convo?.kind === 'group' && unreadRequestCount > 0 ? ( + { + markJoinRequestsRead() + }} + onPress={() => { + markJoinRequestsRead() + navigation.navigate('MessagesJoinRequests', { + conversation: convo.view.id, + }) + }} + /> + ) : null} + {isActive && ( 20 + requestInfo={ + convo.details.unreadJoinRequestCount + ? convo.details.unreadJoinRequestCount > JOIN_REQUESTS_THRESHOLD ? l({ - message: '20+ new join requests', + message: `${JOIN_REQUESTS_THRESHOLD}+ new join requests`, context: 'Displayed when there are more than 20 requests to join a group chat', }) - : plural(convo.details.joinRequestCount, { + : plural(convo.details.unreadJoinRequestCount, { one: '# new join request', other: '# new join requests', }) @@ -241,6 +242,7 @@ function BaseChatItem({ avatar, title, subtitle, + requestInfo, accessibilityHint, isDeletedAccount, isBlockedAccount, @@ -256,6 +258,7 @@ function BaseChatItem({ avatar: React.ReactNode title: string subtitle?: string + requestInfo?: string accessibilityHint: string isDeletedAccount: boolean isBlockedAccount: boolean @@ -280,8 +283,10 @@ function BaseChatItem({ const playHaptic = useHaptics() const queryClient = useQueryClient() const hasUnread = - convo.view.unreadCount > 0 && !isDeletedAccount && + (convo.view.unreadCount > 0 || + (convo.kind === 'group' && + (convo.details.unreadJoinRequestCount ?? 0) > 0)) && (convo.kind !== 'group' || convo.details.lockStatus === 'unlocked') const blockInfo = useMemo(() => { @@ -607,6 +612,19 @@ function BaseChatItem({ {postAlerts} + {requestInfo && ( + + {requestInfo} + + )} + {LastMessageIcon && ( diff --git a/src/screens/Messages/components/RequestStatus.tsx b/src/screens/Messages/components/RequestStatus.tsx new file mode 100644 index 000000000..9b35b6944 --- /dev/null +++ b/src/screens/Messages/components/RequestStatus.tsx @@ -0,0 +1,92 @@ +import {Pressable} from 'react-native' +import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import {plural} from '@lingui/core/macro' +import {useLingui} from '@lingui/react/macro' + +import {HITSLOP_10} from '#/lib/constants' +import {JOIN_REQUESTS_THRESHOLD} from '#/state/queries/messages/list-join-requests' +import {atoms as a, tokens, useTheme} from '#/alf' +import {GlassView} from '#/components/GlassView' +import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope' +import {TimesLarge_Stroke2_Corner0_Rounded as CloseIcon} from '#/components/icons/Times' +import {Text} from '#/components/Typography' +import {IS_LIQUID_GLASS} from '#/env' + +export function RequestStatus({ + top, + count, + onDismiss, + onPress, +}: { + top: number + count: number + onDismiss: () => void + onPress: () => void +}) { + const t = useTheme() + const {t: l} = useLingui() + + return ( + + + + + + {count > JOIN_REQUESTS_THRESHOLD + ? l({ + message: `${JOIN_REQUESTS_THRESHOLD}+ new join requests`, + comment: + 'Displayed when the number of requests is greater than 20', + }) + : plural(count, { + one: '# new join request', + other: '# new join requests', + })} + + + + + + + + ) +} diff --git a/src/state/queries/messages/list-join-requests.ts b/src/state/queries/messages/list-join-requests.ts index 9f0860c20..64a999c11 100644 --- a/src/state/queries/messages/list-join-requests.ts +++ b/src/state/queries/messages/list-join-requests.ts @@ -8,6 +8,8 @@ import {createQueryKey} from '#/state/queries/util' import {useAgent} from '#/state/session' import {STALE} from '..' +export const JOIN_REQUESTS_THRESHOLD = 20 + const listJoinRequestsQueryKeyRoot = 'list-join-requests' export const createListJoinRequestsQueryKey = (args: {convoId: string}) => @@ -53,7 +55,7 @@ export function useListJoinRequestsQuery({ queryKey: createListJoinRequestsQueryKey({convoId: convoId ?? ''}), queryFn: async ({pageParam}) => { const {data} = await agent.chat.bsky.group.listJoinRequests( - {convoId: convoId!, cursor: pageParam, limit: 20}, + {convoId: convoId!, cursor: pageParam, limit: JOIN_REQUESTS_THRESHOLD}, {headers: DM_SERVICE_HEADERS}, ) return data diff --git a/src/state/queries/messages/mark-join-request-read.ts b/src/state/queries/messages/mark-join-request-read.ts new file mode 100644 index 000000000..ae6d7e88a --- /dev/null +++ b/src/state/queries/messages/mark-join-request-read.ts @@ -0,0 +1,85 @@ +import {ChatBskyConvoDefs} from '@atproto/api' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {DM_SERVICE_HEADERS} from '#/lib/constants' +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import {RQKEY as CONVO_KEY} from './conversation' +import { + type ConvoListQueryData, + RQKEY_ROOT as CONVO_LIST_ROOT_KEY, +} from './list-conversations' + +export function useMarkJoinRequestsRead(convoId: string | undefined) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async () => { + if (!convoId) throw new Error('No convoId provided') + await agent.chat.bsky.group.updateJoinRequestsRead( + {convoId}, + {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, + ) + }, + onMutate: () => { + if (!convoId) return + + const prevConvo = queryClient.getQueryData( + CONVO_KEY(convoId), + ) + queryClient.setQueryData( + CONVO_KEY(convoId), + old => { + if (!old || !ChatBskyConvoDefs.isGroupConvo(old.kind)) return old + return { + ...old, + kind: {...old.kind, unreadJoinRequestCount: 0}, + } + }, + ) + + const prevListEntries = queryClient.getQueriesData({ + queryKey: [CONVO_LIST_ROOT_KEY], + }) + queryClient.setQueriesData( + {queryKey: [CONVO_LIST_ROOT_KEY]}, + old => { + if (!old) return old + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + convos: page.convos.map(convo => { + if ( + convo.id !== convoId || + !ChatBskyConvoDefs.isGroupConvo(convo.kind) + ) { + return convo + } + return { + ...convo, + kind: {...convo.kind, unreadJoinRequestCount: 0}, + } + }), + })), + } + }, + ) + + return {prevConvo, prevListEntries} + }, + onError: (error, _, context) => { + logger.error('Failed to mark join requests as read', {safeMessage: error}) + if (!convoId) return + if (context?.prevConvo) { + queryClient.setQueryData(CONVO_KEY(convoId), context.prevConvo) + } + for (const [key, data] of context?.prevListEntries ?? []) { + queryClient.setQueryData(key, data) + } + void queryClient.invalidateQueries({queryKey: CONVO_KEY(convoId)}) + void queryClient.invalidateQueries({queryKey: [CONVO_LIST_ROOT_KEY]}) + }, + }) +} -- 2.51.2 From 144ba30ea647f4d80a2e76b14a380f3eb0a02149 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Thu, 4 Jun 2026 17:22:40 -0400 Subject: [PATCH 33/61] Add gallery embed type support (display + compose) (#10707) --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/components/MediaPreview.tsx | 34 +++- src/components/Post/Embed/ImageEmbed.tsx | 23 ++- src/components/Post/Embed/index.tsx | 4 +- .../Gallery/maybeApplyGalleryOffsetStyles.ts | 33 +++- src/components/images/ImageLayoutGrid.tsx | 22 ++- src/components/images/ImageLayoutGridItem.tsx | 3 + .../ReportDialog/utils/parseReportSubject.ts | 5 +- src/lib/api/index.ts | 31 ++++ src/types/bsky/post.ts | 10 ++ src/view/com/composer/Composer.tsx | 124 ++++++++++---- src/view/com/composer/ComposerReplyTo.tsx | 30 +++- src/view/com/composer/SelectMediaButton.tsx | 19 ++- src/view/com/composer/drafts/state/api.ts | 159 ++++++++++++------ src/view/com/composer/drafts/state/queries.ts | 23 ++- src/view/com/composer/photos/Gallery.tsx | 7 +- src/view/com/composer/state/composer.ts | 92 +++++++--- src/view/com/feeds/ComposerPrompt.tsx | 6 +- 19 files changed, 494 insertions(+), 143 deletions(-) diff --git a/package.json b/package.json index c515ecdce..a879182d9 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "prettier": "prettier --check ." }, "dependencies": { - "@atproto/api": "0.20.8", + "@atproto/api": "0.20.9", "@atproto/syntax": "0.6.1", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0136363f..0ac8974cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,8 +242,8 @@ importers: .: dependencies: '@atproto/api': - specifier: 0.20.8 - version: 0.20.8 + specifier: 0.20.9 + version: 0.20.9 '@atproto/syntax': specifier: 0.6.1 version: 0.6.1 @@ -877,8 +877,8 @@ packages: graphql: optional: true - '@atproto/api@0.20.8': - resolution: {integrity: sha512-rTkA6kOmA2axSrg6VgpdXpsCFWpofnHBOn6pKg69Ju5MpIHqk4haQMgjBcVh1G3kUxzwgSAr7SYrPS3dFe5Etg==} + '@atproto/api@0.20.9': + resolution: {integrity: sha512-Yuw7Ewn+yMJZ8GskbuvI3lKPW65rsXic1xjFA2Dpq6H8WjVYs6xNZ31bkwtTYDDwjKIZcJmAVbAVgdfjo4T9iw==} engines: {node: '>=22'} '@atproto/common-web@0.5.0': @@ -9493,7 +9493,7 @@ snapshots: '@0no-co/graphql.web@1.2.0': {} - '@atproto/api@0.20.8': + '@atproto/api@0.20.9': dependencies: '@atproto/common-web': 0.5.0 '@atproto/lexicon': 0.7.1 diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index b36705d84..034094556 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -1,6 +1,10 @@ import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' import {Image} from 'expo-image' -import {type AppBskyEmbedImages, type AppBskyFeedDefs} from '@atproto/api' +import { + AppBskyEmbedGallery, + type AppBskyEmbedImages, + type AppBskyFeedDefs, +} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {shareImageModal} from '#/lib/media/manip' @@ -47,6 +51,34 @@ export function Embed({ )} ) + } else if (e.type === 'gallery') { + // Notification/DM preview is a narrow inline strip; cap at 4 tiles so + // a 10-image gallery doesn't blow out the row width. Single pass instead + // of filter().slice().map() so we stop at 4 viewable items rather than + // walking every item in a 10-image gallery. + const tiles: React.ReactNode[] = [] + for (const item of e.view.items) { + if (tiles.length >= 4) break + if (!AppBskyEmbedGallery.isViewImage(item)) continue + if (peekable) { + const image: AppBskyEmbedImages.ViewImage = { + thumb: item.thumbnail, + fullsize: item.fullsize, + alt: item.alt, + aspectRatio: item.aspectRatio, + } + tiles.push() + } else { + tiles.push( + , + ) + } + } + return {tiles} } else if (e.type === 'link') { if (!e.view.external.thumb) return null if (!isGifEmbed(e.view.external.uri)) return null diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index 2c6bb6c5d..0a620efde 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -2,6 +2,7 @@ import {useRef} from 'react' import {InteractionManager, View} from 'react-native' import {type AnimatedRef} from 'react-native-reanimated' import {Image} from 'expo-image' +import {AppBskyEmbedGallery, type AppBskyEmbedImages} from '@atproto/api' import {atoms as a, tokens} from '#/alf' import {AutoSizedImage} from '#/components/images/AutoSizedImage' @@ -15,16 +16,29 @@ import {useAnalytics} from '#/analytics' import {type EmbedType} from '#/types/bsky/post' import {type CommonProps} from './types' +const MAX_GRID_IMAGES = 4 + export function ImageEmbed({ embed, ...rest }: CommonProps & { - embed: EmbedType<'images'> + embed: EmbedType<'images'> | EmbedType<'gallery'> }) { const ax = useAnalytics() const {openLightbox} = useLightboxControls() - const {images} = embed.view - const galleryEnabled = ax.features.enabled(ax.features.PostGalleryEmbedEnable) + const images: AppBskyEmbedImages.ViewImage[] = + embed.type === 'gallery' + ? embed.view.items.filter(AppBskyEmbedGallery.isViewImage).map(item => ({ + thumb: item.thumbnail, + fullsize: item.fullsize, + alt: item.alt, + aspectRatio: item.aspectRatio, + })) + : embed.view.images + const useExpandedLayout = + embed.type === 'gallery' + ? images.length > MAX_GRID_IMAGES + : ax.features.enabled(ax.features.PostGalleryEmbedEnable) // Captured from AutoSizedImage so the peek-commit handler can reuse the same // ref + dims that a tap would — keeps the lightbox's return animation intact. @@ -109,7 +123,7 @@ export function ImageEmbed({ ) } - if (galleryEnabled) { + if (useExpandedLayout) { return ( ) diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index ab6f5dc43..28eed2aaa 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -54,6 +54,7 @@ export function Embed({embed: rawEmbed, ...rest}: EmbedProps) { switch (embed.type) { case 'images': + case 'gallery': case 'link': case 'video': { return @@ -89,7 +90,8 @@ function MediaEmbed({ embed: TEmbed }) { switch (embed.type) { - case 'images': { + case 'images': + case 'gallery': { return ( ( post.record, @@ -39,6 +37,13 @@ export function maybeApplyGalleryOffsetStyles( return } + // The gate only controls whether legacy image embeds opt into the new + // expanded gallery layout. Gallery embeds always render expanded by item + // count, so their offset must apply regardless of the gate. + const isPostGalleryEmbedEnabled = features.isOn( + Features.PostGalleryEmbedEnable, + ) + /* * First check if we even have images */ @@ -49,6 +54,12 @@ export function maybeApplyGalleryOffsetStyles( embed, AppBskyEmbedImages.isMain, ) + const isGalleryEmbed = + embed && + bsky.dangerousIsType( + embed, + AppBskyEmbedGallery.isMain, + ) const isRecordWithMedia = embed && bsky.dangerousIsType( @@ -57,10 +68,16 @@ export function maybeApplyGalleryOffsetStyles( ) let hasImages = false if (isImageEmbed) { + if (!isPostGalleryEmbedEnabled) return // one image, not a gallery if (embed.images.length === 1) return hasImages = true } + if (isGalleryEmbed) { + // single (or empty) gallery - no offset needed + if (embed.items.length <= 1) return + hasImages = true + } if (isRecordWithMedia) { if ( bsky.dangerousIsType( @@ -68,9 +85,19 @@ export function maybeApplyGalleryOffsetStyles( AppBskyEmbedImages.isMain, ) ) { + if (!isPostGalleryEmbedEnabled) return // one image, not a gallery if (embed.media.images.length === 1) return } + if ( + bsky.dangerousIsType( + embed.media, + AppBskyEmbedGallery.isMain, + ) + ) { + // single (or empty) gallery - no offset needed + if (embed.media.items.length <= 1) return + } hasImages = true } if (!hasImages) return diff --git a/src/components/images/ImageLayoutGrid.tsx b/src/components/images/ImageLayoutGrid.tsx index 0017ddf9c..5b4ba608b 100644 --- a/src/components/images/ImageLayoutGrid.tsx +++ b/src/components/images/ImageLayoutGrid.tsx @@ -19,21 +19,28 @@ interface ImageLayoutGridProps { onPressIn?: (index: number) => void style?: StyleProp viewContext?: PostEmbedViewContext + isWithinQuote?: boolean } -export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { +export function ImageLayoutGrid({ + style, + isWithinQuote: isWithinQuoteProp, + ...props +}: ImageLayoutGridProps) { const {gtMobile} = useBreakpoints() - const gap = + const isWithinQuote = + isWithinQuoteProp ?? props.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - ? gtMobile - ? a.gap_xs - : a.gap_2xs - : a.gap_xs + const gap = isWithinQuote ? (gtMobile ? a.gap_xs : a.gap_2xs) : a.gap_xs return ( - + ) @@ -49,6 +56,7 @@ interface ImageLayoutGridInnerProps { onLongPress?: (index: number) => void onPressIn?: (index: number) => void viewContext?: PostEmbedViewContext + isWithinQuote?: boolean gap: {gap: number} } diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index d6cbc0ade..640ecd8ba 100644 --- a/src/components/images/ImageLayoutGridItem.tsx +++ b/src/components/images/ImageLayoutGridItem.tsx @@ -29,6 +29,7 @@ interface Props { onPressIn?: EventFunction imageStyle?: StyleProp viewContext?: PostEmbedViewContext + isWithinQuote?: boolean insetBorderStyle?: StyleProp containerRefs: AnimatedRef[] thumbDimsRef: React.RefObject<(Dimensions | null)[]> @@ -42,6 +43,7 @@ export function GalleryItem({ onPressIn, onLongPress, viewContext, + isWithinQuote, insetBorderStyle, containerRefs, thumbDimsRef, @@ -52,6 +54,7 @@ export function GalleryItem({ const image = images[index] const hasAlt = !!image.alt const hideBadges = + isWithinQuote ?? viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia const aspect = diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts index 405640c45..a7d4b94c3 100644 --- a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts +++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts @@ -87,7 +87,10 @@ export function parseReportSubject( reply: !!record.reply, image: embed.type === 'images' || - (embed.type === 'post_with_media' && embed.media.type === 'images'), + embed.type === 'gallery' || + (embed.type === 'post_with_media' && + (embed.media.type === 'images' || + embed.media.type === 'gallery')), video: embed.type === 'video' || (embed.type === 'post_with_media' && embed.media.type === 'video'), diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 7de4e13e5..6f92e2a1d 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,6 +1,7 @@ import { type $Typed, type AppBskyEmbedExternal, + type AppBskyEmbedGallery, type AppBskyEmbedImages, type AppBskyEmbedRecord, type AppBskyEmbedRecordWithMedia, @@ -254,6 +255,7 @@ async function resolveEmbed( onStateChange: ((state: string) => void) | undefined, ): Promise< | $Typed + | $Typed | $Typed | $Typed | $Typed @@ -313,6 +315,7 @@ async function resolveMedia( ): Promise< | $Typed | $Typed + | $Typed | $Typed | undefined > { @@ -343,6 +346,34 @@ async function resolveMedia( images, } } + if (embedDraft.media?.type === 'gallery') { + const imagesDraft = embedDraft.media.images + logger.debug(`Uploading images`, { + count: imagesDraft.length, + }) + onStateChange?.(t`Uploading images...`) + const items: $Typed[] = await Promise.all( + imagesDraft.map(async (image, i) => { + logger.debug(`Compressing image #${i}`) + const {path, width, height, mime} = await compressImage( + image, + IMAGE_SIZE_CONFIG_POSTS, + ) + logger.debug(`Uploading image #${i}`) + const res = await uploadBlob(agent, path, mime) + return { + $type: 'app.bsky.embed.gallery#image' as const, + image: res.data.blob, + alt: image.alt, + aspectRatio: {width, height}, + } + }), + ) + return { + $type: 'app.bsky.embed.gallery', + items, + } + } if ( embedDraft.media?.type === 'video' && embedDraft.media.video.status === 'done' diff --git a/src/types/bsky/post.ts b/src/types/bsky/post.ts index fada39da8..43621ef63 100644 --- a/src/types/bsky/post.ts +++ b/src/types/bsky/post.ts @@ -1,6 +1,7 @@ import { type $Typed, AppBskyEmbedExternal, + AppBskyEmbedGallery, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, @@ -47,6 +48,10 @@ export type Embed = type: 'images' view: $Typed } + | { + type: 'gallery' + view: $Typed + } | { type: 'link' view: $Typed @@ -122,6 +127,11 @@ export function parseEmbed(embed: AppBskyFeedDefs.PostView['embed']): Embed { type: 'images', view: embed, } + } else if (AppBskyEmbedGallery.isView(embed)) { + return { + type: 'gallery', + view: embed, + } } else if (AppBskyEmbedExternal.isView(embed)) { return { type: 'link', diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 5c9f1a05f..a2493557b 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -159,7 +159,7 @@ import { composerReducer, createComposerState, type EmbedDraft, - MAX_IMAGES, + MAX_GALLERY_IMAGES, type PostAction, type PostDraft, type ThreadDraft, @@ -178,6 +178,65 @@ type CancelRef = { onPressCancel: () => void } +function applyGalleryCap( + currentCount: number, + incoming: ComposerImage[], +): + | {status: 'full'} + | {status: 'partial'; accepted: ComposerImage[]; dropped: number} + | {status: 'ok'; accepted: ComposerImage[]} { + const remaining = MAX_GALLERY_IMAGES - currentCount + if (remaining <= 0) { + return {status: 'full'} + } + if (incoming.length > remaining) { + return { + status: 'partial', + accepted: incoming.slice(0, remaining), + dropped: incoming.length - remaining, + } + } + return {status: 'ok', accepted: incoming} +} + +function useAddImagesWithCap( + currentCount: number, + dispatchPostAction: (action: PostAction) => void, +) { + const {t: l} = useLingui() + return useCallback( + (next: ComposerImage[]) => { + const result = applyGalleryCap(currentCount, next) + if (result.status === 'full') { + Toast.show( + l({ + message: `You can only add up to ${MAX_GALLERY_IMAGES} images per post`, + comment: + 'Toast shown when the user tries to add more images but the post gallery is already at the cap', + }), + {type: 'warning'}, + ) + return + } + if (result.status === 'partial') { + Toast.show( + l({ + message: `Only ${result.accepted.length} of ${next.length} ${plural(next.length, {one: 'image', other: 'images'})} added; limit is ${MAX_GALLERY_IMAGES}`, + comment: + 'Toast shown when adding images would exceed the post gallery cap; only the first N are kept', + }), + {type: 'warning'}, + ) + } + dispatchPostAction({ + type: 'embed_add_images', + images: result.accepted, + }) + }, + [currentCount, dispatchPostAction, l], + ) +} + type Props = ComposerOpts export const ComposePost = ({ replyTo, @@ -611,7 +670,11 @@ export const ComposePost = ({ ax.metric('draft:save', { isNewDraft, hasText: posts.some(p => p.richtext.text.trim().length > 0), - hasImages: posts.some(p => p.embed.media?.type === 'images'), + hasImages: posts.some( + p => + p.embed.media?.type === 'images' || + p.embed.media?.type === 'gallery', + ), hasVideo: posts.some(p => p.embed.media?.type === 'video'), hasGif: posts.some(p => p.embed.media?.type === 'gif'), hasQuote: posts.some(p => !!p.embed.quote), @@ -780,7 +843,10 @@ export const ComposePost = ({ for (let i = 0; i < thread.posts.length; i++) { const media = thread.posts[i].embed.media if (media) { - if (media.type === 'images' && media.images.some(img => !img.alt)) { + if ( + (media.type === 'images' || media.type === 'gallery') && + media.images.some(img => !img.alt) + ) { return l`One or more images is missing alt text.` } if (media.type === 'gif' && !media.alt) { @@ -931,7 +997,9 @@ export const ComposePost = ({ logger.error(e, { message: `Composer: create post failed`, hasImages: filteredThread.posts.some( - p => p.embed.media?.type === 'images', + p => + p.embed.media?.type === 'images' || + p.embed.media?.type === 'gallery', ), }) @@ -953,7 +1021,8 @@ export const ComposePost = ({ for (let post of filteredThread.posts) { ax.metric('post:create', { imageCount: - post.embed.media?.type === 'images' + post.embed.media?.type === 'images' || + post.embed.media?.type === 'gallery' ? post.embed.media.images.length : 0, isReply: index > 0 || !!replyTo, @@ -1395,15 +1464,11 @@ let ComposerPost = memo(function ComposerPost({ [dispatch, post.id], ) - const onImageAdd = useCallback( - (next: ComposerImage[]) => { - dispatchPost({ - type: 'embed_add_images', - images: next, - }) - }, - [dispatchPost], - ) + const postImagesCount = + post.embed.media?.type === 'images' || post.embed.media?.type === 'gallery' + ? post.embed.media.images.length + : 0 + const onImageAdd = useAddImagesWithCap(postImagesCount, dispatchPost) const onNewLink = useCallback( (uri: string) => { @@ -1708,7 +1773,7 @@ function ComposerEmbeds({ const video = embed.media?.type === 'video' ? embed.media.video : null return ( <> - {embed.media?.type === 'images' && ( + {(embed.media?.type === 'images' || embed.media?.type === 'gallery') && ( )} @@ -1819,7 +1884,11 @@ function ComposerPills({ }) { const t = useTheme() const media = post.embed.media - const hasMedia = media?.type === 'images' || media?.type === 'video' + const hasMedia = + media?.type === 'images' || + media?.type === 'gallery' || + media?.type === 'gif' || + media?.type === 'video' const hasLink = !!post.embed.link // Don't render anything if no pills are going to be displayed @@ -1908,15 +1977,16 @@ function ComposerFooter({ >(undefined) const media = post.embed.media - const images = media?.type === 'images' ? media.images : [] + const images = + media?.type === 'images' || media?.type === 'gallery' ? media.images : [] const video = media?.type === 'video' ? media.video : null - const isMaxImages = images.length >= MAX_IMAGES + const isMaxImages = images.length >= MAX_GALLERY_IMAGES const isMaxVideos = !!video let selectedAssetsCount = 0 let isMediaSelectionDisabled = false - if (media?.type === 'images') { + if (media?.type === 'images' || media?.type === 'gallery') { isMediaSelectionDisabled = isMaxImages selectedAssetsCount = images.length } else if (media?.type === 'video') { @@ -1926,15 +1996,7 @@ function ComposerFooter({ isMediaSelectionDisabled = !!media } - const onImageAdd = useCallback( - (next: ComposerImage[]) => { - dispatch({ - type: 'embed_add_images', - images: next, - }) - }, - [dispatch], - ) + const onImageAdd = useAddImagesWithCap(images.length, dispatch) const onSelectGif = useCallback( (gif: Gif) => { @@ -2017,7 +2079,11 @@ function ComposerFooter({ autoOpen={openGallery} /> diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index bac3e74d3..235139ef8 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -2,6 +2,7 @@ import {useCallback, useMemo, useState} from 'react' import {LayoutAnimation, Pressable, View} from 'react-native' import {Image} from 'expo-image' import { + AppBskyEmbedGallery, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, @@ -61,11 +62,14 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { const images = useMemo(() => { if (AppBskyEmbedImages.isView(embed)) { return embed.images - } else if ( - AppBskyEmbedRecordWithMedia.isView(embed) && - AppBskyEmbedImages.isView(embed.media) - ) { - return embed.media.images + } else if (AppBskyEmbedGallery.isView(embed)) { + return galleryItemsToImages(embed.items) + } else if (AppBskyEmbedRecordWithMedia.isView(embed)) { + if (AppBskyEmbedImages.isView(embed.media)) { + return embed.media.images + } else if (AppBskyEmbedGallery.isView(embed.media)) { + return galleryItemsToImages(embed.media.items) + } } }, [embed]) @@ -129,6 +133,22 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { ) } +function galleryItemsToImages( + items: AppBskyEmbedGallery.View['items'], +): AppBskyEmbedImages.ViewImage[] { + // The reply-to thumbnail only renders up to 4 tiles; slicing here keeps + // the existing layout switch valid for galleries up to 10 items. + return items + .filter(AppBskyEmbedGallery.isViewImage) + .slice(0, 4) + .map(item => ({ + thumb: item.thumbnail, + fullsize: item.fullsize, + alt: item.alt, + aspectRatio: item.aspectRatio, + })) +} + function ComposerReplyToImages({ images, }: { diff --git a/src/view/com/composer/SelectMediaButton.tsx b/src/view/com/composer/SelectMediaButton.tsx index 2d7019548..1de39f5f5 100644 --- a/src/view/com/composer/SelectMediaButton.tsx +++ b/src/view/com/composer/SelectMediaButton.tsx @@ -16,7 +16,7 @@ import { } from '#/lib/hooks/usePermissions' import {openUnifiedPicker} from '#/lib/media/picker' import {extractDataUriMime} from '#/lib/media/util' -import {MAX_IMAGES} from '#/view/com/composer/state/composer' +import {MAX_GALLERY_IMAGES} from '#/view/com/composer/state/composer' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' @@ -393,7 +393,9 @@ export function SelectMediaButton({ const t = useTheme() const hasAutoOpened = useRef(false) - const selectionCountRemaining = MAX_IMAGES - selectedAssetsCount + // Picker uses the gallery cap; the reducer decides which embed variant + // to land in based on the final image count. + const selectionCountRemaining = MAX_GALLERY_IMAGES - selectedAssetsCount const processSelectedAssets = useCallback( async (rawAssets: ImagePickerAsset[]) => { @@ -419,10 +421,10 @@ export function SelectMediaButton({ ), [SelectedAssetError.MaxImages]: _( msg({ - message: `You can select up to ${plural(MAX_IMAGES, { + message: `You can select up to ${plural(MAX_GALLERY_IMAGES, { other: '# images', })} in total.`, - comment: `Error message for maximum number of images that can be selected to add to a post, currently 4 but may change.`, + comment: `Error message for maximum number of images that can be selected to add to a post.`, }), ), [SelectedAssetError.MaxVideos]: _( @@ -507,10 +509,11 @@ export function SelectMediaButton({ )} accessibilityHint={_( msg({ - message: `Opens device gallery to select up to ${plural(MAX_IMAGES, { - other: '# images', - })}, or a single video or GIF.`, - comment: `Accessibility hint for button in composer to add images, a video, or a GIF to a post. Maximum number of images that can be selected is currently 4 but may change.`, + message: `Opens device gallery to select up to ${plural( + MAX_GALLERY_IMAGES, + {other: '# images'}, + )}, or a single video or GIF.`, + comment: `Accessibility hint for button in composer to add images, a video, or a GIF to a post.`, }), )} style={a.p_sm} diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index 8f178d528..817820eff 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -1,7 +1,7 @@ /** * Type converters for Draft API - convert between ComposerState and server Draft types. */ -import {type AppBskyDraftDefs, AtUri, RichText} from '@atproto/api' +import {AppBskyDraftDefs, AtUri, RichText} from '@atproto/api' import {nanoid} from 'nanoid/non-secure' import {resolveLink} from '#/lib/api/resolve' @@ -15,6 +15,7 @@ import {createPublicAgent} from '#/state/session/agent' import { type ComposerState, type EmbedDraft, + LEGACY_IMAGES_EMBED_MAX, type PostDraft, } from '#/view/com/composer/state/composer' import {type VideoState} from '#/view/com/composer/state/video' @@ -115,6 +116,16 @@ async function postDraftToServerPost( post.embed.media.images, localRefPaths, ) + } else if (post.embed.media.type === 'gallery') { + draftPost.embedGallery = { + $type: 'app.bsky.draft.defs#draftEmbedGallery', + items: serializeImages(post.embed.media.images, localRefPaths).map( + img => ({ + $type: 'app.bsky.draft.defs#draftEmbedImage' as const, + ...img, + }), + ), + } } else if (post.embed.media.type === 'video') { const video = await serializeVideo(post.embed.media.video, localRefPaths) if (video) { @@ -269,6 +280,59 @@ function serializeGif(gifMedia: { } } +/** + * Restore an array of draft image refs back to ComposerImages. Shared by + * both the `embedImages` and `embedGallery` paths in draftToComposerPosts. + */ +async function restoreDraftImages( + draftImages: AppBskyDraftDefs.DraftEmbedImage[], + loadedMedia: Map, +): Promise { + const imagePromises = draftImages.map(async img => { + const path = loadedMedia.get(img.localRef.path) + if (!path) { + return null + } + + let width = 0 + let height = 0 + try { + const dims = await getImageDim(path) + width = dims.width + height = dims.height + } catch (e) { + logger.warn('Failed to get image dimensions', { + path, + error: e, + }) + } + + logger.debug('restoring image with localRefPath', { + localRefPath: img.localRef.path, + loadedPath: path, + width, + height, + }) + + return { + alt: img.alt || '', + // Preserve the original localRefPath for reuse when saving + localRefPath: img.localRef.path, + source: { + id: nanoid(), + path, + width, + height, + mime: 'image/jpeg', + }, + } satisfies ComposerImage + }) + + return (await Promise.all(imagePromises)).filter( + (img): img is NonNullable => img !== null, + ) +} + /** * Convert server DraftView to DraftSummary for list display. * Also checks which media files exist locally. @@ -314,6 +378,24 @@ export function draftViewToSummary({ } } + // Process gallery + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue + meta.mediaCount++ + meta.hasMedia = true + const exists = storage.mediaExists(item.localRef.path) + if (!exists) { + meta.hasMissingMedia = true + } + images.push({ + localPath: item.localRef.path, + altText: item.alt || '', + exists, + }) + } + } + // Process videos if (post.embedVideos) { for (const vid of post.embedVideos) { @@ -431,54 +513,31 @@ export async function draftToComposerPosts( media: undefined, } - // Restore images + // Restore images / gallery. Pick the variant from the restored count so + // we match the composer reducer's `imagesToMediaVariant` rule (<=4 stays + // legacy `images`, >4 promotes to `gallery`). This keeps restore robust + // to drafts whose server slot disagrees with their count - e.g. a draft + // saved in `embedImages` with 5 items would otherwise restore as a + // broken `images` variant the rest of the composer can't grow. + const restoredImages: ComposerImage[] = [] if (post.embedImages && post.embedImages.length > 0) { - const imagePromises = post.embedImages.map(async img => { - const path = loadedMedia.get(img.localRef.path) - if (!path) { - return null - } - - let width = 0 - let height = 0 - try { - const dims = await getImageDim(path) - width = dims.width - height = dims.height - } catch (e) { - logger.warn('Failed to get image dimensions', { - path, - error: e, - }) - } - - logger.debug('restoring image with localRefPath', { - localRefPath: img.localRef.path, - loadedPath: path, - width, - height, - }) - - return { - alt: img.alt || '', - // Preserve the original localRefPath for reuse when saving - localRefPath: img.localRef.path, - source: { - id: nanoid(), - path, - width, - height, - mime: 'image/jpeg', - }, - } satisfies ComposerImage - }) - - const images = (await Promise.all(imagePromises)).filter( - (img): img is NonNullable => img !== null, + restoredImages.push( + ...(await restoreDraftImages(post.embedImages, loadedMedia)), ) - if (images.length > 0) { - embed.media = {type: 'images', images} - } + } + if (post.embedGallery && post.embedGallery.items.length > 0) { + const galleryImages = post.embedGallery.items.filter( + AppBskyDraftDefs.isDraftEmbedImage, + ) + restoredImages.push( + ...(await restoreDraftImages(galleryImages, loadedMedia)), + ) + } + if (restoredImages.length > 0) { + embed.media = + restoredImages.length <= LEGACY_IMAGES_EMBED_MAX + ? {type: 'images', images: restoredImages} + : {type: 'gallery', images: restoredImages} } // Restore GIF from external embed @@ -630,6 +689,12 @@ export function extractLocalRefs(draft: AppBskyDraftDefs.Draft): Set { refs.add(img.localRef.path) } } + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue + refs.add(item.localRef.path) + } + } if (post.embedVideos) { for (const vid of post.embedVideos) { refs.add(vid.localRef.path) diff --git a/src/view/com/composer/drafts/state/queries.ts b/src/view/com/composer/drafts/state/queries.ts index e66c9d023..07dd67dd8 100644 --- a/src/view/com/composer/drafts/state/queries.ts +++ b/src/view/com/composer/drafts/state/queries.ts @@ -1,4 +1,4 @@ -import {AppBskyDraftCreateDraft, type AppBskyDraftDefs} from '@atproto/api' +import {AppBskyDraftCreateDraft, AppBskyDraftDefs} from '@atproto/api' import { useInfiniteQuery, useMutation, @@ -74,6 +74,21 @@ export async function loadDraftMedia(draft: AppBskyDraftDefs.Draft): Promise<{ } } } + // Load gallery + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue + try { + const url = await storage.loadMediaFromLocal(item.localRef.path) + loadedMedia.set(item.localRef.path, url) + } catch (e) { + logger.error('Failed to load draft gallery image', { + path: item.localRef.path, + safeMessage: e instanceof Error ? e.message : String(e), + }) + } + } + } // Load videos if (post.embedVideos) { for (const vid of post.embedVideos) { @@ -226,6 +241,12 @@ export function useDeleteDraftMutation() { await storage.deleteMediaFromLocal(img.localRef.path) } } + if (post.embedGallery) { + for (const item of post.embedGallery.items) { + if (!AppBskyDraftDefs.isDraftEmbedImage(item)) continue + await storage.deleteMediaFromLocal(item.localRef.path) + } + } if (post.embedVideos) { for (const vid of post.embedVideos) { await storage.deleteMediaFromLocal(vid.localRef.path) diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 2cc000019..98bfac779 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -70,11 +70,13 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => { const {isMobile} = useWebMediaQueries() const {altTextControlStyle, imageControlsStyle, imageStyle} = useMemo(() => { + // Cap columns at 4 so tiles stay tappable when MAX_GALLERY_IMAGES is high; + // n > 4 wraps to multiple rows via flexWrap on the gallery container. + const columns = Math.min(images.length, 4) const side = images.length === 1 ? 250 - : (containerInfo.width - IMAGE_GAP * (images.length - 1)) / - images.length + : (containerInfo.width - IMAGE_GAP * (columns - 1)) / columns const isOverflow = isMobile && images.length > 2 @@ -273,6 +275,7 @@ const styles = StyleSheet.create({ gallery: { flex: 1, flexDirection: 'row', + flexWrap: 'wrap', gap: IMAGE_GAP, marginTop: 16, }, diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index d2b9af7b8..35e170600 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -16,6 +16,7 @@ import { postUriToRelativePath, toBskyAppUrl, } from '#/lib/strings/url-helpers' +import {logger} from '#/logger' import {type ComposerImage, createInitialImages} from '#/state/gallery' import {createPostgateRecord} from '#/state/queries/postgate/util' import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate' @@ -38,6 +39,11 @@ type ImagesMedia = { images: ComposerImage[] } +type GalleryMedia = { + type: 'gallery' + images: ComposerImage[] +} + type VideoMedia = { type: 'video' video: VideoState @@ -59,7 +65,7 @@ type Link = { export type EmbedDraft = { // We'll always submit quote and actual media (images, video, gifs) chosen by the user. quote: Link | undefined - media: ImagesMedia | VideoMedia | GifMedia | undefined + media: ImagesMedia | GalleryMedia | VideoMedia | GifMedia | undefined // This field may end up ignored if we have more important things to display than a link card: link: Link | undefined } @@ -154,7 +160,31 @@ export type ComposerAction = draftId: string } -export const MAX_IMAGES = 4 +/** + * Threshold for picking between embed variants. <= this count uses the + * legacy `app.bsky.embed.images` shape; > this count promotes to + * `app.bsky.embed.gallery`. Named to flag that if/when we deprecate the + * legacy images embed entirely, this constant (and the variant split it + * gates) should go away. + */ +export const LEGACY_IMAGES_EMBED_MAX = 4 +export const MAX_GALLERY_IMAGES = 10 + +/** + * Picks the embed variant for a set of images. <=4 lands in the legacy + * `app.bsky.embed.images` shape; >4 promotes to `app.bsky.embed.gallery`. + * Anything beyond the gallery cap is dropped by the hard slice; callers + * should already have enforced the cap upstream (picker, paste, etc), + * and the reducer logs a warning when the cap is exceeded so the UI + * layer can surface a toast. + */ +function imagesToMediaVariant( + images: ComposerImage[], +): ImagesMedia | GalleryMedia { + return images.length <= LEGACY_IMAGES_EMBED_MAX + ? {type: 'images', images: images.slice(0, LEGACY_IMAGES_EMBED_MAX)} + : {type: 'gallery', images: images.slice(0, MAX_GALLERY_IMAGES)} +} export function composerReducer( state: ComposerState, @@ -337,16 +367,28 @@ function postReducer(state: PostDraft, action: PostAction): PostDraft { } const prevMedia = state.embed.media let nextMedia = prevMedia + const prevCount = + prevMedia?.type === 'images' || prevMedia?.type === 'gallery' + ? prevMedia.images.length + : 0 + const incomingCount = prevCount + action.images.length + if (incomingCount > MAX_GALLERY_IMAGES) { + // Defense in depth: callers (applyGalleryCap in Composer) should have + // already trimmed and surfaced a toast. The hard slice in + // imagesToMediaVariant still drops the excess so the cap holds. + logger.warn('composer: image add exceeds MAX_GALLERY_IMAGES', { + prevCount, + incomingCount, + dropped: incomingCount - MAX_GALLERY_IMAGES, + }) + } if (!prevMedia) { - nextMedia = { - type: 'images', - images: action.images.slice(0, MAX_IMAGES), - } - } else if (prevMedia.type === 'images') { - nextMedia = { - ...prevMedia, - images: [...prevMedia.images, ...action.images].slice(0, MAX_IMAGES), - } + nextMedia = imagesToMediaVariant(action.images) + } else if (prevMedia.type === 'images' || prevMedia.type === 'gallery') { + nextMedia = imagesToMediaVariant([ + ...prevMedia.images, + ...action.images, + ]) } return { ...state, @@ -358,7 +400,7 @@ function postReducer(state: PostDraft, action: PostAction): PostDraft { } case 'embed_update_image': { const prevMedia = state.embed.media - if (prevMedia?.type === 'images') { + if (prevMedia?.type === 'images' || prevMedia?.type === 'gallery') { const updatedImage = action.image const nextMedia = { ...prevMedia, @@ -382,19 +424,22 @@ function postReducer(state: PostDraft, action: PostAction): PostDraft { case 'embed_remove_image': { const prevMedia = state.embed.media let nextLabels = state.labels - if (prevMedia?.type === 'images') { + if (prevMedia?.type === 'images' || prevMedia?.type === 'gallery') { const removedImage = action.image - let nextMedia: ImagesMedia | undefined = { - ...prevMedia, - images: prevMedia.images.filter(img => { - return img.source.id !== removedImage.source.id - }), - } - if (nextMedia.images.length === 0) { + const remainingImages = prevMedia.images.filter(img => { + return img.source.id !== removedImage.source.id + }) + let nextMedia: ImagesMedia | GalleryMedia | undefined + if (remainingImages.length === 0) { nextMedia = undefined if (!state.embed.link) { nextLabels = [] } + } else { + // Re-pick the variant so a gallery that shrinks to <=4 demotes + // back to the legacy `app.bsky.embed.images` shape - keeps old + // clients rendering it when possible. + nextMedia = imagesToMediaVariant(remainingImages) } return { ...state, @@ -581,12 +626,9 @@ export function createComposerState({ | AppBskyActorDefs.PostInteractionSettingsPref | undefined }): ComposerState { - let media: ImagesMedia | undefined + let media: ImagesMedia | GalleryMedia | undefined if (initImageUris?.length) { - media = { - type: 'images', - images: createInitialImages(initImageUris), - } + media = imagesToMediaVariant(createInitialImages(initImageUris)) } let quote: Link | undefined if (initQuoteUri) { diff --git a/src/view/com/feeds/ComposerPrompt.tsx b/src/view/com/feeds/ComposerPrompt.tsx index b35c1c1ad..592266871 100644 --- a/src/view/com/feeds/ComposerPrompt.tsx +++ b/src/view/com/feeds/ComposerPrompt.tsx @@ -12,7 +12,7 @@ import { } from '#/lib/hooks/usePermissions' import {openCamera, openUnifiedPicker} from '#/lib/media/picker' import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' -import {MAX_IMAGES} from '#/view/com/composer/state/composer' +import {MAX_GALLERY_IMAGES} from '#/view/com/composer/state/composer' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme, web} from '#/alf' import {Button} from '#/components/Button' @@ -64,7 +64,7 @@ export function ComposerPrompt() { Keyboard.dismiss() } - const selectionCountRemaining = MAX_IMAGES + const selectionCountRemaining = MAX_GALLERY_IMAGES const {assets, canceled} = await sheetWrapper( openUnifiedPicker({selectionCountRemaining}), ) @@ -76,7 +76,7 @@ export function ComposerPrompt() { if (assets.length > 0) { const imageUris = assets .filter(asset => asset.mimeType?.startsWith('image/')) - .slice(0, MAX_IMAGES) + .slice(0, MAX_GALLERY_IMAGES) .map(asset => ({ uri: asset.uri, width: asset.width, -- 2.51.2 From f84c85aac6811e76f58b12e8880e0897f4331d66 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 00:37:42 +0300 Subject: [PATCH 34/61] Use Reanimated layout animations for Android composer (#8546) --- src/view/shell/Composer.ios.tsx | 23 ++++++++------- src/view/shell/Composer.tsx | 52 ++++++++++++++------------------- src/view/shell/Composer.web.tsx | 7 ++--- src/view/shell/index.tsx | 4 +-- src/view/shell/index.web.tsx | 2 +- 5 files changed, 38 insertions(+), 50 deletions(-) diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx index 437e610b2..7a1f59937 100644 --- a/src/view/shell/Composer.ios.tsx +++ b/src/view/shell/Composer.ios.tsx @@ -1,29 +1,30 @@ -import {useEffect, useRef} from 'react' +import {useEffect} from 'react' import {Modal, View} from 'react-native' +import {SystemBars} from 'react-native-edge-to-edge' -import {useDialogStateControlContext} from '#/state/dialogs' import {useComposerState} from '#/state/shell/composer' import {ComposePost, useComposerCancelRef} from '#/view/com/composer/Composer' import {atoms as a, useTheme} from '#/alf' import {SheetCompatProvider as TooltipSheetCompatProvider} from '#/components/Tooltip' +import {IS_LIQUID_GLASS} from '#/env' -export function Composer({}: {winHeight: number}) { - const {setFullyExpandedCount} = useDialogStateControlContext() +export function Composer() { const t = useTheme() const state = useComposerState() const ref = useComposerCancelRef() const open = !!state - const prevOpen = useRef(open) useEffect(() => { - if (open && !prevOpen.current) { - setFullyExpandedCount(c => c + 1) - } else if (!open && prevOpen.current) { - setFullyExpandedCount(c => c - 1) + if (open && !IS_LIQUID_GLASS) { + const entry = SystemBars.pushStackEntry({ + style: { + statusBar: 'light', + }, + }) + return () => SystemBars.popStackEntry(entry) } - prevOpen.current = open - }, [open, setFullyExpandedCount]) + }, [open]) return ( { - if (state) { - Animated.timing(initInterp, { - toValue: 1, - duration: 300, - easing: Easing.out(Easing.exp), - useNativeDriver: true, - }).start() - } else { - initInterp.setValue(0) + if (open) { + const entry = SystemBars.pushStackEntry({ + style: { + statusBar: t.name !== 'light' ? 'light' : 'dark', + }, + }) + return () => SystemBars.popStackEntry(entry) } - }, [initInterp, state]) - const wrapperAnimStyle = { - transform: [ - { - translateY: initInterp.interpolate({ - inputRange: [0, 1], - outputRange: [winHeight, 0], - }), - }, - ], - } - - // rendering - // = + }, [open, t.name]) - if (!state) { + if (!open) { return null } return ( - - + diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index bfce0729d..62dec9bcf 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -64,7 +64,7 @@ function ShellInner() { - + -- 2.51.2 From 2f61c0a7b4ab735bf7ace70aa85ffc09206388e1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 4 Jun 2026 16:51:34 -0500 Subject: [PATCH 35/61] Gate StandardSite subscribe button custom theme on AAA contrast (#10735) Co-authored-by: Claude Opus 4.8 --- src/alf/index.tsx | 8 +- src/alf/util/colorGeneration.test.ts | 38 ++++++++- src/alf/util/colorGeneration.ts | 42 ++++++++++ .../Post/Embed/StandardSiteEmbed/index.tsx | 84 ++++++++++++------- 4 files changed, 141 insertions(+), 31 deletions(-) diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 0cb5ceb3f..a1a2d8ada 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -9,7 +9,12 @@ import { setFontScale as persistFontScale, } from '#/alf/fonts' import {themes} from '#/alf/themes' -import {darken, lighten, rgbToHex} from '#/alf/util/colorGeneration' +import { + contrastRatio, + darken, + lighten, + rgbToHex, +} from '#/alf/util/colorGeneration' import {type Device} from '#/storage' export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf' @@ -26,6 +31,7 @@ export const utils = { rgbToHex, lighten, darken, + contrastRatio, } export type Alf = { diff --git a/src/alf/util/colorGeneration.test.ts b/src/alf/util/colorGeneration.test.ts index c4a2b0bbb..8d330201e 100644 --- a/src/alf/util/colorGeneration.test.ts +++ b/src/alf/util/colorGeneration.test.ts @@ -1,4 +1,10 @@ -import {darken, hexToRgb, lighten, rgbToHex} from './colorGeneration' +import { + contrastRatio, + darken, + hexToRgb, + lighten, + rgbToHex, +} from './colorGeneration' describe('hexToRgb', () => { it('parses 6-digit hex', () => { @@ -92,3 +98,33 @@ describe('lighten / darken', () => { expect(darken('#zzz', 10)).toBe('#zzz') }) }) + +describe('contrastRatio', () => { + it('returns 21 for black on white', () => { + expect(contrastRatio('#000000', '#ffffff')).toBeCloseTo(21, 5) + }) + + it('returns 1 for identical colors', () => { + expect(contrastRatio('#abcdef', '#abcdef')).toBeCloseTo(1, 5) + }) + + it('is symmetric regardless of argument order', () => { + expect(contrastRatio('#123456', '#fedcba')).toBeCloseTo( + contrastRatio('#fedcba', '#123456')!, + 5, + ) + }) + + it('clears AAA large text (4.5:1) for a high-contrast pairing', () => { + expect(contrastRatio('#1d3a5f', '#ffffff')!).toBeGreaterThanOrEqual(4.5) + }) + + it('fails AAA large text (4.5:1) for a low-contrast pairing', () => { + expect(contrastRatio('#777777', '#888888')!).toBeLessThan(4.5) + }) + + it('returns null for invalid hex input', () => { + expect(contrastRatio('not-a-color', '#ffffff')).toBeNull() + expect(contrastRatio('#ffffff', '#zzz')).toBeNull() + }) +}) diff --git a/src/alf/util/colorGeneration.ts b/src/alf/util/colorGeneration.ts index 85659af25..f3be07d50 100644 --- a/src/alf/util/colorGeneration.ts +++ b/src/alf/util/colorGeneration.ts @@ -72,6 +72,48 @@ export function rgbToHex(r: number, g: number, b: number): string { .slice(1)}` } +/** + * Computes the WCAG contrast ratio between two colors, ranging from 1 (no + * contrast) to 21 (maximum contrast, i.e. black on white). Returns null if + * either argument is not a valid hex color. + * + * @see https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio + */ +export function contrastRatio(hexA: string, hexB: string): number | null { + const rgbA = hexToRgb(hexA) + const rgbB = hexToRgb(hexB) + if (!rgbA || !rgbB) return null + const luminanceA = relativeLuminance(rgbA) + const luminanceB = relativeLuminance(rgbB) + const lighter = Math.max(luminanceA, luminanceB) + const darker = Math.min(luminanceA, luminanceB) + return (lighter + 0.05) / (darker + 0.05) +} + +/** + * Computes the WCAG relative luminance of an RGB color, ranging from 0 (black) + * to 1 (white). + * + * @see https://www.w3.org/TR/WCAG21/#dfn-relative-luminance + */ +function relativeLuminance({ + r, + g, + b, +}: { + r: number + g: number + b: number +}): number { + const toLinear = (channel: number) => { + const normalized = channel / 255 + return normalized <= 0.03928 + ? normalized / 12.92 + : ((normalized + 0.055) / 1.055) ** 2.4 + } + return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b) +} + function rgbToHsl( r: number, g: number, diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index c7b8abf88..2c38f3806 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -427,6 +427,26 @@ export function SubscribeButton({ ? l`Subscribe on ${highlightedPublisher.name}` : l`View publication` + /* + * The custom site theme paints the button background with `accent` and the + * text with `accentForeground`. Only honor it when that pairing clears WCAG + * AAA (4.5:1) for large text, which the button's bold label qualifies as. + * Otherwise we fall through to the default `secondary_inverted` styling, + * which is guaranteed to be legible. + */ + const {accentRGB, accentForegroundRGB} = view.source?.theme || {} + let useCustomTheme = false + if (accentRGB && accentForegroundRGB) { + const accent = utils.rgbToHex(accentRGB.r, accentRGB.g, accentRGB.b) + const accentForeground = utils.rgbToHex( + accentForegroundRGB.r, + accentForegroundRGB.g, + accentForegroundRGB.b, + ) + const ratio = utils.contrastRatio(accent, accentForeground) + useCustomTheme = ratio !== null && ratio >= 4.5 + } + if (!view.source) return null const publicationTitle = view.source.title @@ -468,36 +488,42 @@ export function SubscribeButton({ } } + const button = ( + + {highlightedPublisher ? ( + <> + + + + {cta} + + ) : ( + <> + {cta} + + + )} + + ) + + if (!useCustomTheme) { + return button + } + return ( - - - {highlightedPublisher ? ( - <> - - - - {cta} - - ) : ( - <> - {cta} - - - )} - - + {button} ) } -- 2.51.2 From 68da7971806182cb99856c21954836b6facff0a9 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Thu, 4 Jun 2026 22:19:35 -0400 Subject: [PATCH 36/61] Add OTA fallback for gallery embed (#10734) Co-authored-by: Eric Bailey --- .../Post/Embed/GalleryFallbackEmbed.tsx | 99 +++++++++++++++++++ .../screens/Storybook/GalleryFallback.tsx | 25 +++++ src/view/screens/Storybook/Storybook.tsx | 2 + 3 files changed, 126 insertions(+) create mode 100644 src/components/Post/Embed/GalleryFallbackEmbed.tsx create mode 100644 src/view/screens/Storybook/GalleryFallback.tsx diff --git a/src/components/Post/Embed/GalleryFallbackEmbed.tsx b/src/components/Post/Embed/GalleryFallbackEmbed.tsx new file mode 100644 index 000000000..38d7e4d01 --- /dev/null +++ b/src/components/Post/Embed/GalleryFallbackEmbed.tsx @@ -0,0 +1,99 @@ +import {Linking, View} from 'react-native' +import {plural} from '@lingui/core/macro' +import {Trans, useLingui} from '@lingui/react/macro' + +import {BSKY_DOWNLOAD_URL} from '#/lib/constants' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {Sparkle_Stroke2_Corner0_Rounded as Sparkle} from '#/components/icons/Sparkle' +import {Text} from '#/components/Typography' +import {IS_NATIVE} from '#/env' + +/** + * OTA-able fallback that ships to native builds which don't yet know how to + * render the new gallery embed (>4 images, Photos v2). Final copy and visual + * treatment pending design from Darrin/Danielle/Alex. + * + * Native-only per APP-2308 - web builds receive the new gallery support in + * the same release that adds it. + */ +export function GalleryFallbackEmbed({count}: {count?: number}) { + const t = useTheme() + const {t: l} = useLingui() + + const bodyStyle = [ + a.text_sm, + a.text_center, + a.leading_snug, + t.atoms.text_contrast_high, + ] + + return ( + + + + Something new is here + + {count ? ( + + + {plural(count, { + one: 'This post has # photo.', + other: 'This post has # photos.', + })} + + {IS_NATIVE ? ( + + {plural(count, { + one: 'Update your app to see it.', + other: 'Update your app to see them all.', + })} + + ) : ( + + {plural(count, { + one: 'Refresh the page to see it.', + other: 'Refresh the page to see them all.', + })} + + )} + + ) : IS_NATIVE ? ( + + Update your app to see it. + + ) : ( + + Refresh the page to see it. + + )} + {IS_NATIVE && ( + + )} + + ) +} diff --git a/src/view/screens/Storybook/GalleryFallback.tsx b/src/view/screens/Storybook/GalleryFallback.tsx new file mode 100644 index 000000000..7cca087db --- /dev/null +++ b/src/view/screens/Storybook/GalleryFallback.tsx @@ -0,0 +1,25 @@ +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {GalleryFallbackEmbed} from '#/components/Post/Embed/GalleryFallbackEmbed' +import {H1, H3} from '#/components/Typography' + +export function GalleryFallback() { + return ( + +

Gallery fallback (APP-2308)

+ +

No count

+ + +

1 photo

+ + +

5 photos

+ + +

10 photos

+ +
+ ) +} diff --git a/src/view/screens/Storybook/Storybook.tsx b/src/view/screens/Storybook/Storybook.tsx index 8fb85e8e0..33c31a9ee 100644 --- a/src/view/screens/Storybook/Storybook.tsx +++ b/src/view/screens/Storybook/Storybook.tsx @@ -16,6 +16,7 @@ import {Breakpoints} from './Breakpoints' import {Buttons} from './Buttons' import {Dialogs} from './Dialogs' import {Forms} from './Forms' +import {GalleryFallback} from './GalleryFallback' import {Icons} from './Icons' import {Links} from './Links' import {Menus} from './Menus' @@ -120,6 +121,7 @@ export default function Storybook() { +
) break } case Step.GENERATE: { const linkEnabled = joinLink?.enabledStatus === 'enabled' const linkHasChanged = linkEnabled && joinLinkRuleKey !== whoCanJoin header = linkEnabled ? l`Update invite link` : l`Generate invite link` content = ( <> Choose who can join this group chat and how. setWhoCanJoin(value)}> {whoCanJoinOptions.map(option => ( {({selected}) => ( )} ))} ) break } case Step.MANAGE: { const linkEnabled = joinLink?.enabledStatus === 'enabled' const linkDisabled = joinLink?.enabledStatus === 'disabled' const joinLinkURI = joinLink?.code ? `https://bsky.app/c/${joinLink.code}` : 'https://bsky.app/' const createdAt = joinLink ? new Date(joinLink.createdAt) : null const currentOption = whoCanJoinOptions.find( o => o.name === (joinLink ? joinLinkToKey(joinLink) : null), ) ?? whoCanJoinOptions[0] const ownerValue = currentOption?.owner ?? whoCanJoinOptions[0].owner const memberValue = currentOption?.member ?? whoCanJoinOptions[0].member header = linkEnabled ? l`Invite link` : l`Invite link disabled` content = ( <> {joinLinkURI} {createdAt ? ( Created{' '} {i18n.date(createdAt, { dateStyle: 'long', timeStyle: 'short', })} ) : null} {linkEnabled ? ( {isOwner ? ( setStep(Step.GENERATE)}> {ownerValue} ) : ( {memberValue} )} ) : null} {linkEnabled ? ( {isOwner ? ( setStep(Step.CONFIRM_DISABLE)}> Disable ) : null} { control.close(() => { openComposer({ text: joinLinkURI, logContext: 'Other', }) }) }}> Post link { void shareUrl(joinLinkURI) }}> Share ) : ( )} ) break } case Step.CONFIRM_DISABLE: { content = ( <> Disable this invite link? Anyone who has it will no longer be able to join or request to join. You can always create a new one. ) break } } if (!isOwner && (!joinLink || joinLink.enabledStatus === 'disabled')) { header = l`Invite link` content = ( <> There is no invite link for this group chat. ) } return ( { setStep(defaultStep) setWhoCanJoin(defaultWhoCanJoin) }}> {header}
} label={l`Group chat invite link dialog`} style={web({maxWidth: 400})}> {content} ) } function joinLinkToKey(joinLink: ChatBskyGroupDefs.JoinLinkView): string { return `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}` } function keyToJoinLink( key: string, ): Pick { const [joinRule, requireApproval] = key.split(':') return { joinRule, requireApproval: requireApproval === 'requireApproval', } } -#: src/screens/Messages/components/InviteLinkDialog.tsx:179 -msgid "Group chats can only have a maximum of {0}." -msgstr "Group chats can only have a maximum of {0}." +#. placeholder {0}: convo.details.memberLimit +#: src/screens/Messages/components/InviteLinkDialog.tsx:178 +msgid "Group chats can only have a maximum of {0, plural, other {# people}}." +msgstr "Group chats can only have a maximum of {0, plural, other {# people}}." #: src/components/dialogs/SearchablePeopleList.tsx:569 msgid "Group is locked" @@ -5586,7 +5632,7 @@ msgstr "" msgid "Help" msgstr "" -#: src/screens/Onboarding/StepProfile/index.tsx:261 +#: src/screens/Onboarding/StepProfile/index.tsx:262 msgid "Help people know you're not a bot by uploading a picture or creating an avatar." msgstr "" @@ -5759,14 +5805,14 @@ msgstr "" msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!" msgstr "" -#: src/screens/Messages/Conversation.tsx:337 +#: src/screens/Messages/Conversation.tsx:362 msgid "Hold your horses! This feature isn't available to you yet. Please check back later." msgstr "Hold your horses! This feature isn't available to you yet. Please check back later." #: src/Navigation.tsx:740 #: src/Navigation.tsx:761 -#: src/view/shell/bottom-bar/BottomBar.tsx:184 -#: src/view/shell/desktop/LeftNav.tsx:666 +#: src/view/shell/bottom-bar/BottomBar.tsx:185 +#: src/view/shell/desktop/LeftNav.tsx:667 #: src/view/shell/Drawer.tsx:443 msgid "Home" msgstr "" @@ -5824,7 +5870,7 @@ msgstr "" msgid "I'm on Bluesky as {0} - come find me! https://bsky.app/download" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:242 +#: src/components/Lightbox/Lightbox.web.tsx:246 msgid "If alt text is long, toggles alt text expanded state" msgstr "" @@ -5889,32 +5935,32 @@ msgid "If you're trying to change your handle or email, do so before you deactiv msgstr "" #. Ignore a request to join a chat -#: src/screens/Messages/JoinRequests.tsx:485 +#: src/screens/Messages/JoinRequests.tsx:484 msgctxt "button" msgid "Ignore" msgstr "Ignore" -#: src/screens/Messages/JoinRequests.tsx:479 +#: src/screens/Messages/JoinRequests.tsx:478 msgid "Ignore join request" msgstr "Ignore join request" -#: src/components/images/ImageLayoutGridItem.tsx:93 +#: src/components/images/ImageLayoutGridItem.tsx:96 msgid "Image" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:443 +#: src/components/images/Gallery/index.tsx:451 msgid "Image {0}" msgstr "Image {0}" #. placeholder {0}: index + 1 #. placeholder {1}: imgs.length -#: src/components/Lightbox/Lightbox.web.tsx:257 +#: src/components/Lightbox/Lightbox.web.tsx:261 msgid "Image {0} of {1}" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:428 +#: src/components/images/Gallery/index.tsx:436 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5944,12 +5990,12 @@ msgid "Image is unavailable." msgstr "Image is unavailable." #: src/components/Lightbox/chrome/ImageMenu.tsx:73 -#: src/components/Lightbox/Lightbox.web.tsx:261 -#: src/components/Lightbox/Lightbox.web.tsx:269 +#: src/components/Lightbox/Lightbox.web.tsx:265 +#: src/components/Lightbox/Lightbox.web.tsx:273 msgid "Image options" msgstr "Image options" -#: src/components/Lightbox/Lightbox.web.tsx:312 +#: src/components/Lightbox/Lightbox.web.tsx:316 #: src/lib/media/save-image.ios.ts:25 #: src/lib/media/save-image.ts:29 msgid "Image saved" @@ -6075,7 +6121,7 @@ msgstr "" msgid "Invalid 2FA confirmation code." msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:126 +#: src/components/intents/GroupChatJoinDialog.tsx:127 msgid "Invalid group chat code." msgstr "Invalid group chat code." @@ -6097,7 +6143,7 @@ msgstr "" msgid "Invalid report subject" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:157 +#: src/components/intents/GroupChatJoinDialog.tsx:158 msgid "Invalid rescind request." msgstr "Invalid rescind request." @@ -6130,10 +6176,10 @@ msgstr "" msgid "Invite friends <0/>" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:167 -#: src/screens/Messages/components/InviteLinkDialog.tsx:309 -#: src/screens/Messages/components/InviteLinkDialog.tsx:315 -#: src/screens/Messages/components/InviteLinkDialog.tsx:482 +#: src/screens/Messages/components/InviteLinkDialog.tsx:166 +#: src/screens/Messages/components/InviteLinkDialog.tsx:304 +#: src/screens/Messages/components/InviteLinkDialog.tsx:310 +#: src/screens/Messages/components/InviteLinkDialog.tsx:477 #: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:148 #: src/screens/Messages/ConversationSettings/index.tsx:490 msgid "Invite link" @@ -6144,7 +6190,7 @@ msgid "Invite link created" msgstr "Invite link created" #: src/components/dms/getSystemMessageInfo.ts:138 -#: src/screens/Messages/components/InviteLinkDialog.tsx:309 +#: src/screens/Messages/components/InviteLinkDialog.tsx:304 msgid "Invite link disabled" msgstr "Invite link disabled" @@ -6194,7 +6240,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:2386 +#: src/view/com/composer/Composer.tsx:2452 msgid "Job ID: {0}" msgstr "" @@ -6203,7 +6249,8 @@ msgstr "" msgid "Jobs" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:253 +#: src/components/dms/ChatInvite/Root.tsx:98 +#: src/components/intents/GroupChatJoinDialog.tsx:254 msgid "Join" msgstr "Join" @@ -6215,11 +6262,11 @@ msgid "Join Bluesky" msgstr "" #: src/components/intents/GroupChatJoinDialog.tsx:63 -#: src/components/intents/GroupChatJoinDialog.tsx:415 +#: src/components/intents/GroupChatJoinDialog.tsx:419 msgid "Join group chat" msgstr "Join group chat" -#: src/components/intents/GroupChatJoinDialog.tsx:146 +#: src/components/intents/GroupChatJoinDialog.tsx:147 msgid "Join request rescinded." msgstr "Join request rescinded." @@ -6228,16 +6275,12 @@ msgstr "Join request rescinded." msgid "Join the conversation" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:420 -msgid "Join this group chat" -msgstr "Join this group chat" - #: src/lib/interests.ts:63 msgid "Journalism" msgstr "" -#: src/view/com/composer/Composer.tsx:1324 -#: src/view/com/composer/Composer.tsx:1334 +#: src/view/com/composer/Composer.tsx:1393 +#: src/view/com/composer/Composer.tsx:1403 #: src/view/com/composer/drafts/DraftsButton.tsx:135 msgid "Keep editing" msgstr "" @@ -6538,7 +6581,7 @@ msgstr "" msgid "Linear" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:296 +#: src/components/Lightbox/Lightbox.web.tsx:300 msgid "Link copied to clipboard" msgstr "Link copied to clipboard" @@ -6632,7 +6675,7 @@ msgstr "" #: src/view/screens/Lists.tsx:60 #: src/view/screens/Profile.tsx:233 #: src/view/screens/Profile.tsx:241 -#: src/view/shell/desktop/LeftNav.tsx:721 +#: src/view/shell/desktop/LeftNav.tsx:722 #: src/view/shell/Drawer.tsx:548 msgid "Lists" msgstr "" @@ -6900,7 +6943,7 @@ msgstr "" msgid "Message {displayName}" msgstr "Message {displayName}" -#: src/screens/Messages/components/ChatListItem.tsx:325 +#: src/screens/Messages/components/ChatListItem.tsx:330 msgid "Message deleted" msgstr "" @@ -6909,7 +6952,7 @@ msgctxt "toast" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageItem.tsx:522 +#: src/components/dms/MessageItem.tsx:535 msgid "Message failed to send." msgstr "Message failed to send." @@ -6946,11 +6989,11 @@ msgstr "" msgid "Messages" msgstr "" -#: src/components/dms/MessageItem.tsx:621 +#: src/components/dms/MessageItem.tsx:634 msgid "Messages from this person are hidden while they are blocking you." msgstr "Messages from this person are hidden while they are blocking you." -#: src/components/dms/MessageItem.tsx:616 +#: src/components/dms/MessageItem.tsx:629 msgid "Messages from this person are hidden while you are blocking them." msgstr "Messages from this person are hidden while you are blocking them." @@ -7344,7 +7387,7 @@ msgid "New post" msgstr "" #: src/view/com/feeds/FeedPage.tsx:179 -#: src/view/shell/desktop/LeftNav.tsx:597 +#: src/view/shell/desktop/LeftNav.tsx:598 msgctxt "action" msgid "New post" msgstr "" @@ -7479,7 +7522,7 @@ msgstr "" msgid "No media yet" msgstr "" -#: src/screens/Messages/components/ChatListItem.tsx:311 +#: src/screens/Messages/components/ChatListItem.tsx:316 msgid "No messages yet" msgstr "" @@ -7680,8 +7723,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:197 #: src/screens/Settings/Settings.tsx:200 #: src/view/screens/Notifications.tsx:128 -#: src/view/shell/bottom-bar/BottomBar.tsx:260 -#: src/view/shell/desktop/LeftNav.tsx:686 +#: src/view/shell/bottom-bar/BottomBar.tsx:261 +#: src/view/shell/desktop/LeftNav.tsx:687 #: src/view/shell/Drawer.tsx:496 msgid "Notifications" msgstr "" @@ -7724,7 +7767,7 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 #: src/components/dms/InitiateChatFlow.tsx:677 -#: src/components/dms/MessageItem.tsx:628 +#: src/components/dms/MessageItem.tsx:641 #: src/screens/Login/PasswordUpdatedForm.tsx:37 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:661 msgid "Okay" @@ -7757,27 +7800,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:787 +#: src/view/com/composer/Composer.tsx:853 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:784 +#: src/view/com/composer/Composer.tsx:850 msgid "One or more images is missing alt text." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:415 +#: src/view/com/composer/SelectMediaButton.tsx:417 msgid "One or more of your selected files are not supported." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:438 +#: src/view/com/composer/SelectMediaButton.tsx:440 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:589 +#: src/view/com/composer/Composer.tsx:648 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:794 +#: src/view/com/composer/Composer.tsx:860 msgid "One or more videos is missing alt text." msgstr "" @@ -7786,6 +7829,14 @@ msgstr "" msgid "Only {0} can reply." msgstr "" +#. Toast shown when adding images would exceed the post gallery cap; only the first N are kept +#. placeholder {0}: result.accepted.length +#. placeholder {1}: next.length +#. placeholder {2}: next.length +#: src/view/com/composer/Composer.tsx:223 +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}" + #: src/screens/Messages/JoinRequests.tsx:196 msgid "Only admins can accept join requests." msgstr "Only admins can accept join requests." @@ -7794,7 +7845,7 @@ msgstr "Only admins can accept join requests." msgid "Only admins can ignore join requests." msgstr "Only admins can ignore join requests." -#: src/components/intents/GroupChatJoinDialog.tsx:124 +#: src/components/intents/GroupChatJoinDialog.tsx:125 msgid "Only followers can join this group chat." msgstr "Only followers can join this group chat." @@ -7813,7 +7864,8 @@ msgstr "" msgid "Only people {0} follows can join." msgstr "Only people {0} follows can join." -#: src/components/intents/GroupChatJoinDialog.tsx:268 +#: src/components/dms/ChatInvite/Root.tsx:113 +#: src/components/intents/GroupChatJoinDialog.tsx:269 msgid "Only people the chat owner follows can join" msgstr "Only people the chat owner follows can join" @@ -7834,7 +7886,7 @@ msgstr "" msgid "Oops!" msgstr "" -#: src/screens/Onboarding/StepProfile/index.tsx:310 +#: src/screens/Onboarding/StepProfile/index.tsx:311 msgid "Open avatar creator" msgstr "" @@ -7842,12 +7894,17 @@ msgstr "" msgid "Open camera" msgstr "" +#: src/components/dms/ChatInvite/Root.tsx:85 +#: src/components/intents/GroupChatJoinDialog.tsx:408 +msgid "Open chat" +msgstr "Open chat" + #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:139 msgid "Open chat member options for {displayName}" msgstr "Open chat member options for {displayName}" -#: src/screens/Messages/components/ChatListItem.tsx:486 -#: src/screens/Messages/components/ChatListItem.tsx:490 +#: src/screens/Messages/components/ChatListItem.tsx:491 +#: src/screens/Messages/components/ChatListItem.tsx:495 msgid "Open conversation options" msgstr "" @@ -7861,7 +7918,7 @@ msgstr "" #: src/screens/Messages/components/MessageComposer.tsx:176 #: src/screens/Messages/components/MessageInput.web.tsx:148 -#: src/view/com/composer/Composer.tsx:2026 +#: src/view/com/composer/Composer.tsx:2092 msgid "Open emoji picker" msgstr "" @@ -7882,7 +7939,7 @@ msgstr "" msgid "Open Germ DM" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:399 +#: src/components/intents/GroupChatJoinDialog.tsx:401 msgid "Open group chat" msgstr "Open group chat" @@ -7943,7 +8000,7 @@ msgstr "" msgid "Open system log" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:400 +#: src/components/intents/GroupChatJoinDialog.tsx:402 msgid "Open this group chat" msgstr "Open this group chat" @@ -7968,7 +8025,7 @@ msgstr "" msgid "Opens alt text dialog" msgstr "" -#: src/view/com/composer/photos/OpenCameraBtn.tsx:71 +#: src/view/com/composer/photos/OpenCameraBtn.tsx:70 msgid "Opens camera on device" msgstr "" @@ -7988,10 +8045,10 @@ msgstr "" msgid "Opens device camera" msgstr "" -#. Accessibility hint for button in composer to add images, a video, or a GIF to a post. Maximum number of images that can be selected is currently 4 but may change. -#: src/view/com/composer/SelectMediaButton.tsx:509 -msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." -msgstr "" +#. Accessibility hint for button in composer to add images, a video, or a GIF to a post. +#: src/view/com/composer/SelectMediaButton.tsx:511 +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." #: src/screens/Messages/JoinRequest.tsx:295 #: src/view/com/auth/SplashScreen.tsx:102 @@ -8005,7 +8062,7 @@ msgstr "" msgid "Opens flow to sign in to your existing Bluesky account" msgstr "" -#: src/components/images/Gallery/index.tsx:444 +#: src/components/images/Gallery/index.tsx:452 msgid "Opens full image" msgstr "Opens full image" @@ -8022,7 +8079,7 @@ msgstr "" msgid "Opens link {0}" msgstr "" -#: src/view/com/util/UserAvatar.tsx:600 +#: src/view/com/util/UserAvatar.tsx:603 msgid "Opens live status dialog" msgstr "" @@ -8051,9 +8108,9 @@ msgstr "" msgid "Opens this draft in the composer" msgstr "" -#: src/components/dms/MessageItem.tsx:229 +#: src/components/dms/MessageItem.tsx:233 #: src/view/com/notifications/NotificationFeedItem.tsx:1019 -#: src/view/com/util/UserAvatar.tsx:618 +#: src/view/com/util/UserAvatar.tsx:621 msgid "Opens this profile" msgstr "" @@ -8197,11 +8254,11 @@ msgstr "" msgid "People" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:154 +#: src/screens/Messages/components/InviteLinkDialog.tsx:153 msgid "People {ownerName} follows can join instantly" msgstr "People {ownerName} follows can join instantly" -#: src/screens/Messages/components/InviteLinkDialog.tsx:159 +#: src/screens/Messages/components/InviteLinkDialog.tsx:158 msgid "People {ownerName} follows can request to join" msgstr "People {ownerName} follows can request to join" @@ -8220,11 +8277,11 @@ msgstr "" msgid "People I follow" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:153 +#: src/screens/Messages/components/InviteLinkDialog.tsx:152 msgid "People I follow can join instantly" msgstr "People I follow can join instantly" -#: src/screens/Messages/components/InviteLinkDialog.tsx:158 +#: src/screens/Messages/components/InviteLinkDialog.tsx:157 msgid "People I follow can request to join" msgstr "People I follow can request to join" @@ -8512,7 +8569,7 @@ msgstr "" msgid "Porn" msgstr "" -#: src/view/com/composer/Composer.tsx:1675 +#: src/view/com/composer/Composer.tsx:1740 msgctxt "action" msgid "Post" msgstr "" @@ -8532,12 +8589,12 @@ msgstr "" msgid "Post a video" msgstr "" -#: src/view/com/composer/Composer.tsx:1673 +#: src/view/com/composer/Composer.tsx:1738 msgctxt "action" msgid "Post All" msgstr "" -#: src/view/com/composer/Composer.tsx:1333 +#: src/view/com/composer/Composer.tsx:1402 msgid "Post anyway" msgstr "Post anyway" @@ -8558,7 +8615,7 @@ msgctxt "toast" msgid "Post deleted" msgstr "" -#: src/lib/api/index.ts:186 +#: src/lib/api/index.ts:189 msgid "Post failed to upload. Please check your Internet connection and try again." msgstr "" @@ -8593,8 +8650,8 @@ msgstr "" msgid "Post language selection" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:371 -#: src/screens/Messages/components/InviteLinkDialog.tsx:383 +#: src/screens/Messages/components/InviteLinkDialog.tsx:366 +#: src/screens/Messages/components/InviteLinkDialog.tsx:378 msgid "Post link" msgstr "Post link" @@ -8710,15 +8767,15 @@ msgstr "" msgid "Privacy violation of a minor" msgstr "" -#: src/view/com/composer/Composer.tsx:2460 +#: src/view/com/composer/Composer.tsx:2526 msgid "Processing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2462 +#: src/view/com/composer/Composer.tsx:2528 msgid "Processing video..." msgstr "" -#: src/lib/api/index.ts:60 +#: src/lib/api/index.ts:62 msgid "Processing..." msgstr "" @@ -8726,8 +8783,8 @@ msgstr "" msgid "profile" msgstr "" -#: src/view/shell/bottom-bar/BottomBar.tsx:303 -#: src/view/shell/desktop/LeftNav.tsx:744 +#: src/view/shell/bottom-bar/BottomBar.tsx:304 +#: src/view/shell/desktop/LeftNav.tsx:745 #: src/view/shell/Drawer.tsx:80 #: src/view/shell/Drawer.tsx:599 msgid "Profile" @@ -8759,22 +8816,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:1659 +#: src/view/com/composer/Composer.tsx:1724 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1654 +#: src/view/com/composer/Composer.tsx:1719 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:1643 +#: src/view/com/composer/Composer.tsx:1708 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:1648 +#: src/view/com/composer/Composer.tsx:1713 msgid "Publish reply" msgstr "" @@ -8853,11 +8910,11 @@ msgstr "" msgid "Re-attach quote" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:401 +#: src/screens/Messages/components/InviteLinkDialog.tsx:396 msgid "Re-enable invite link" msgstr "Re-enable invite link" -#: src/screens/Messages/components/InviteLinkDialog.tsx:408 +#: src/screens/Messages/components/InviteLinkDialog.tsx:403 msgid "Re-enable link" msgstr "Re-enable link" @@ -8961,6 +9018,10 @@ msgstr "" msgid "Reconnect" msgstr "" +#: src/components/Post/Embed/GalleryFallbackEmbed.tsx:80 +msgid "Refresh the page to see it." +msgstr "Refresh the page to see it." + #. Reject a chat request, this opens a menu with options #: src/screens/Messages/components/RequestButtons.tsx:131 msgid "Reject" @@ -9016,17 +9077,17 @@ msgstr "" msgid "Remove all contacts" msgstr "" -#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:19 +#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:18 msgid "Remove attachment" msgstr "" -#: src/view/com/util/UserAvatar.tsx:520 #: src/view/com/util/UserAvatar.tsx:523 +#: src/view/com/util/UserAvatar.tsx:526 msgid "Remove Avatar" msgstr "" -#: src/view/com/util/UserBanner.tsx:190 #: src/view/com/util/UserBanner.tsx:193 +#: src/view/com/util/UserBanner.tsx:196 msgid "Remove Banner" msgstr "" @@ -9034,8 +9095,9 @@ msgstr "" msgid "Remove caption file" msgstr "Remove caption file" -#: src/screens/Messages/components/MessageInputEmbed.tsx:192 -#: src/screens/Messages/components/MessageInputEmbed.tsx:248 +#: src/screens/Messages/components/MessageInputEmbed.tsx:234 +#: src/screens/Messages/components/MessageInputEmbed.tsx:289 +#: src/screens/Messages/components/MessageInputEmbed.tsx:355 msgid "Remove embed" msgstr "" @@ -9079,7 +9141,7 @@ msgstr "" msgid "Remove from your feeds?" msgstr "" -#: src/view/com/composer/photos/Gallery.tsx:225 +#: src/view/com/composer/photos/Gallery.tsx:227 msgid "Remove image" msgstr "" @@ -9129,11 +9191,11 @@ msgstr "" msgid "Remove your verification for this account?" msgstr "" -#: src/components/Post/Embed/index.tsx:225 +#: src/components/Post/Embed/index.tsx:244 msgid "Removed by author" msgstr "" -#: src/components/Post/Embed/index.tsx:223 +#: src/components/Post/Embed/index.tsx:242 msgid "Removed by you" msgstr "" @@ -9223,7 +9285,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1671 +#: src/view/com/composer/Composer.tsx:1736 msgctxt "action" msgid "Reply" msgstr "" @@ -9408,14 +9470,10 @@ msgstr "" msgid "Reposts of your reposts" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:414 +#: src/components/intents/GroupChatJoinDialog.tsx:418 msgid "Request access to group chat" msgstr "Request access to group chat" -#: src/components/intents/GroupChatJoinDialog.tsx:419 -msgid "Request access to join this group chat" -msgstr "Request access to join this group chat" - #: src/screens/Messages/JoinRequests.tsx:178 msgid "Request approved." msgstr "Request approved." @@ -9429,10 +9487,15 @@ msgstr "" msgid "Request ignored." msgstr "Request ignored." -#: src/components/intents/GroupChatJoinDialog.tsx:252 +#: src/components/dms/ChatInvite/Root.tsx:98 +#: src/components/intents/GroupChatJoinDialog.tsx:253 msgid "Request to join" msgstr "Request to join" +#: src/components/dms/ChatInvite/Root.tsx:117 +msgid "Requested" +msgstr "Requested" + #. Incoming message requests #: src/screens/Messages/components/InboxRequests.tsx:24 msgid "Requests" @@ -9440,7 +9503,7 @@ msgstr "Requests" #: src/Navigation.tsx:495 #: src/screens/Messages/JoinRequests.tsx:58 -#: src/screens/Messages/JoinRequests.tsx:421 +#: src/screens/Messages/JoinRequests.tsx:420 msgid "Requests to join" msgstr "Requests to join" @@ -9461,7 +9524,7 @@ msgstr "" msgid "Required in your region" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:272 +#: src/components/intents/GroupChatJoinDialog.tsx:273 msgid "Rescind request" msgstr "Rescind request" @@ -9617,28 +9680,28 @@ msgstr "" #: src/screens/SavedFeeds.tsx:124 #: src/screens/SavedFeeds.tsx:311 #: src/screens/SavedFeeds.tsx:315 -#: src/view/com/composer/Composer.tsx:1314 +#: src/view/com/composer/Composer.tsx:1383 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save changes" msgstr "" -#: src/view/com/composer/Composer.tsx:1286 +#: src/view/com/composer/Composer.tsx:1355 #: src/view/com/composer/drafts/DraftsButton.tsx:93 msgid "Save changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1314 +#: src/view/com/composer/Composer.tsx:1383 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save draft" msgstr "" -#: src/view/com/composer/Composer.tsx:1288 +#: src/view/com/composer/Composer.tsx:1357 #: src/view/com/composer/drafts/DraftsButton.tsx:95 msgid "Save draft?" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:99 -#: src/components/MediaPreview.tsx:197 +#: src/components/MediaPreview.tsx:229 #: src/components/Post/Embed/ImageContextMenu.tsx:70 #: src/components/StarterPack/ShareDialog.tsx:144 #: src/components/StarterPack/ShareDialog.tsx:150 @@ -9663,7 +9726,7 @@ msgstr "" msgid "Save to my feeds" msgstr "" -#: src/view/shell/desktop/LeftNav.tsx:731 +#: src/view/shell/desktop/LeftNav.tsx:732 #: src/view/shell/Drawer.tsx:574 msgctxt "link to bookmarks screen" msgid "Saved" @@ -9721,7 +9784,7 @@ msgstr "" #: src/components/forms/SearchInput.tsx:53 #: src/screens/Search/Shell.tsx:362 #: src/screens/Search/Shell.tsx:525 -#: src/view/shell/bottom-bar/BottomBar.tsx:204 +#: src/view/shell/bottom-bar/BottomBar.tsx:205 msgid "Search" msgstr "" @@ -10048,7 +10111,7 @@ msgstr "" msgid "Select your preferred notification channels" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:418 +#: src/view/com/composer/SelectMediaButton.tsx:420 msgid "Selecting multiple media types is not supported." msgstr "" @@ -10079,7 +10142,7 @@ msgstr "Send error report" msgid "Send feedback" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:284 +#: src/screens/Messages/components/MessageComposer.tsx:288 #: src/screens/Messages/components/MessageInput.web.tsx:225 msgid "Send message" msgstr "" @@ -10157,7 +10220,7 @@ msgstr "" #: src/Navigation.tsx:212 #: src/screens/Settings/Settings.tsx:98 -#: src/view/shell/desktop/LeftNav.tsx:754 +#: src/view/shell/desktop/LeftNav.tsx:755 #: src/view/shell/Drawer.tsx:612 msgid "Settings" msgstr "" @@ -10219,12 +10282,12 @@ msgstr "" msgid "Sexually Suggestive" msgstr "" -#: src/components/MediaPreview.tsx:203 +#: src/components/MediaPreview.tsx:235 #: src/components/Post/Embed/ImageContextMenu.tsx:74 #: src/components/StarterPack/QrCodeDialog.tsx:195 #: src/screens/Hashtag.tsx:130 -#: src/screens/Messages/components/InviteLinkDialog.tsx:387 -#: src/screens/Messages/components/InviteLinkDialog.tsx:394 +#: src/screens/Messages/components/InviteLinkDialog.tsx:382 +#: src/screens/Messages/components/InviteLinkDialog.tsx:389 #: src/screens/StarterPack/StarterPackScreen.tsx:447 #: src/screens/Topic.tsx:90 msgid "Share" @@ -10240,8 +10303,8 @@ msgid "Share author DID" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:94 -#: src/components/Lightbox/Lightbox.web.tsx:277 -#: src/components/Lightbox/Lightbox.web.tsx:303 +#: src/components/Lightbox/Lightbox.web.tsx:281 +#: src/components/Lightbox/Lightbox.web.tsx:307 msgid "Share image" msgstr "Share image" @@ -10446,10 +10509,10 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:124 #: src/view/com/auth/SplashScreen.web.tsx:122 #: src/view/com/auth/SplashScreen.web.tsx:130 -#: src/view/shell/bottom-bar/BottomBar.tsx:342 -#: src/view/shell/bottom-bar/BottomBar.tsx:347 -#: src/view/shell/bottom-bar/BottomBarWeb.tsx:245 +#: src/view/shell/bottom-bar/BottomBar.tsx:343 +#: src/view/shell/bottom-bar/BottomBar.tsx:348 #: src/view/shell/bottom-bar/BottomBarWeb.tsx:250 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:255 #: src/view/shell/NavSignupCard.tsx:58 #: src/view/shell/NavSignupCard.tsx:63 msgid "Sign in" @@ -10500,9 +10563,9 @@ msgstr "" #: src/screens/SignupQueued.tsx:94 #: src/screens/SignupQueued.tsx:97 #: src/screens/Takendown.tsx:88 -#: src/view/shell/desktop/LeftNav.tsx:226 -#: src/view/shell/desktop/LeftNav.tsx:281 -#: src/view/shell/desktop/LeftNav.tsx:284 +#: src/view/shell/desktop/LeftNav.tsx:227 +#: src/view/shell/desktop/LeftNav.tsx:282 +#: src/view/shell/desktop/LeftNav.tsx:285 msgid "Sign out" msgstr "" @@ -10511,7 +10574,7 @@ msgid "Sign Out" msgstr "" #: src/screens/Settings/Settings.tsx:296 -#: src/view/shell/desktop/LeftNav.tsx:223 +#: src/view/shell/desktop/LeftNav.tsx:224 msgid "Sign out?" msgstr "" @@ -10543,7 +10606,7 @@ msgstr "" msgid "Skip contact sharing and continue to the app" msgstr "" -#: src/view/com/composer/Composer.tsx:1331 +#: src/view/com/composer/Composer.tsx:1400 msgid "Skip empty posts?" msgstr "Skip empty posts?" @@ -10557,7 +10620,7 @@ msgstr "" msgid "Skip to next step" msgstr "" -#: src/components/images/Gallery/index.tsx:427 +#: src/components/images/Gallery/index.tsx:435 msgid "slide" msgstr "slide" @@ -10614,7 +10677,7 @@ msgid "Someone left the group" msgstr "Someone left the group" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:270 +#: src/components/dms/MessageItem.tsx:274 msgid "Someone reacted {0}" msgstr "" @@ -10639,11 +10702,15 @@ msgstr "Someone was removed" msgid "Someone was removed from the group" msgstr "Someone was removed from the group" +#: src/components/Post/Embed/GalleryFallbackEmbed.tsx:48 +msgid "Something new is here" +msgstr "Something new is here" + #: src/components/moderation/ReportDialog/index.tsx:103 msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" -#: src/screens/Messages/Conversation.tsx:135 +#: src/screens/Messages/Conversation.tsx:137 #: src/screens/Messages/ConversationSettings/index.tsx:112 #: src/screens/Messages/JoinRequests.tsx:87 msgid "Something went wrong" @@ -10848,13 +10915,13 @@ msgid "Subscribe" msgstr "" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:426 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:435 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:427 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:456 msgid "Subscribe on {0}" msgstr "Subscribe on {0}" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:434 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:455 msgid "Subscribe to {publicationTitle} on {0}" msgstr "Subscribe to {publicationTitle} on {0}" @@ -10889,7 +10956,7 @@ msgstr "" msgid "Success!" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:100 +#: src/components/intents/GroupChatJoinDialog.tsx:101 msgid "Successfully joined the group chat!" msgstr "Successfully joined the group chat!" @@ -10950,7 +11017,7 @@ msgstr "Suspended accounts cannot participate in chat." #: src/screens/Settings/Settings.tsx:122 #: src/screens/Settings/Settings.tsx:134 #: src/screens/Settings/Settings.tsx:615 -#: src/view/shell/desktop/LeftNav.tsx:261 +#: src/view/shell/desktop/LeftNav.tsx:262 msgid "Switch account" msgstr "" @@ -10959,7 +11026,7 @@ msgid "Switch accounts" msgstr "" #. placeholder {0}: sanitizeHandle( profile?.handle ?? account.handle, '@', ) -#: src/view/shell/desktop/LeftNav.tsx:363 +#: src/view/shell/desktop/LeftNav.tsx:364 msgid "Switch to {0}" msgstr "" @@ -10985,7 +11052,7 @@ msgstr "" msgid "Tap below to allow Bluesky to access your GPS location. We will then use that data to more accurately determine the content and features available in your region." msgstr "" -#: src/components/dms/MessageItem.tsx:567 +#: src/components/dms/MessageItem.tsx:580 msgid "Tap for details" msgstr "Tap for details" @@ -11011,10 +11078,23 @@ msgstr "" msgid "Tap to close context menu" msgstr "" +#: src/components/dms/ChatInvite/Root.tsx:73 +msgid "Tap to copy this invite link" +msgstr "Tap to copy this invite link" + #: src/components/ProgressGuide/Toast.tsx:163 msgid "Tap to dismiss" msgstr "" +#: src/components/dms/ChatInvite/Root.tsx:126 +#: src/components/intents/GroupChatJoinDialog.tsx:424 +msgid "Tap to join this group chat immediately" +msgstr "Tap to join this group chat immediately" + +#: src/components/dms/ChatInvite/Root.tsx:86 +msgid "Tap to open this group chat" +msgstr "Tap to open this group chat" + #: src/components/dms/ReactionsDialog.tsx:196 msgid "Tap to remove" msgstr "Tap to remove" @@ -11024,7 +11104,12 @@ msgstr "Tap to remove" msgid "Tap to remove your {0} reaction" msgstr "Tap to remove your {0} reaction" -#: src/components/dms/MessageItem.tsx:532 +#: src/components/dms/ChatInvite/Root.tsx:125 +#: src/components/intents/GroupChatJoinDialog.tsx:423 +msgid "Tap to request access to join this group chat" +msgstr "Tap to request access to join this group chat" + +#: src/components/dms/MessageItem.tsx:545 msgid "Tap to retry" msgstr "Tap to retry" @@ -11037,7 +11122,7 @@ msgstr "Tap to show {0} reactions" msgid "Tap to show all reactions" msgstr "Tap to show all reactions" -#: src/components/dms/MessageItem.tsx:293 +#: src/components/dms/MessageItem.tsx:297 msgid "Tap to view reactions" msgstr "Tap to view reactions" @@ -11206,7 +11291,7 @@ msgstr "" msgid "The laws in your region require you to verify you're an adult to access certain features. Tap to learn more." msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:134 +#: src/components/intents/GroupChatJoinDialog.tsx:135 #: src/screens/Messages/JoinRequests.tsx:201 msgid "The member limit has been reached." msgstr "The member limit has been reached." @@ -11263,7 +11348,7 @@ msgstr "" msgid "There is a limit to how often you can change your birthdate. You may need to wait a day or two before updating it again." msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:487 +#: src/screens/Messages/components/InviteLinkDialog.tsx:482 msgid "There is no invite link for this group chat." msgstr "There is no invite link for this group chat." @@ -11277,8 +11362,8 @@ msgid "There was a problem loading GIFs. Check your connection and try again." msgstr "There was a problem loading GIFs. Check your connection and try again." #: src/components/contacts/screens/GetContacts.tsx:147 -#: src/components/intents/GroupChatJoinDialog.tsx:118 -#: src/components/intents/GroupChatJoinDialog.tsx:152 +#: src/components/intents/GroupChatJoinDialog.tsx:119 +#: src/components/intents/GroupChatJoinDialog.tsx:153 msgid "There was a problem with your internet connection, please try again" msgstr "" @@ -11435,11 +11520,12 @@ msgstr "" msgid "This chat has ended" msgstr "This chat has ended" -#: src/components/intents/GroupChatJoinDialog.tsx:263 +#: src/components/dms/ChatInvite/Root.tsx:108 +#: src/components/intents/GroupChatJoinDialog.tsx:264 msgid "This chat is full" msgstr "This chat is full" -#: src/screens/Messages/components/ChatListItem.tsx:375 +#: src/screens/Messages/components/ChatListItem.tsx:380 #: src/screens/Messages/components/ChatLocked.tsx:61 msgid "This chat is locked" msgstr "This chat is locked" @@ -11478,11 +11564,11 @@ msgstr "" msgid "This content is not viewable without a Bluesky account." msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:120 +#: src/components/intents/GroupChatJoinDialog.tsx:121 msgid "This conversation is locked." msgstr "This conversation is locked." -#: src/screens/Messages/components/ChatListItem.tsx:155 +#: src/screens/Messages/components/ChatListItem.tsx:156 msgid "This conversation is with a deleted or a deactivated account. Press for options" msgstr "" @@ -11532,7 +11618,7 @@ msgstr "" msgid "This handle is reserved. Please try a different one." msgstr "" -#: src/screens/Onboarding/StepProfile/index.tsx:141 +#: src/screens/Onboarding/StepProfile/index.tsx:142 msgid "This image could not be used. Try a different format like .jpg or .png." msgstr "This image could not be used. Try a different format like .jpg or .png." @@ -11540,7 +11626,7 @@ msgstr "This image could not be used. Try a different format like .jpg or .png." msgid "This information is private and not shared with other users." msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:130 +#: src/components/intents/GroupChatJoinDialog.tsx:131 msgid "This invite link has been disabled." msgstr "This invite link has been disabled." @@ -11548,7 +11634,7 @@ msgstr "This invite link has been disabled." msgid "This invite link has expired" msgstr "This invite link has expired" -#: src/components/intents/GroupChatJoinDialog.tsx:194 +#: src/components/intents/GroupChatJoinDialog.tsx:195 msgid "This invite link is invalid" msgstr "This invite link is invalid" @@ -11594,13 +11680,13 @@ msgstr "" msgid "This list is empty." msgstr "" -#: src/components/dms/MessageItem.tsx:565 -#: src/components/dms/MessageItem.tsx:594 +#: src/components/dms/MessageItem.tsx:578 +#: src/components/dms/MessageItem.tsx:607 msgid "This message is hidden because this user is blocking you." msgstr "This message is hidden because this user is blocking you." -#: src/components/dms/MessageItem.tsx:564 -#: src/components/dms/MessageItem.tsx:590 +#: src/components/dms/MessageItem.tsx:577 +#: src/components/dms/MessageItem.tsx:603 msgid "This message is hidden because you are blocking this user." msgstr "This message is hidden because you are blocking this user." @@ -11638,7 +11724,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:945 +#: src/view/com/composer/Composer.tsx:1013 msgid "This post's author has disabled quote posts." msgstr "" @@ -11980,7 +12066,7 @@ msgstr "" msgid "Unavailable feed information" msgstr "" -#: src/components/dms/MessageItem.tsx:632 +#: src/components/dms/MessageItem.tsx:645 #: src/components/dms/MessagesListBlockedFooter.tsx:97 #: src/components/dms/MessagesListBlockedFooter.tsx:104 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:222 @@ -12219,11 +12305,11 @@ msgstr "" msgid "Unsubscribed from list" msgstr "" -#: src/view/com/composer/text-input/TextInput.tsx:131 +#: src/view/com/composer/text-input/TextInput.tsx:128 msgid "Unsupported clipboard content" msgstr "Unsupported clipboard content" -#: src/view/com/composer/Composer.tsx:1424 +#: src/view/com/composer/Composer.tsx:1489 msgid "Unsupported video type: {mimeType}" msgstr "" @@ -12236,6 +12322,10 @@ msgstr "" msgid "Update <0>{displayName} in Lists" msgstr "" +#: src/components/Post/Embed/GalleryFallbackEmbed.tsx:93 +msgid "Update app" +msgstr "Update app" + #: src/components/dialogs/EmailDialog/screens/Update.tsx:296 #: src/components/dialogs/EmailDialog/screens/Update.tsx:308 #: src/screens/Settings/AccountSettings.tsx:112 @@ -12243,9 +12333,9 @@ msgstr "" msgid "Update email" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:217 -#: src/screens/Messages/components/InviteLinkDialog.tsx:255 -#: src/screens/Messages/components/InviteLinkDialog.tsx:283 +#: src/screens/Messages/components/InviteLinkDialog.tsx:212 +#: src/screens/Messages/components/InviteLinkDialog.tsx:250 +#: src/screens/Messages/components/InviteLinkDialog.tsx:278 msgid "Update invite link" msgstr "Update invite link" @@ -12254,7 +12344,15 @@ msgstr "Update invite link" msgid "Update to {domain}" msgstr "" -#: src/screens/Messages/Conversation.tsx:335 +#: src/components/Post/Embed/GalleryFallbackEmbed.tsx:85 +msgid "Update your app" +msgstr "Update your app" + +#: src/components/Post/Embed/GalleryFallbackEmbed.tsx:76 +msgid "Update your app to see it." +msgstr "Update your app to see it." + +#: src/screens/Messages/Conversation.tsx:360 msgid "Update your app to the latest version to join in!" msgstr "Update your app to the latest version to join in!" @@ -12279,7 +12377,7 @@ msgctxt "toast" msgid "Updating reply visibility failed" msgstr "" -#: src/screens/Onboarding/StepProfile/index.tsx:314 +#: src/screens/Onboarding/StepProfile/index.tsx:315 msgid "Upload a photo instead" msgstr "" @@ -12287,39 +12385,40 @@ msgstr "" msgid "Upload a text file to:" msgstr "" -#: src/view/com/util/UserAvatar.tsx:491 #: src/view/com/util/UserAvatar.tsx:494 -#: src/view/com/util/UserBanner.tsx:161 +#: src/view/com/util/UserAvatar.tsx:497 #: src/view/com/util/UserBanner.tsx:164 +#: src/view/com/util/UserBanner.tsx:167 msgid "Upload from Camera" msgstr "" -#: src/view/com/util/UserAvatar.tsx:508 -#: src/view/com/util/UserBanner.tsx:178 +#: src/view/com/util/UserAvatar.tsx:511 +#: src/view/com/util/UserBanner.tsx:181 msgid "Upload from Files" msgstr "" -#: src/view/com/util/UserAvatar.tsx:502 -#: src/view/com/util/UserAvatar.tsx:506 -#: src/view/com/util/UserBanner.tsx:172 -#: src/view/com/util/UserBanner.tsx:176 +#: src/view/com/util/UserAvatar.tsx:505 +#: src/view/com/util/UserAvatar.tsx:509 +#: src/view/com/util/UserBanner.tsx:175 +#: src/view/com/util/UserBanner.tsx:179 msgid "Upload from Library" msgstr "" -#: src/view/com/composer/Composer.tsx:2453 +#: src/view/com/composer/Composer.tsx:2519 msgid "Uploading GIF..." msgstr "" -#: src/lib/api/index.ts:322 +#: src/lib/api/index.ts:327 +#: src/lib/api/index.ts:354 msgid "Uploading images..." msgstr "" -#: src/lib/api/index.ts:390 -#: src/lib/api/index.ts:414 +#: src/lib/api/index.ts:426 +#: src/lib/api/index.ts:450 msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:2455 +#: src/view/com/composer/Composer.tsx:2521 msgid "Uploading video..." msgstr "" @@ -12608,7 +12707,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:2473 +#: src/view/com/composer/Composer.tsx:2539 msgid "Video uploaded" msgstr "" @@ -12621,18 +12720,18 @@ msgstr "" msgid "Videos" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:432 +#: src/view/com/composer/SelectMediaButton.tsx:434 msgid "Videos must be less than 3 minutes long." msgstr "" -#: src/view/com/composer/Composer.tsx:1037 +#: src/view/com/composer/Composer.tsx:1106 msgctxt "Action to view the post the user just created" msgid "View" msgstr "" #. placeholder {0}: view.source.title #: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:616 msgid "View {0}" msgstr "View {0}" @@ -12661,7 +12760,7 @@ msgstr "View {0}’s profile" msgid "View {displayName}’s profile" msgstr "View {displayName}’s profile" -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:437 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:458 msgid "View {publicationTitle}" msgstr "View {publicationTitle}" @@ -12696,6 +12795,14 @@ msgstr "" msgid "View incoming group chat requests" msgstr "View incoming group chat requests" +#: src/screens/Messages/components/RequestStatus.tsx:54 +msgid "View incoming requests" +msgstr "View incoming requests" + +#: src/screens/Messages/components/RequestStatus.tsx:55 +msgid "View incoming requests to join this group chat" +msgstr "View incoming requests to join this group chat" + #: src/components/moderation/LabelsOnMe.tsx:56 msgid "View information about these labels" msgstr "" @@ -12711,7 +12818,7 @@ msgstr "" msgid "View more trending videos" msgstr "" -#: src/view/com/composer/Composer.tsx:1032 +#: src/view/com/composer/Composer.tsx:1101 msgid "View post" msgstr "" @@ -12729,9 +12836,9 @@ msgid "View profile banner" msgstr "View profile banner" #: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:427 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:438 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:428 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:459 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:616 msgid "View publication" msgstr "View publication" @@ -12854,7 +12961,7 @@ msgstr "" msgid "We couldn't find any results for that topic." msgstr "" -#: src/screens/Messages/Conversation.tsx:136 +#: src/screens/Messages/Conversation.tsx:138 msgid "We couldn't load this conversation" msgstr "" @@ -13007,7 +13114,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:943 +#: src/view/com/composer/Composer.tsx:1011 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -13058,7 +13165,7 @@ msgid "What do you want to call your starter pack?" msgstr "" #: src/view/com/auth/SplashScreen.web.tsx:98 -#: src/view/com/composer/Composer.tsx:1384 +#: src/view/com/composer/Composer.tsx:1453 #: src/view/com/feeds/ComposerPrompt.tsx:193 msgid "What's up?" msgstr "" @@ -13071,7 +13178,7 @@ msgstr "" msgid "Who can interact with this post?" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:227 +#: src/screens/Messages/components/InviteLinkDialog.tsx:222 msgid "Who can join this group chat and how" msgstr "Who can join this group chat and how" @@ -13144,7 +13251,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:1302 +#: src/view/com/composer/Composer.tsx:1371 msgid "Would you like to save this as a draft to edit later?" msgstr "" @@ -13153,12 +13260,12 @@ msgstr "" msgid "Write a post" msgstr "" -#: src/view/com/composer/Composer.tsx:1484 +#: src/view/com/composer/Composer.tsx:1549 msgid "Write post" msgstr "" #: src/screens/PostThread/components/ThreadComposePrompt.tsx:91 -#: src/view/com/composer/Composer.tsx:1382 +#: src/view/com/composer/Composer.tsx:1451 msgid "Write your reply" msgstr "" @@ -13224,7 +13331,7 @@ msgid "You are accessing Bluesky from a region that legally requires us to verif msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:605 +#: src/components/dms/MessageItem.tsx:618 msgid "You are blocking {0}" msgstr "You are blocking {0}" @@ -13325,7 +13432,12 @@ msgstr "" msgid "You can now sign in with your new password." msgstr "" -#: src/view/com/composer/Composer.tsx:1307 +#. 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:212 +msgid "You can only add up to {MAX_GALLERY_IMAGES} images per post" +msgstr "You can only add up to {MAX_GALLERY_IMAGES} images per post" + +#: src/view/com/composer/Composer.tsx:1376 msgid "You can only save drafts up to 1000 characters." msgstr "" @@ -13333,11 +13445,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:435 +#: src/view/com/composer/SelectMediaButton.tsx:437 msgid "You can only select one GIF at a time." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:429 +#: src/view/com/composer/SelectMediaButton.tsx:431 msgid "You can only select one video at a time." msgstr "" @@ -13349,10 +13461,10 @@ msgstr "" 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, currently 4 but may change. -#: src/view/com/composer/SelectMediaButton.tsx:421 -msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." -msgstr "" +#. Error message for maximum number of images that can be selected to add to a post. +#: src/view/com/composer/SelectMediaButton.tsx:423 +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." #: src/components/interstitials/Trending.tsx:132 #: src/components/interstitials/TrendingVideos.tsx:138 @@ -13391,9 +13503,9 @@ msgstr "" msgid "You got here first" msgstr "" -#: src/components/intents/GroupChatJoinDialog.tsx:136 -msgid "You have been removed from this group." -msgstr "You have been removed from this group." +#: src/components/intents/GroupChatJoinDialog.tsx:137 +msgid "You have been previously removed from this group and can’t join it using this link." +msgstr "You have been previously removed from this group and can’t join it using this link." #: src/components/moderation/ModerationDetailsDialog.tsx:77 #: src/lib/moderation/useModerationCauseDescription.ts:58 @@ -13464,7 +13576,7 @@ msgstr "" msgid "You have temporarily reached the limit for video uploads. Please try again later." msgstr "" -#: src/view/com/composer/Composer.tsx:1297 +#: src/view/com/composer/Composer.tsx:1366 msgid "You have unsaved changes to this draft, would you like to save them?" msgstr "" @@ -13534,7 +13646,7 @@ msgstr "" msgid "You must grant access to your photo library to save a QR code" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:464 +#: src/view/com/composer/SelectMediaButton.tsx:466 msgid "You need to allow access to your media library." msgstr "" @@ -13552,7 +13664,7 @@ msgid "You probably want to restart the app now." msgstr "" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:263 +#: src/components/dms/MessageItem.tsx:267 msgid "You reacted {0}" msgstr "" @@ -13567,7 +13679,7 @@ msgid "You recently changed your birthdate" msgstr "" #: src/screens/Settings/Settings.tsx:297 -#: src/view/shell/desktop/LeftNav.tsx:224 +#: src/view/shell/desktop/LeftNav.tsx:225 msgid "You will be signed out of all your accounts." msgstr "" @@ -13661,7 +13773,7 @@ msgstr "" msgid "You've reached the end of your feed! Find some more accounts to follow." msgstr "" -#: src/view/com/composer/Composer.tsx:576 +#: src/view/com/composer/Composer.tsx:635 msgid "You've reached the maximum number of drafts" msgstr "" @@ -13817,7 +13929,7 @@ msgstr "" msgid "Your muted words" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:189 +#: src/screens/Messages/components/InviteLinkDialog.tsx:184 msgid "Your name, avatar, the name of the group chat, and the number of members will be visible to everyone." msgstr "Your name, avatar, the name of the group chat, and the number of members will be visible to everyone." @@ -13829,11 +13941,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:1028 +#: src/view/com/composer/Composer.tsx:1097 msgid "Your post was sent" msgstr "" -#: src/view/com/composer/Composer.tsx:1025 +#: src/view/com/composer/Composer.tsx:1094 msgid "Your posts were sent" msgstr "" @@ -13854,7 +13966,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:1027 +#: src/view/com/composer/Composer.tsx:1096 msgid "Your reply was sent" msgstr "" @@ -13867,7 +13979,7 @@ msgstr "" msgid "Your selected interests help us serve you content you care about." msgstr "" -#: src/view/com/composer/Composer.tsx:1332 +#: src/view/com/composer/Composer.tsx:1401 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." -- 2.51.2 From d3f073b3d29df83cc288ea6bb4ec0e24e22c87b3 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 16:35:06 +0300 Subject: [PATCH 38/61] [Chat] Request screen tweaks (#10747) --- src/screens/Messages/Inbox.tsx | 45 ++----------------- .../Messages/components/ChatListItem.tsx | 18 ++++---- 2 files changed, 12 insertions(+), 51 deletions(-) diff --git a/src/screens/Messages/Inbox.tsx b/src/screens/Messages/Inbox.tsx index 55d3b0f4d..59ca05bdb 100644 --- a/src/screens/Messages/Inbox.tsx +++ b/src/screens/Messages/Inbox.tsx @@ -26,10 +26,9 @@ import {useLeftConvos} from '#/state/queries/messages/leave-conversation' import {useListConvosQuery} from '#/state/queries/messages/list-conversations' import {useUpdateAllRead} from '#/state/queries/messages/update-all-read' import {EmptyState} from '#/view/com/util/EmptyState' -import {FAB} from '#/view/com/util/fab/FAB' import {List} from '#/view/com/util/List' import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {atoms as a, useTheme, web} from '#/alf' import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -62,8 +61,6 @@ export function MessagesInboxScreen(props: Props) { } export function MessagesInboxScreenInner({}: Props) { - const {gtTablet} = useBreakpoints() - const listConvosQuery = useListConvosQuery({status: 'request'}) const {data} = listConvosQuery @@ -94,21 +91,16 @@ export function MessagesInboxScreenInner({}: Props) { - + Chat requests - {hasUnreadConvos && gtTablet ? ( - - ) : ( - - )} + {hasUnreadConvos ? : } ) @@ -117,14 +109,12 @@ export function MessagesInboxScreenInner({}: Props) { function RequestList({ listConvosQuery, conversations, - hasUnreadConvos, }: { listConvosQuery: UseInfiniteQueryResult< InfiniteData, Error > conversations: ChatBskyConvoDefs.ConvoView[] - hasUnreadConvos: boolean }) { const {t: l} = useLingui() const t = useTheme() @@ -285,7 +275,6 @@ function RequestList({ desktopFixedHeight sideBorders={false} /> - {hasUnreadConvos && } ) } @@ -298,34 +287,6 @@ function renderItem({item}: {item: ChatBskyConvoDefs.ConvoView}) { return } -function MarkAllReadFAB() { - const {t: l} = useLingui() - const t = useTheme() - const {mutate: markAllRead} = useUpdateAllRead('request', { - onMutate: () => { - Toast.show(l`Marked all as read`, { - type: 'success', - }) - }, - onError: () => { - Toast.show(l`Failed to mark all requests as read`, { - type: 'error', - }) - }, - }) - - return ( - markAllRead()} - icon={} - accessibilityRole="button" - accessibilityLabel={l`Mark all as read`} - accessibilityHint="" - /> - ) -} - function MarkAsReadHeaderButton() { const {t: l} = useLingui() const {mutate: markAllRead} = useUpdateAllRead('request', { diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index e8c0b9cc6..142ca4a97 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -122,7 +122,7 @@ function DirectChatItem({ }) { const {t: l} = useLingui() const profile = useProfileShadow(convo.primaryMember) - const {isWithinSplitView} = useIsWithinSplitView() + const {isWithinLeftPanel} = useIsWithinSplitView() const moderation = useMemo( () => moderateProfile(profile, moderationOpts), @@ -140,7 +140,7 @@ function DirectChatItem({ avatar={ } @@ -161,7 +161,7 @@ function DirectChatItem({ isBlockedAccount={moderation.blocked} showProfileBadges postAlerts={ - isWithinSplitView ? null : ( + isWithinLeftPanel ? null : ( @@ -205,7 +205,7 @@ function GroupChatItem({ avatar={ } @@ -278,7 +278,7 @@ function BaseChatItem({ const leaveConvoControl = useDialogControl() const {mutate: markAsRead} = useMarkAsReadMutation() const {gtMobile} = useBreakpoints() - const {isWithinSplitView} = useIsWithinSplitView() + const {isWithinLeftPanel} = useIsWithinSplitView() const playHaptic = useHaptics() const queryClient = useQueryClient() @@ -458,7 +458,7 @@ function BaseChatItem({ leftFirst: deleteAction, } - const avatarSize = isWithinSplitView ? 48 : 52 + const avatarSize = isWithinLeftPanel ? 48 : 52 return ( @@ -469,7 +469,7 @@ function BaseChatItem({ // @ts-expect-error web only onFocus={onFocus} onBlur={onMouseLeave} - style={[a.relative, t.atoms.bg, isWithinSplitView && a.mx_sm]}> + style={[a.relative, t.atoms.bg, isWithinLeftPanel && a.mx_sm]}> Date: Fri, 5 Jun 2026 16:38:23 +0300 Subject: [PATCH 39/61] [Chat] Fix stale reactions dialog after removing a reaction (#10743) Co-authored-by: Claude Opus 4.8 (1M context) --- src/components/dms/MessageOverlays.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/dms/MessageOverlays.tsx b/src/components/dms/MessageOverlays.tsx index 41228f289..2384aad88 100644 --- a/src/components/dms/MessageOverlays.tsx +++ b/src/components/dms/MessageOverlays.tsx @@ -125,6 +125,22 @@ export function MessageOverlays({children}: {children: React.ReactNode}) { [openDeleteMessage, openReportMessage, openReactions], ) + // `reactionsTarget` is a snapshot from when the dialog was opened. Read the + // live message out of the convo items so optimistic reaction changes (e.g. + // "Tap to remove") are reflected in the dialog without closing it first. + const reactionsMessage = useMemo(() => { + if (!reactionsTarget) return null + for (const item of convo.items) { + if ( + (item.type === 'message' || item.type === 'pending-message') && + item.message.id === reactionsTarget.id + ) { + return item.message + } + } + return reactionsTarget + }, [convo.items, reactionsTarget]) + const reportSubject = reportTarget ? ({ view: 'message', @@ -153,11 +169,11 @@ export function MessageOverlays({children}: {children: React.ReactNode}) { onClose={() => setAfterReportTarget(null)} /> )} - {reactionsTarget && ( + {reactionsMessage && ( setReactionsTarget(null)} /> )} -- 2.51.2 From f81bb6f4942f6321837e5987d5c1dac51666eb35 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 16:38:48 +0300 Subject: [PATCH 40/61] [Chat] Fix navigation stacking on messages links in split view (#10741) Co-authored-by: Claude Opus 4.8 (1M context) --- src/screens/Messages/ChatList.tsx | 8 ++++++++ src/screens/Messages/components/ChatListItem.tsx | 3 +++ src/screens/Messages/components/InboxRequests.tsx | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 83c4b0c00..387fcd2bd 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -451,6 +451,12 @@ export function Header({ const {gtMobile} = useBreakpoints() const requireEmailVerification = useRequireEmailVerification() const leftConvos = useLeftConvos() + const {isWithinSplitView} = useIsWithinSplitView() + + // In split view, the left column (and this header) stays mounted while the + // right column shows the selected route. Pushing would stack duplicate routes + // on repeated clicks, so navigate instead to dedupe by route + params. + const action = isWithinSplitView ? 'navigate' : 'push' const {data: unreadInboxData, hasNextPage: hasMoreRequests} = useListConvosQuery({ @@ -494,9 +500,11 @@ export function Header({ count={inboxAllConvos.length} more={hasMoreRequests} variant="solid" + action={action} /> -- 2.51.2 From 7d4d7642fdcfe684076dbb4d13f48e36ac470071 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 16:39:23 +0300 Subject: [PATCH 41/61] [Chat] Sync single-convo cache on chat lock firehose events (#10746) Co-authored-by: Claude Opus 4.8 (1M context) --- .../queries/messages/list-conversations.tsx | 63 +++++-------------- 1 file changed, 15 insertions(+), 48 deletions(-) diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx index f026e57d3..dadf30bcd 100644 --- a/src/state/queries/messages/list-conversations.tsx +++ b/src/state/queries/messages/list-conversations.tsx @@ -418,67 +418,34 @@ export function ListConvosProviderInner({ })), ) } else if (ChatBskyConvoDefs.isLogLockConvo(log)) { - queryClient.setQueriesData( - {queryKey: [RQKEY_ROOT]}, - (old?: ConvoListQueryData) => - optimisticUpdate(log.convoId, old, convo => { - if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { - return { - ...convo, - kind: { - ...convo.kind, - lockStatus: 'locked', - }, - rev: log.rev, - } - } - return { + mutateConvoView(log.convoId, convo => + ChatBskyConvoDefs.isGroupConvo(convo.kind) + ? { ...convo, + kind: {...convo.kind, lockStatus: 'locked'}, rev: log.rev, } - }), + : {...convo, rev: log.rev}, ) } else if (ChatBskyConvoDefs.isLogUnlockConvo(log)) { - queryClient.setQueriesData( - {queryKey: [RQKEY_ROOT]}, - (old?: ConvoListQueryData) => - optimisticUpdate(log.convoId, old, convo => { - if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { - return { - ...convo, - kind: { - ...convo.kind, - lockStatus: 'unlocked', - }, - rev: log.rev, - } - } - return { + mutateConvoView(log.convoId, convo => + ChatBskyConvoDefs.isGroupConvo(convo.kind) + ? { ...convo, + kind: {...convo.kind, lockStatus: 'unlocked'}, rev: log.rev, } - }), + : {...convo, rev: log.rev}, ) } else if (ChatBskyConvoDefs.isLogLockConvoPermanently(log)) { - queryClient.setQueriesData( - {queryKey: [RQKEY_ROOT]}, - (old?: ConvoListQueryData) => - optimisticUpdate(log.convoId, old, convo => { - if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { - return { - ...convo, - kind: { - ...convo.kind, - lockStatus: 'locked-permanently', - }, - rev: log.rev, - } - } - return { + mutateConvoView(log.convoId, convo => + ChatBskyConvoDefs.isGroupConvo(convo.kind) + ? { ...convo, + kind: {...convo.kind, lockStatus: 'locked-permanently'}, rev: log.rev, } - }), + : {...convo, rev: log.rev}, ) } else if ( ChatBskyConvoDefs.isLogCreateJoinLink(log) || -- 2.51.2 From 67ed59fbcd85809ac135ae32ec3fa970d6a829ee Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 18:10:14 +0300 Subject: [PATCH 42/61] Align group invite settings with eligibility logic (#10748) Co-authored-by: Claude Opus 4.8 (1M context) --- src/components/dms/util.ts | 16 +++++++++++ src/screens/Messages/Settings.tsx | 6 ++--- .../Messages/components/ChatListItem.tsx | 2 +- .../queries/messages/actor-declaration.ts | 27 +++++++++++++++---- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts index 89b9c5b01..7fe5f91dc 100644 --- a/src/components/dms/util.ts +++ b/src/components/dms/util.ts @@ -47,6 +47,22 @@ export function canBeAddedToGroup(profile: bsky.profile.AnyProfileView) { } } +/** + * Resolves the effective `allowGroupInvites` value for a chat declaration. + * When unset, group invites follow the general DM preference + * (`allowIncoming`), which itself defaults to `following`. This mirrors the + * `undefined` fallthrough in canBeAddedToGroup, and is the single source of + * truth for both displaying and persisting the setting. + */ +export function resolveAllowGroupInvites( + chat: {allowIncoming?: string; allowGroupInvites?: string} | undefined, +): 'all' | 'none' | 'following' { + return (chat?.allowGroupInvites ?? chat?.allowIncoming ?? 'following') as + | 'all' + | 'none' + | 'following' +} + export function localDateString(date: Date) { // can't use toISOString because it should be in local time const mm = date.getMonth() diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index f49311b0d..91d9f879d 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -13,6 +13,7 @@ import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' +import {resolveAllowGroupInvites} from '#/components/dms/util' import * as Toggle from '#/components/forms/Toggle' import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell' import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car' @@ -199,10 +200,7 @@ export function MessagesSettingsScreenInner({}: Props) { {allowGroupInvitesFromOptions.map(option => ( diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 1f691290b..b6d237f17 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -483,7 +483,7 @@ function BaseChatItem({ to={`/messages/${convo.view.id}`} // In split view, this list stays mounted alongside the open convo, // so push would stack duplicate routes on repeated clicks. - action={isWithinSplitView ? 'navigate' : 'push'} + action={isWithinLeftPanel ? 'navigate' : 'push'} label={title} accessibilityHint={accessibilityHint} accessibilityActions={ diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index 53b493ab6..f6cd2d51d 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -7,6 +7,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {useAgent, useSession} from '#/state/session' +import {resolveAllowGroupInvites} from '#/components/dms/util' import {RQKEY as PROFILE_RKEY} from '../profile' export function useUpdateActorDeclaration({ @@ -34,8 +35,12 @@ export function useUpdateActorDeclaration({ update.allowIncoming ?? current?.associated?.chat?.allowIncoming ?? 'following' - const allowGroupInvites = - update.allowGroupInvites ?? current?.associated?.chat?.allowGroupInvites + const allowGroupInvites = resolveAllowGroupInvites({ + allowIncoming, + allowGroupInvites: + update.allowGroupInvites ?? + current?.associated?.chat?.allowGroupInvites, + }) const result = await agent.com.atproto.repo.putRecord({ repo: currentAccount.did, collection: 'chat.bsky.actor.declaration', @@ -43,7 +48,7 @@ export function useUpdateActorDeclaration({ record: { $type: 'chat.bsky.actor.declaration', allowIncoming, - ...(allowGroupInvites && {allowGroupInvites}), + allowGroupInvites, }, }) return result @@ -54,14 +59,26 @@ export function useUpdateActorDeclaration({ PROFILE_RKEY(currentAccount?.did), (old?: AppBskyActorDefs.ProfileViewDetailed) => { if (!old) return old + const allowIncoming = + update.allowIncoming ?? + old.associated?.chat?.allowIncoming ?? + 'following' + // resolve the same concrete value the server will receive, so + // optimistic cache and persisted record stay aligned + const allowGroupInvites = resolveAllowGroupInvites({ + allowIncoming, + allowGroupInvites: + update.allowGroupInvites ?? + old.associated?.chat?.allowGroupInvites, + }) return { ...old, associated: { ...old.associated, chat: { - allowIncoming: 'following', ...old.associated?.chat, - ...update, + allowIncoming, + allowGroupInvites, }, }, } satisfies AppBskyActorDefs.ProfileViewDetailed -- 2.51.2 From f8aae4a192eb81cbe551e0bf10e3f045e114eaee Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 18:18:27 +0300 Subject: [PATCH 43/61] [Chat] Disable font scaling on fixed-height chat invite cards (#10744) Co-authored-by: Claude Opus 4.8 (1M context) --- src/components/Post/Embed/ChatInviteEmbed.tsx | 2 +- .../Post/Embed/JoinRequestEmbed.tsx | 5 ++- src/components/ProfileBadges.tsx | 12 ++++--- src/components/Typography.tsx | 4 ++- src/components/dms/ChatInvite/Card.tsx | 32 ++++++++++++------- src/components/dms/ChatInvite/Context.tsx | 2 ++ src/components/dms/ChatInvite/JoinButton.tsx | 4 +-- src/components/dms/ChatInvite/Root.tsx | 4 ++- src/components/dms/MessageItemInviteEmbed.tsx | 3 +- .../Messages/components/MessageInputEmbed.tsx | 2 +- 10 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/components/Post/Embed/ChatInviteEmbed.tsx b/src/components/Post/Embed/ChatInviteEmbed.tsx index f50085426..387c3cfd9 100644 --- a/src/components/Post/Embed/ChatInviteEmbed.tsx +++ b/src/components/Post/Embed/ChatInviteEmbed.tsx @@ -23,7 +23,7 @@ export function ChatInviteEmbed({ style?: StyleProp }) { return ( - + ) diff --git a/src/components/Post/Embed/JoinRequestEmbed.tsx b/src/components/Post/Embed/JoinRequestEmbed.tsx index db7ec589b..e020bb933 100644 --- a/src/components/Post/Embed/JoinRequestEmbed.tsx +++ b/src/components/Post/Embed/JoinRequestEmbed.tsx @@ -31,7 +31,10 @@ export function JoinRequestEmbed({ if (!resolvedCode) return null return ( - + ) diff --git a/src/components/ProfileBadges.tsx b/src/components/ProfileBadges.tsx index 22c682bbb..cb257e78b 100644 --- a/src/components/ProfileBadges.tsx +++ b/src/components/ProfileBadges.tsx @@ -31,10 +31,12 @@ export function ProfileBadges({ interactive = false, size, style, + allowFontScaling = true, }: ViewStyleProp & { profile: bsky.profile.AnyProfileView interactive?: boolean size: Size + allowFontScaling?: boolean }) { const shadowed = useProfileShadow(profile) const verification = useSimpleVerificationState({profile}) @@ -48,10 +50,12 @@ export function ProfileBadges({ const isOnTheSmallSide = size === 'xs' || size === 'sm' - const verificationIconWidth = - verificationIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier - const botIconWidth = - botIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier + const scaleMultiplier = allowFontScaling + ? nativeScaleMultiplier * alfScaleMultiplier + : 1 + + const verificationIconWidth = verificationIconSizes[size] * scaleMultiplier + const botIconWidth = botIconSizes[size] * scaleMultiplier return ( + numberOfLines={1} + allowFontScaling={!hasFixedHeight}> {preview.name}
+ numberOfLines={1} + allowFontScaling={!hasFixedHeight}> Group chat + numberOfLines={1} + allowFontScaling={!hasFixedHeight}> {preview.memberCount}/{preview.memberLimit}{' '} + numberOfLines={1} + allowFontScaling={!hasFixedHeight}> - By {ownerDisplayName} + By{' '} + + {ownerDisplayName} + - + + numberOfLines={1} + allowFontScaling={!hasFixedHeight}> {ownerHandle} diff --git a/src/components/dms/ChatInvite/Context.tsx b/src/components/dms/ChatInvite/Context.tsx index 53c7a6cc2..cedcfd8d8 100644 --- a/src/components/dms/ChatInvite/Context.tsx +++ b/src/components/dms/ChatInvite/Context.tsx @@ -32,6 +32,8 @@ export type ChatInviteContextValue = { * preview to act on. */ action: ChatInviteAction | undefined + /** Whether the invite is rendered inside a fixed-height container; when true, text inside disables font scaling so the card doesn't overflow. */ + hasFixedHeight: boolean } const ChatInviteContext = createContext(null) diff --git a/src/components/dms/ChatInvite/JoinButton.tsx b/src/components/dms/ChatInvite/JoinButton.tsx index 0036391b0..6f3b8c654 100644 --- a/src/components/dms/ChatInvite/JoinButton.tsx +++ b/src/components/dms/ChatInvite/JoinButton.tsx @@ -17,7 +17,7 @@ export function JoinButton({ onPress?: () => void style?: StyleProp }) { - const {action} = useChatInvite() + const {action, hasFixedHeight} = useChatInvite() if (!action) return null @@ -35,7 +35,7 @@ export function JoinButton({ disabled={action.disabled} style={[a.w_full, style]}> {action.side === 'left' && } - {action.label} + {action.label} {action.side === 'right' && } ) diff --git a/src/components/dms/ChatInvite/Root.tsx b/src/components/dms/ChatInvite/Root.tsx index a9f2c54e3..a2cfc558e 100644 --- a/src/components/dms/ChatInvite/Root.tsx +++ b/src/components/dms/ChatInvite/Root.tsx @@ -30,6 +30,7 @@ export function Root({ code, initialPreview, currentConvoId, + hasFixedHeight, children, }: { code: string @@ -40,6 +41,7 @@ export function Root({ * open/join (you're already here). */ currentConvoId?: string + hasFixedHeight: boolean children: React.ReactNode }) { const {hasSession} = useSession() @@ -137,7 +139,7 @@ export function Root({ return ( + value={{code, loading, error: !!error, preview, action, hasFixedHeight}}> {children} ) diff --git a/src/components/dms/MessageItemInviteEmbed.tsx b/src/components/dms/MessageItemInviteEmbed.tsx index 43fd94482..d85e7fd08 100644 --- a/src/components/dms/MessageItemInviteEmbed.tsx +++ b/src/components/dms/MessageItemInviteEmbed.tsx @@ -74,7 +74,8 @@ let MessageItemInviteEmbed = ({ + currentConvoId={convo.convo.view.id} + hasFixedHeight={false}> diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index a73a6564f..21503ce8f 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -273,7 +273,7 @@ function MessageInputInviteEmbed({ const {t: l} = useLingui() return ( - + Date: Fri, 5 Jun 2026 12:50:16 -0500 Subject: [PATCH 44/61] Fire embed:standardSite:view from feed viewability, not embed mount (#10736) Co-authored-by: Claude Opus 4.8 --- .../Post/Embed/StandardSiteEmbed/index.tsx | 7 ------- src/view/com/posts/PostFeed.tsx | 13 +++++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 2c38f3806..2bee7da91 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -5,7 +5,6 @@ import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' import {useHaptics} from '#/lib/haptics' -import {useCallOnce} from '#/lib/once' import {shareUrl} from '#/lib/sharing' import {niceDate} from '#/lib/strings/time' import {toNiceDomain} from '#/lib/strings/url-helpers' @@ -104,12 +103,6 @@ export const StandardSiteEmbed = ({ } } - useCallOnce(() => { - if (!preview) { - ax.metric('embed:standardSite:view', {url: view.uri}) - } - })() - if (isStandardPublication) { return ( >(new Set()) const seenPostUrisRef = useRef>(new Set()) + const seenStandardSiteUrisRef = useRef>(new Set()) // Helper to calculate position in feed (count only root posts, not interstitials or thread replies) const getPostPosition = useNonReactiveCallback( @@ -974,6 +977,16 @@ let PostFeed = ({ }) } } + + // Standard site embed view tracking + if ( + AppBskyEmbedExternal.isView(post.embed) && + isStandardSiteEmbed(post.embed.external) && + !seenStandardSiteUrisRef.current.has(post.embed.external.uri) + ) { + seenStandardSiteUrisRef.current.add(post.embed.external.uri) + ax.metric('embed:standardSite:view', {url: post.embed.external.uri}) + } } else if (item.type === 'videoGridRow') { // Track each video in the grid row for (let i = 0; i < item.items.length; i++) { -- 2.51.2 From a5aa985b33c92c5b73505fda677a76e04f9186bf Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 22:48:34 +0300 Subject: [PATCH 45/61] Fix feed scroll reset on device rotation (#10737) --- src/view/com/util/List.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index cde8d50c4..f8a537a65 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -147,12 +147,10 @@ let List = forwardRef( ) } - let contentOffset if (headerOffset != null) { style = addStyle(style, { paddingTop: headerOffset, }) - contentOffset = {x: 0, y: headerOffset * -1} } return ( @@ -170,7 +168,6 @@ let List = forwardRef( ...props.scrollIndicatorInsets, }} indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} - contentOffset={contentOffset} refreshControl={refreshControl} onScroll={scrollHandler} scrollsToTop={scrollsToTop} -- 2.51.2 From 6d468c8ca47d9bbace760526c5ed82bac41d511c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 22:49:02 +0300 Subject: [PATCH 46/61] [CI] Remove `Wandalen/wretry.action`, call pnpm install directly (#10745) --- .github/workflows/golang-test-lint.yml | 2 ++ .github/workflows/lint.yml | 12 ++---------- .../workflows/nightly-update-source-languages.yaml | 6 +----- .github/workflows/verify-pnpm-lock.yml | 8 ++------ 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/.github/workflows/golang-test-lint.yml b/.github/workflows/golang-test-lint.yml index 6531694d5..2e7388bbd 100644 --- a/.github/workflows/golang-test-lint.yml +++ b/.github/workflows/golang-test-lint.yml @@ -20,6 +20,7 @@ jobs: uses: actions/setup-go@v6 with: go-version-file: bskyweb/go.mod + cache-dependency-path: bskyweb/go.sum - name: Dummy Static Files run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt - name: Check @@ -37,6 +38,7 @@ jobs: uses: actions/setup-go@v6 with: go-version-file: bskyweb/go.mod + cache-dependency-path: bskyweb/go.sum - name: Dummy Static Files run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt - name: Lint diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index aa6c92765..2fb464a92 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -57,11 +57,7 @@ jobs: node-version-file: package.json cache: pnpm - name: pnpm install - uses: Wandalen/wretry.action@master - with: - command: pnpm install --frozen-lockfile - attempt_limit: 3 - attempt_delay: 2000 + run: pnpm install --frozen-lockfile - name: Check & compile i18n run: pnpm intl:build - name: Lint checks @@ -99,11 +95,7 @@ jobs: node-version-file: package.json cache: pnpm - name: pnpm install - uses: Wandalen/wretry.action@master - with: - command: pnpm install --frozen-lockfile - attempt_limit: 3 - attempt_delay: 2000 + run: pnpm install --frozen-lockfile - name: Check & compile i18n run: pnpm intl:build - name: Run tests diff --git a/.github/workflows/nightly-update-source-languages.yaml b/.github/workflows/nightly-update-source-languages.yaml index 4b14c65c6..2a6ec26ac 100644 --- a/.github/workflows/nightly-update-source-languages.yaml +++ b/.github/workflows/nightly-update-source-languages.yaml @@ -26,11 +26,7 @@ jobs: node-version-file: package.json cache: pnpm - name: pnpm install - uses: Wandalen/wretry.action@master - with: - command: pnpm install --frozen-lockfile - attempt_limit: 3 - attempt_delay: 2000 + run: pnpm install --frozen-lockfile - name: Extract language strings run: pnpm intl:extract - name: Create commit diff --git a/.github/workflows/verify-pnpm-lock.yml b/.github/workflows/verify-pnpm-lock.yml index 2d10b11a2..f1b61e95e 100644 --- a/.github/workflows/verify-pnpm-lock.yml +++ b/.github/workflows/verify-pnpm-lock.yml @@ -34,12 +34,8 @@ jobs: run: git show "origin/$BASE_REF:pnpm-lock.yaml" > pnpm-lock.yaml - name: pnpm install - uses: Wandalen/wretry.action@master - with: - # Fine to skip scripts since we don't run any code - command: pnpm clean && pnpm install --ignore-scripts --no-frozen-lockfile - attempt_limit: 3 - attempt_delay: 2000 + # Fine to skip scripts since we don't run any code + run: pnpm clean && pnpm install --ignore-scripts --no-frozen-lockfile - name: Verify pnpm-lock.yaml run: | -- 2.51.2 From 32c0c0ec838367b5ddd00df97b0102fc36ea6a29 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 23:08:39 +0300 Subject: [PATCH 47/61] "Follows You" string dedupe (#10751) --- src/components/Pills.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Pills.tsx b/src/components/Pills.tsx index 36bcb3370..37e940d20 100644 --- a/src/components/Pills.tsx +++ b/src/components/Pills.tsx @@ -172,7 +172,7 @@ export function FollowsYou({size = 'sm'}: CommonProps) { return ( - Follows You + Follows you ) -- 2.51.2 From 2e9bfd363c1f6cf6980361a93c1e4aec8dfa7234 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 23:09:55 +0300 Subject: [PATCH 48/61] [Chat] Put `pointer_events_none` on chat bubbles in ChatListItem (#10752) --- src/screens/Messages/components/ChatListItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index b6d237f17..70390606f 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -475,6 +475,7 @@ function BaseChatItem({ a.z_10, a.absolute, {top: tokens.space.md, left: tokens.space.lg}, + isGroupConvo && a.pointer_events_none, ]}> {avatar} -- 2.51.2 From b236f274c3524a0e091a12741b6b3855ae9283ef Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 5 Jun 2026 23:17:29 +0300 Subject: [PATCH 49/61] [Chat] Enforce limits for group size (#10753) --- .../dialogs/SearchablePeopleList.tsx | 1 - src/components/dms/AddMembersFlow.tsx | 11 ++++ src/components/dms/InitiateChatFlow.tsx | 34 ++++++++++- .../dms/components/GroupChatProfileCard.tsx | 58 +++++++++++-------- src/lib/constants.ts | 2 + .../Messages/ConversationSettings/prompts.tsx | 35 +++++++++-- 6 files changed, 109 insertions(+), 32 deletions(-) diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx index 614234c17..cc07900be 100644 --- a/src/components/dialogs/SearchablePeopleList.tsx +++ b/src/components/dialogs/SearchablePeopleList.tsx @@ -667,7 +667,6 @@ function SearchInput({ /> - + + {groupNameTooLong ? ( + + + Group name is too long. The maximum number of characters + is {MAX_GROUP_NAME_GRAPHEME_LENGTH}. + + + ) : null}
) : ( - - - - - - {enabled ? ( - - ) : ( - - {handle} can’t be added - - )} + {({disabled, selected}) => ( + <> + + + + + + {enabled ? ( + + ) : ( + + {handle} can’t be added + + )} + + - - - {enabled ? : null} + {enabled ? : null} + + )} ) } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 13ae162a8..91d1319c6 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -67,6 +67,8 @@ export const MAX_DRAFT_GRAPHEME_LENGTH = 1000 export const MAX_DM_GRAPHEME_LENGTH = 1000 +export const MAX_GROUP_NAME_GRAPHEME_LENGTH = 50 + // Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html // but increasing limit per user feedback export const MAX_ALT_TEXT = 2000 diff --git a/src/screens/Messages/ConversationSettings/prompts.tsx b/src/screens/Messages/ConversationSettings/prompts.tsx index f5b980b99..e8b594c0b 100644 --- a/src/screens/Messages/ConversationSettings/prompts.tsx +++ b/src/screens/Messages/ConversationSettings/prompts.tsx @@ -1,10 +1,13 @@ import {View} from 'react-native' import {Trans, useLingui} from '@lingui/react/macro' -import {atoms as a} from '#/alf' +import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants' +import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' +import {atoms as a, useTheme} from '#/alf' import type * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' export function EditNamePrompt({ control, @@ -17,8 +20,14 @@ export function EditNamePrompt({ onChangeText: (value: string) => void onConfirm: () => void }) { + const t = useTheme() const {t: l} = useLingui() + const nameTooLong = isOverMaxGraphemeCount({ + text: value, + maxCount: MAX_GROUP_NAME_GRAPHEME_LENGTH, + }) + return ( <> @@ -27,7 +36,7 @@ export function EditNamePrompt({ Edit group name - + + {nameTooLong ? ( + + + Group name is too long. The maximum number of characters is{' '} + {MAX_GROUP_NAME_GRAPHEME_LENGTH}. + + + ) : null} - + -- 2.51.2 From fb8316fd85d2d596266e0e1e7c47fa07823e4f98 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 6 Jun 2026 00:04:21 +0300 Subject: [PATCH 50/61] Fix external card long press on web (#10739) --- eslint-suppressions.json | 5 -- .../Post/Embed/ExternalEmbed/index.tsx | 20 +++--- .../Post/Embed/StandardSiteEmbed/index.tsx | 66 ++++++++++--------- 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index b8e65eb26..1c4c89f38 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -261,11 +261,6 @@ "count": 1 } }, - "src/components/Post/Embed/StandardSiteEmbed/index.tsx": { - "@typescript-eslint/no-floating-promises": { - "count": 3 - } - }, "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx": { "@typescript-eslint/no-floating-promises": { "count": 2 diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index 472c40306..9a4f7e56b 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo} from 'react' +import {useMemo} from 'react' import {type StyleProp, View, type ViewStyle} from 'react-native' import {Image} from 'expo-image' import {type AppBskyEmbedExternal} from '@atproto/api' @@ -51,17 +51,19 @@ export const ExternalEmbed = ({ }, [link.uri, externalEmbedPrefs]) const hasMedia = Boolean(imageUri || embedPlayerParams) - const onPress = useCallback(() => { + const onPress = () => { playHaptic('Light') onOpen?.() - }, [playHaptic, onOpen]) + } - const onShareExternal = useCallback(() => { - if (link.uri && IS_NATIVE) { - playHaptic('Heavy') - void shareUrl(link.uri) - } - }, [link.uri, playHaptic]) + const onShareExternal = IS_NATIVE + ? () => { + if (link.uri) { + playHaptic('Heavy') + void shareUrl(link.uri) + } + } + : undefined if ( embedPlayerParams?.source === 'tenor' || diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 2bee7da91..b328bc38f 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -79,13 +79,15 @@ export const StandardSiteEmbed = ({ onEmbedInteractionCallback?.() ax.metric('embed:standardSite:article:press', {url: view.uri}) } - const onLongPress = () => { - if (view.uri && IS_NATIVE) { - playHaptic('Heavy') - shareUrl(view.uri) - ax.metric('embed:standardSite:article:longPress', {url: view.uri}) - } - } + const onLongPress = IS_NATIVE + ? () => { + if (view.uri) { + playHaptic('Heavy') + void shareUrl(view.uri) + ax.metric('embed:standardSite:article:longPress', {url: view.uri}) + } + } + : undefined const onPressPublication = () => { playHaptic('Light') onEmbedInteractionCallback?.() @@ -93,15 +95,17 @@ export const StandardSiteEmbed = ({ url: view.source?.uri || '', }) } - const onLongPressPublication = () => { - if (view.source?.uri && IS_NATIVE) { - playHaptic('Heavy') - shareUrl(view.source.uri) - ax.metric('embed:standardSite:publication:longPress', { - url: view.source.uri, - }) - } - } + const onLongPressPublication = IS_NATIVE + ? () => { + if (view.source?.uri) { + playHaptic('Heavy') + void shareUrl(view.source.uri) + ax.metric('embed:standardSite:publication:longPress', { + url: view.source.uri, + }) + } + } + : undefined if (isStandardPublication) { return ( @@ -465,21 +469,23 @@ export function SubscribeButton({ } } - const onLongPress = () => { - if (view.source?.uri && IS_NATIVE) { - playHaptic('Heavy') - shareUrl(view.source.uri) - if (highlightedPublisher) { - ax.metric('embed:standardSite:subscribe:longPress', { - url: view.source?.uri || '', - }) - } else { - ax.metric('embed:standardSite:publicationCta:longPress', { - url: view.source?.uri || '', - }) + const onLongPress = IS_NATIVE + ? () => { + if (view.source?.uri) { + playHaptic('Heavy') + void shareUrl(view.source.uri) + if (highlightedPublisher) { + ax.metric('embed:standardSite:subscribe:longPress', { + url: view.source?.uri || '', + }) + } else { + ax.metric('embed:standardSite:publicationCta:longPress', { + url: view.source?.uri || '', + }) + } + } } - } - } + : undefined const button = ( Date: Sat, 6 Jun 2026 00:13:26 +0300 Subject: [PATCH 51/61] Let specified facets take priority in feed descriptions (#10740) --- eslint-suppressions.json | 3 - .../Profile/components/ProfileFeedHeader.tsx | 74 ++++++++----------- src/state/queries/feed.ts | 28 +++++-- 3 files changed, 50 insertions(+), 55 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 1c4c89f38..5bd3357d1 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1346,9 +1346,6 @@ } }, "src/screens/Profile/components/ProfileFeedHeader.tsx": { - "@typescript-eslint/no-floating-promises": { - "count": 1 - }, "@typescript-eslint/no-misused-promises": { "count": 5 } diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx index 6966fa6fe..c640c84f3 100644 --- a/src/screens/Profile/components/ProfileFeedHeader.tsx +++ b/src/screens/Profile/components/ProfileFeedHeader.tsx @@ -1,9 +1,7 @@ import {useCallback, useMemo, useState} from 'react' import {View} from 'react-native' import {AtUri} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Plural, Trans} from '@lingui/react/macro' +import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useHaptics} from '#/lib/haptics' import {makeCustomFeedLink, makeProfileLink} from '#/lib/routes/links' @@ -26,7 +24,6 @@ import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Divider} from '#/components/Divider' -import {useRichText} from '#/components/hooks/useRichText' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' @@ -86,7 +83,7 @@ export function ProfileFeedHeaderSkeleton() { export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { const t = useTheme() - const {_, i18n} = useLingui() + const {t: l, i18n} = useLingui() const ax = useAnalytics() const {hasSession} = useSession() const {gtMobile} = useBreakpoints() @@ -121,7 +118,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { if (savedFeedConfig) { await removeFeed(savedFeedConfig) - Toast.show(_(msg`Removed from your feeds`)) + Toast.show(l`Removed from your feeds`) ax.metric('feed:unsave', {feedUrl: info.uri}) } else { await addSavedFeeds([ @@ -131,14 +128,12 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { pinned: false, }, ]) - Toast.show(_(msg`Saved to your feeds`)) + Toast.show(l`Saved to your feeds`) ax.metric('feed:save', {feedUrl: info.uri}) } } catch (err) { Toast.show( - _( - msg`There was an issue updating your feeds, please check your internet connection and try again.`, - ), + l`There was an issue updating your feeds, please check your internet connection and try again.`, { type: 'error', }, @@ -161,10 +156,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { ]) if (pinned) { - Toast.show(_(msg`Pinned ${info.displayName} to Home`)) + Toast.show(l`Pinned ${info.displayName} to Home`) ax.metric('feed:pin', {feedUrl: info.uri}) } else { - Toast.show(_(msg`Unpinned ${info.displayName} from Home`)) + Toast.show(l`Unpinned ${info.displayName} from Home`) ax.metric('feed:unpin', {feedUrl: info.uri}) } } else { @@ -175,11 +170,11 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { pinned: true, }, ]) - Toast.show(_(msg`Pinned ${info.displayName} to Home`)) + Toast.show(l`Pinned ${info.displayName} to Home`) ax.metric('feed:pin', {feedUrl: info.uri}) } } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), { + Toast.show(l`There was an issue contacting the server`, { type: 'error', }) logger.error('Failed to toggle pinned feed', {message: e}) @@ -194,7 +189,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
- - - + {typeof likeCount === 'number' && ( control.close()}> @@ -502,13 +489,12 @@ function DialogInner({ )} - {hasSession && ( <>