From e58feaeb0f55759dd4b6b7505983d7d6a132c226 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Apr 2026 05:45:56 -0700 Subject: [PATCH 01/18] Animate border radius in lightbox transition (#10272) --- src/components/Post/Embed/ImageEmbed.tsx | 3 ++- .../com/lightbox/ImageViewing/@types/index.ts | 10 +++++++ .../ImageItem/ImageItem.android.tsx | 15 +++-------- .../components/ImageItem/ImageItem.ios.tsx | 15 +++-------- .../components/ImageItem/ImageItem.tsx | 12 ++------- src/view/com/lightbox/ImageViewing/index.tsx | 26 ++++++++++++++++--- 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index 6ca166ff10..2adc33b6e6 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -4,7 +4,7 @@ import {Image} from 'expo-image' import {useLightboxControls} from '#/state/lightbox' import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' -import {atoms as a} from '#/alf' +import {atoms as a, tokens} from '#/alf' import {AutoSizedImage} from '#/components/images/AutoSizedImage' import {Gallery} from '#/components/images/Gallery' import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid' @@ -42,6 +42,7 @@ export function ImageEmbed({ thumbRect: null, thumbRef: refs[i] ?? null, thumbDimensions: fetchedDims[i] ?? null, + thumbBorderRadius: tokens.borderRadius.md, type: 'image', })), index, diff --git a/src/view/com/lightbox/ImageViewing/@types/index.ts b/src/view/com/lightbox/ImageViewing/@types/index.ts index 8435513af0..c3aca3d859 100644 --- a/src/view/com/lightbox/ImageViewing/@types/index.ts +++ b/src/view/com/lightbox/ImageViewing/@types/index.ts @@ -29,6 +29,7 @@ export type ImageSource = { thumbDimensions: Dimensions | null thumbRect: MeasuredDimensions | null thumbRef?: AnimatedRef | null + thumbBorderRadius?: number alt?: string type: 'image' | 'circle-avi' | 'rect-avi' } @@ -37,3 +38,12 @@ export type Transform = Exclude< TransformsStyle['transform'], string | undefined > + +export type LightboxTransforms = { + scaleAndMoveTransform: Transform + cropFrameTransform: Transform + cropContentTransform: Transform + borderRadius: number + isResting: boolean + isHidden: boolean +} diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 6e045ede34..9d128a41cf 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -19,7 +19,7 @@ import {Image} from 'expo-image' import { type Dimensions as ImageDimensions, type ImageSource, - type Transform, + type LightboxTransforms, } from '../../@types' import { applyRounding, @@ -53,15 +53,7 @@ type Props = { imageAspect: number | undefined imageDimensions: ImageDimensions | undefined dismissSwipePan: PanGesture - transforms: Readonly< - SharedValue<{ - scaleAndMoveTransform: Transform - cropFrameTransform: Transform - cropContentTransform: Transform - isResting: boolean - isHidden: boolean - }> - > + transforms: Readonly> } const ImageItem = ({ imageSrc, @@ -339,11 +331,12 @@ const ImageItem = ({ }) const imageCropStyle = useAnimatedStyle(() => { - const {cropFrameTransform} = transforms.get() + const {cropFrameTransform, borderRadius: br} = transforms.get() return { flex: 1, overflow: 'hidden', transform: cropFrameTransform, + borderRadius: br, } }) diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index 2af9816301..52173e057a 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -29,7 +29,7 @@ import {Image} from 'expo-image' import { type Dimensions as ImageDimensions, type ImageSource, - type Transform, + type LightboxTransforms, } from '../../@types' const MAX_ORIGINAL_IMAGE_ZOOM = 2 @@ -52,15 +52,7 @@ type Props = { imageAspect: number | undefined imageDimensions: ImageDimensions | undefined dismissSwipePan: PanGesture - transforms: Readonly< - SharedValue<{ - scaleAndMoveTransform: Transform - cropFrameTransform: Transform - cropContentTransform: Transform - isResting: boolean - isHidden: boolean - }> - > + transforms: Readonly> } const ImageItem = ({ @@ -170,10 +162,11 @@ const ImageItem = ({ const imageCropStyle = useAnimatedStyle(() => { const screenSize = measureSafeArea() - const {cropFrameTransform} = transforms.get() + const {cropFrameTransform, borderRadius: br} = transforms.get() return { overflow: 'hidden', transform: cropFrameTransform, + borderRadius: br, width: screenSize.width, maxHeight: screenSize.height, alignSelf: 'center', diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx index 767fcadc20..cd0e45d3c5 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx @@ -9,7 +9,7 @@ import {type Dimensions} from '#/lib/media/types' import { type Dimensions as ImageDimensions, type ImageSource, - type Transform, + type LightboxTransforms, } from '../../@types' type Props = { @@ -29,15 +29,7 @@ type Props = { imageAspect: number | undefined imageDimensions: ImageDimensions | undefined dismissSwipePan: PanGesture - transforms: Readonly< - SharedValue<{ - scaleAndMoveTransform: Transform - cropFrameTransform: Transform - cropContentTransform: Transform - isResting: boolean - isHidden: boolean - }> - > + transforms: Readonly> } const ImageItem = (_props: Props) => { diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 82d1ce27a5..40cabd722d 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -52,7 +52,11 @@ import {useTheme} from '#/alf' import {setSystemUITheme} from '#/alf/util/systemUI' import {IS_IOS} from '#/env' import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' -import {type ImageSource, type Transform} from './@types' +import { + type ImageSource, + type LightboxTransforms, + type Transform, +} from './@types' import ImageDefaultHeader from './components/ImageDefaultHeader' import ImageItem from './components/ImageItem/ImageItem' @@ -494,8 +498,8 @@ function LightboxImage({ return safeArea }, [safeAreaRef, heightDelayedForJSThreadOnly, widthDelayedForJSThreadOnly]) - const {thumbRect: thumbRectJS} = imageSrc - const transforms = useDerivedValue(() => { + const {thumbRect: thumbRectJS, thumbBorderRadius} = imageSrc + const transforms = useDerivedValue(() => { 'worklet' const safeArea = measureSafeArea() const openProgressValue = openProgress.get() @@ -506,6 +510,7 @@ function LightboxImage({ return { isHidden: true, isResting: false, + borderRadius: 0, scaleAndMoveTransform: [], cropFrameTransform: [], cropContentTransform: [], @@ -525,12 +530,14 @@ function LightboxImage({ thumbRect, safeArea, imageAspect, + thumbBorderRadius, ) } } return { isHidden: false, isResting: dismissTranslateY === 0, + borderRadius: 0, scaleAndMoveTransform: [{translateY: dismissTranslateY}], cropFrameTransform: [], cropContentTransform: [], @@ -772,10 +779,12 @@ function interpolateTransform( }, safeArea: {width: number; height: number; x: number; y: number}, imageAspect: number, + thumbBorderRadius?: number, ): { scaleAndMoveTransform: Transform cropFrameTransform: Transform cropContentTransform: Transform + borderRadius: number isResting: boolean isHidden: boolean } { @@ -827,12 +836,23 @@ function interpolateTransform( [0, 1], [croppedFinalHeight / finalHeight, 1], ) + // The border radius in the source thumbnail needs to be scaled to account + // for the crop frame and overall scale so it visually matches at progress=0. + const sourceBorderRadius = thumbBorderRadius ?? 0 + const initialCropScaleX = croppedFinalWidth / finalWidth + const borderRadius = interpolate( + progress, + [0, 1], + [sourceBorderRadius / (initialScale * initialCropScaleX), 0], + ) + return { isHidden: false, isResting: progress === 1, scaleAndMoveTransform: [{translateX}, {translateY}, {scale}], cropFrameTransform: [{scaleX: cropScaleX}, {scaleY: cropScaleY}], cropContentTransform: [{scaleX: 1 / cropScaleX}, {scaleY: 1 / cropScaleY}], + borderRadius, } } From 226a321a2726b8a8cc395dbf87d88a3cc2b8e7bc Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Apr 2026 10:12:12 -0700 Subject: [PATCH 02/18] Fix GrowthBook cache breaking on cold start (#10282) Co-authored-by: Claude Opus 4.6 (1M context) --- src/analytics/features/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/analytics/features/index.ts b/src/analytics/features/index.ts index 4644d7f1b4..7ec6f359a7 100644 --- a/src/analytics/features/index.ts +++ b/src/analytics/features/index.ts @@ -14,8 +14,7 @@ const CACHE = new MMKV({id: 'bsky_features_cache'}) setPolyfills({ localStorage: { getItem: key => { - const value = CACHE.getString(key) - return value != null ? JSON.parse(value) : null + return CACHE.getString(key) ?? null }, setItem: async (key, value) => { CACHE.set(key, value) @@ -29,7 +28,7 @@ setPolyfills({ */ export type FeatureFetchStrategy = 'prefer-low-latency' | 'prefer-fresh-gates' -const TIMEOUT_INIT = 500 // TODO should base on p99 or something +const TIMEOUT_INIT = 2000 // TODO should base on p99 or something const TIMEOUT_PREFER_LOW_LATENCY = 250 const TIMEOUT_PREFER_FRESH_GATES = 1500 From 9e9ff70682a48f751196b0807322c730fea7202b Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:40:37 -0700 Subject: [PATCH 03/18] Enable access for group clip clops with no messages (#10276) --- src/components/dms/MessagesListHeader.tsx | 233 +++++++++++++--------- src/screens/Messages/Conversation.tsx | 34 ++-- 2 files changed, 157 insertions(+), 110 deletions(-) diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 4d7c2d7e33..a2f32ab3de 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -13,7 +13,11 @@ import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/profile-shadow' -import {isConvoActive, useConvo} from '#/state/messages/convo' +import { + type ActiveConvoStates, + isConvoActive, + useConvo, +} from '#/state/messages/convo' import {type ConvoItem} from '#/state/messages/convo/types' import {useSession} from '#/state/session' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' @@ -36,10 +40,13 @@ export function MessagesListHeader({ moderation, }: { profile?: Shadow - moderation?: ModerationDecision + moderation?: ModerationDecision | null }) { const t = useTheme() + const convoState = useConvo() + const isGroupChat = convoState?.isGroup?.() + const blockInfo = useMemo(() => { if (!moderation) return const modui = moderation.ui('profileView') @@ -58,12 +65,21 @@ export function MessagesListHeader({ - {profile && moderation && blockInfo ? ( - + {isConvoActive(convoState) ? ( + moderation && blockInfo && profile && !isGroupChat ? ( + + ) : ( + + ) ) : ( <> @@ -94,11 +110,13 @@ export function MessagesListHeader({ ) } -function HeaderReady({ +function ProfileHeaderReady({ + convoState, profile, moderation, blockInfo, }: { + convoState: ActiveConvoStates profile: Shadow moderation: ModerationDecision blockInfo: { @@ -107,21 +125,12 @@ function HeaderReady({ } }) { const {t: l} = useLingui() - const t = useTheme() - const convoState = useConvo() const {currentAccount} = useSession() - const navigation = useNavigation() - - const groupInfo = convoState.getGroupInfo?.() - const isGroupChat = groupInfo != null - const isDeletedAccount = profile?.handle === 'missing.invalid' - const displayName = isGroupChat - ? (groupInfo.name ?? l`${profile.handle}'s group chat`) - : isDeletedAccount - ? l`Deleted Account` - : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) + const displayName = isDeletedAccount + ? l`Deleted Account` + : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) const latestMessageFromOther = convoState.items.findLast( (item: ConvoItem) => @@ -134,6 +143,63 @@ function HeaderReady({ ? latestMessageFromOther.message : undefined + return ( + + + + + } + muted={convoState.convo?.muted} + settings={ + isConvoActive(convoState) ? ( + + ) : null + } + /> + ) +} + +function GroupHeaderReady({ + convoState, + profile, + moderation, +}: { + convoState: ActiveConvoStates + profile?: Shadow + moderation?: ModerationDecision | null +}) { + const {t: l} = useLingui() + + const navigation = useNavigation() + + const groupInfo = convoState.getGroupInfo?.() + + const isDeletedAccount = profile?.handle === 'missing.invalid' + const displayName = isDeletedAccount + ? l`Deleted Account` + : profile + ? createSanitizedDisplayName(profile, true, moderation?.ui('displayName')) + : undefined + const groupName = + groupInfo?.name ?? + (displayName ? l`${displayName}’s group chat` : l`Group chat`) + const handleNavigateToSettings = () => { const convoId = convoState.convo?.id if (convoId) { @@ -145,84 +211,67 @@ function HeaderReady({ } } + return ( + + + + {groupName} + + + } + muted={convoState.convo?.muted} + settings={ + isConvoActive(convoState) ? ( + + ) : null + } + /> + ) +} + +function Wrapper({ + heading, + muted, + settings, +}: { + heading: React.ReactNode + muted: boolean + settings: React.ReactNode +}) { return ( - {isGroupChat ? ( - - - - {displayName} - - - ) : ( - - - - - - {displayName} - - - {convoState.convo?.muted && ( - <> - - {' '} - ·{' '} - - - - )} - - - - )} + + {heading} + + - - {isConvoActive(convoState) ? ( - isGroupChat ? ( - - ) : ( - - ) - ) : null} - + {settings} ) } + +function MuteStatus({muted}: {muted: boolean}) { + const t = useTheme() + + return muted ? ( + <> + · + + + ) : undefined +} diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index ec082bb8b8..f56f612e28 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -173,16 +173,12 @@ function Inner() { )} - {moderation && recipient ? ( - - ) : ( - - )} + {!readyToShow && ( + moderation: ModerationDecision | null + recipient: Shadow | undefined hasScrolled: boolean setHasScrolled: React.Dispatch> }) { @@ -289,12 +285,14 @@ function InnerReady({ hasAcceptOverride={!!params.accept} transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0} footer={ - 0} - moderation={moderation} - /> + moderation && recipient ? ( + 0} + moderation={moderation} + /> + ) : null } /> )} From bc9ad2c2d95c38753ce43d257d8131d1e52effd9 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:48:08 -0700 Subject: [PATCH 04/18] Fix crash when deleting account from NoAccessScreen (#10283) --- src/components/Link.tsx | 10 ++-- .../components/DeleteAccountDialog.tsx | 50 ++++++++----------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 653e281c10..7d55a5f48b 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -141,18 +141,18 @@ export function useLink({ }) } else { if (isExternal) { - openLink(href, overridePresentation, shouldProxy) + void openLink(href, overridePresentation, shouldProxy) } else { const shouldOpenInNewTab = shouldClickOpenNewTab(e) if (isBskyDownloadUrl(href)) { - shareUrl(BSKY_DOWNLOAD_URL) + void shareUrl(BSKY_DOWNLOAD_URL) } else if ( shouldOpenInNewTab || href.startsWith('http') || href.startsWith('mailto') ) { - openLink(href) + void openLink(href) } else { closeModal() // close any active modals @@ -232,7 +232,7 @@ export function useLink({ share: true, }) } else { - shareUrl(href) + void shareUrl(href) } }, [ disableMismatchWarning, @@ -451,7 +451,7 @@ export function SimpleInlineLinkText({ const onPress = (e: GestureResponderEvent) => { const exitEarlyIfFalse = outerOnPress?.(e) if (exitEarlyIfFalse === false) return - Linking.openURL(href) + void Linking.openURL(href) } return ( diff --git a/src/screens/Settings/components/DeleteAccountDialog.tsx b/src/screens/Settings/components/DeleteAccountDialog.tsx index f7f33f75eb..9903fce3ba 100644 --- a/src/screens/Settings/components/DeleteAccountDialog.tsx +++ b/src/screens/Settings/components/DeleteAccountDialog.tsx @@ -9,7 +9,7 @@ import {useCleanError} from '#/lib/hooks/useCleanError' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {useAgent, useSession, useSessionApi} from '#/state/session' -import {atoms as a, useTheme, web} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {type DialogOuterProps} from '#/components/Dialog' import { @@ -19,7 +19,7 @@ import { import * as TextField from '#/components/forms/TextField' import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {createStaticClick, InlineLinkText} from '#/components/Link' +import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import * as toast from '#/components/Toast' @@ -113,12 +113,9 @@ function DeleteAccountDialogInner({ } const token = confirmCode.replace(WHITESPACE_RE, '') // Inform chat service of intent to delete account. - const {success} = await agent.api.chat.bsky.actor.deleteAccount( - undefined, - { - headers: DM_SERVICE_HEADERS, - }, - ) + const {success} = await agent.chat.bsky.actor.deleteAccount(undefined, { + headers: DM_SERVICE_HEADERS, + }) if (!success) { throw new Error('Failed to inform chat service of account deletion') } @@ -213,11 +210,11 @@ function DeleteAccountDialogInner({ You can also{' '} - + temporarily deactivate - {' '} + {' '} your account instead. 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. @@ -262,24 +259,20 @@ function DeleteAccountDialogInner({ {emailSentCount > 1 ? ( Email sent!{' '} - { - void handleSendEmail() - })}> + Click here to resend. - + ) : ( Don’t see a code?{' '} - { - void handleSendEmail() - })}> + Click here to resend. - + )}{' '} @@ -340,12 +333,11 @@ function DeleteAccountDialogInner({ {currentHandle} {' '} and all associated data. Note that this will affect any other{' '} - + AT Protocol - {' '} + {' '} services you use with this account. From 591504307d89edceac04743767bc105a6d83459f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Apr 2026 10:50:52 -0700 Subject: [PATCH 05/18] [Chat] Minor bugfixes (#10284) --- src/components/dms/MessageItemEmbed.tsx | 1 - src/components/dms/MessagesListHeader.tsx | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx index ba48b6e123..4c8112fd40 100644 --- a/src/components/dms/MessageItemEmbed.tsx +++ b/src/components/dms/MessageItemEmbed.tsx @@ -28,7 +28,6 @@ let MessageItemEmbed = ({ - + + + {displayName} + + + } muted={convoState.convo?.muted} From adca192f3afbfe227b2d54d7387d557f1d3f502a Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:44:34 -0700 Subject: [PATCH 06/18] Add ability to edit group clip clop name (#10275) --- src/screens/Messages/ConversationSettings.tsx | 26 +++-- src/state/messages/convo/agent.ts | 28 ++++- src/state/messages/convo/index.tsx | 11 +- src/state/queries/messages/edit-group-name.ts | 108 ++++++++++++++++++ 4 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 src/state/queries/messages/edit-group-name.ts diff --git a/src/screens/Messages/ConversationSettings.tsx b/src/screens/Messages/ConversationSettings.tsx index 892e2e2c45..c926afb62d 100644 --- a/src/screens/Messages/ConversationSettings.tsx +++ b/src/screens/Messages/ConversationSettings.tsx @@ -20,6 +20,7 @@ import {type Shadow} from '#/state/cache/types' import {ConvoProvider, useConvo} from '#/state/messages/convo' import {ConvoStatus} from '#/state/messages/convo/types' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useEditGroupName} from '#/state/queries/messages/edit-group-name' import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' @@ -717,11 +718,26 @@ function SettingsHeader({ const convoState = useConvo() const {currentAccount} = useSession() + const groupName = convoState.getGroupInfo?.()?.name ?? '' + const [newGroupName, setNewGroupName] = useState(groupName) + + const [isLocked, setIsLocked] = useState(false) + const isOwner = currentAccount?.did == null ? false : convoState.getPrimaryMember?.()?.did === currentAccount.did + const {mutate: editGroupName} = useEditGroupName(convo.id, { + onError: e => { + setNewGroupName(groupName) + logger.error('Failed to edit group chat name', {message: e}) + Toast.show(l`Failed to edit group chat name`, { + type: 'error', + }) + }, + }) + const {mutate: muteConvo} = useMuteConvo(convo.id, { onSuccess: data => { if (data.convo.muted) { @@ -755,13 +771,6 @@ function SettingsHeader({ const lockChatPrompt = Prompt.usePromptControl() const leaveChatPrompt = Prompt.usePromptControl() - const [groupName, setGroupName] = useState( - convoState.getGroupInfo?.()?.name ?? '', - ) - const [newGroupName, setNewGroupName] = useState(groupName) - - const [isLocked, setIsLocked] = useState(false) - const handleToggleMute = () => { muteConvo({mute: !convo?.muted}) } @@ -777,7 +786,7 @@ function SettingsHeader({ } const handleEditName = () => { - setGroupName(newGroupName) + editGroupName({name: newGroupName}) editNamePrompt.close() } @@ -1045,6 +1054,7 @@ function EditNamePrompt({ autoCapitalize="none" autoComplete="off" autoCorrect={false} + autoFocus onSubmitEditing={onConfirm} /> diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 9f0693c4a3..f89155bec4 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -116,6 +116,7 @@ export class Convo { this.isGroup = this.isGroup.bind(this) this.getGroupInfo = this.getGroupInfo.bind(this) this.getPrimaryMember = this.getPrimaryMember.bind(this) + this.updateGroupName = this.updateGroupName.bind(this) } private commit() { @@ -655,7 +656,7 @@ export class Convo { const nextCursor = this.oldestRev // for TS const response = await networkRetry(2, () => { - return this.agent.api.chat.bsky.convo.getMessages( + return this.agent.chat.bsky.convo.getMessages( { cursor: nextCursor, convoId: this.convoId, @@ -889,6 +890,25 @@ export class Convo { this.commit() } + updateGroupName(name: string) { + if ( + this.convo && + bsky.dangerousIsType( + this.convo.kind, + ChatBskyConvoDefs.isGroupConvo, + ) + ) { + this.convo = { + ...this.convo, + kind: { + ...this.convo.kind, + name, + }, + } + } + this.commit() + } + async processPendingMessages() { logger.debug( `processing messages (${this.pendingMessages.size} remaining)`, @@ -1388,14 +1408,14 @@ export class Convo { getPrimaryMember(): ChatBskyActorDefs.ProfileViewBasic | undefined { if (this.isGroup()) { - return this.recipients?.find(r => { + return this.convo?.members.find(m => { if ( bsky.dangerousIsType( - r.kind, + m.kind, ChatBskyActorDefs.isGroupConvoMember, ) ) { - return r.kind.role === 'owner' + return m.kind.role === 'owner' } else { throw new Error( 'Expected a GroupConvoMember, got an unknown kind of member', diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index 4117aef2ed..5461301fb3 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -6,7 +6,7 @@ import { useState, useSyncExternalStore, } from 'react' -import {type ChatBskyConvoDefs} from '@atproto/api' +import {ChatBskyConvoDefs} from '@atproto/api' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -130,6 +130,15 @@ export function ConvoProvider({ if (data && convo.convo && data.muted !== convo.convo.muted) { convo.updateMuted(data.muted) } + if ( + data && + convo.convo && + ChatBskyConvoDefs.isGroupConvo(data.kind) && + ChatBskyConvoDefs.isGroupConvo(convo.convo.kind) && + data.kind.name !== convo.convo.kind.name + ) { + convo.updateGroupName(data.kind.name) + } } }) }, [convo, convoId, queryClient]) diff --git a/src/state/queries/messages/edit-group-name.ts b/src/state/queries/messages/edit-group-name.ts new file mode 100644 index 0000000000..cbff0331ed --- /dev/null +++ b/src/state/queries/messages/edit-group-name.ts @@ -0,0 +1,108 @@ +import { + ChatBskyConvoDefs, + type ChatBskyConvoListConvos, + type ChatBskyGroupEditGroup, +} from '@atproto/api' +import { + type InfiniteData, + 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 {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations' + +export function useEditGroupName( + convoId: string | undefined, + { + onSuccess, + onError, + }: { + onSuccess?: (data: ChatBskyGroupEditGroup.OutputSchema) => void + onError?: (error: Error) => void + }, +) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async ({name: groupName}: {name: string}) => { + if (!convoId) throw new Error('No convoId provided') + const {data} = await agent.chat.bsky.group.editGroup( + {convoId, name: groupName}, + {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, + ) + return data + }, + onMutate: ({name: groupName}) => { + if (!convoId) return + + const prevConvo = queryClient.getQueryData( + CONVO_KEY(convoId), + ) + const prevListEntries = queryClient.getQueriesData< + InfiniteData + >({queryKey: [CONVO_LIST_KEY]}) + + // Update for a single chat thread + queryClient.setQueryData( + CONVO_KEY(convoId), + prev => { + if (!prev) return + if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev + return { + ...prev, + kind: { + ...prev.kind, + name: groupName, + }, + } + }, + ) + + // Update for the chat list + queryClient.setQueriesData< + InfiniteData + >({queryKey: [CONVO_LIST_KEY]}, prev => { + if (!prev?.pages) return + return { + ...prev, + pages: prev.pages.map(page => ({ + ...page, + convos: page.convos.map(convo => { + if (convo.id !== convoId) return convo + if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo + return { + ...convo, + kind: { + ...convo.kind, + name: groupName, + }, + } + }), + })), + } + }) + + return {prevConvo, prevListEntries} + }, + onSuccess: data => { + onSuccess?.(data) + }, + onError: (e, _variables, context) => { + logger.error(e) + if (context?.prevConvo && convoId) { + queryClient.setQueryData(CONVO_KEY(convoId), context.prevConvo) + } + if (context?.prevListEntries) { + for (const [key, data] of context.prevListEntries) { + queryClient.setQueryData(key, data) + } + } + onError?.(e) + }, + }) +} From 43108533ebddd732e1273c397d2ecb802bad4ecc Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Apr 2026 11:44:48 -0700 Subject: [PATCH 07/18] [Chat] Fix embed offset (#10285) --- src/components/dms/MessageItemEmbed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/dms/MessageItemEmbed.tsx b/src/components/dms/MessageItemEmbed.tsx index 4c8112fd40..a1f6abe28b 100644 --- a/src/components/dms/MessageItemEmbed.tsx +++ b/src/components/dms/MessageItemEmbed.tsx @@ -28,6 +28,7 @@ let MessageItemEmbed = ({ Date: Fri, 17 Apr 2026 14:24:42 -0500 Subject: [PATCH 08/18] Squash carousel in chat messages, fix rounding (#10286) --- src/components/Post/Embed/index.tsx | 16 ++++++---------- src/components/Post/Embed/types.ts | 5 +---- src/components/dms/MessageItemEmbed.tsx | 4 ++-- src/components/images/Gallery/index.tsx | 12 ++++++++---- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index c9dae73a7e..0c3106c1b9 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -39,12 +39,11 @@ import {PostPlaceholder as PostPlaceholderText} from './PostPlaceholder' import { type CommonProps, type EmbedProps, - PostEmbedViewContext, - QuoteEmbedViewContext, + type PostEmbedViewContext, } from './types' import {VideoEmbed} from './VideoEmbed' -export {PostEmbedViewContext, QuoteEmbedViewContext} from './types' +export {PostEmbedViewContext} from './types' export function Embed({embed: rawEmbed, ...rest}: EmbedProps) { const embed = parseEmbed(rawEmbed) @@ -164,11 +163,7 @@ function RecordEmbed({ @@ -229,9 +224,10 @@ export function QuoteEmbed({ linkDisabled, isWithinQuote: parentIsWithinQuote, allowNestedQuotes: parentAllowNestedQuotes, + viewContext, }: Omit & { embed: EmbedType<'post'> - viewContext?: QuoteEmbedViewContext + viewContext?: PostEmbedViewContext linkDisabled?: boolean }) { const moderationOpts = useModerationOpts() @@ -309,7 +305,7 @@ export function QuoteEmbed({ { + if (isWithinChat) { + return 120 + } if (bps.gtMobile) { return 300 } else if (bps.gtPhone) { @@ -111,10 +118,7 @@ export function Gallery({ } else { return 200 } - }, [bps]) - const isWithinQuote = - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - const hideBadges = isWithinQuote + }, [bps, isWithinChat]) /* * Container overflow styles From feebc6a98b18dfa5717233be005f19e70cb250e9 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:50:41 -0700 Subject: [PATCH 09/18] Fix layout issue with avatar bubbles (#10289) --- src/components/AvatarBubbles.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarBubbles.tsx b/src/components/AvatarBubbles.tsx index 44fd7f4e90..2ed58f5ad3 100644 --- a/src/components/AvatarBubbles.tsx +++ b/src/components/AvatarBubbles.tsx @@ -3,6 +3,7 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' import Animated, { Easing, interpolate, + type SharedValue, useAnimatedStyle, useSharedValue, withDelay, @@ -191,7 +192,7 @@ function AvatarBubble({ includeProfileBorder, }: { profile?: bsky.profile.AnyProfileView - scale: Animated.SharedValue + scale: SharedValue size: number style?: StyleProp x: number @@ -214,7 +215,6 @@ function AvatarBubble({ a.absolute, a.rounded_full, a.flex_grow_0, - {transform: [{translateX: x}, {translateY: y}]}, includeProfileBorder && { borderColor: t.atoms.text_inverted.color, borderWidth: 2, From 935347c73d6058834bef08e2d3da86a7ded037ed Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:23:44 -0700 Subject: [PATCH 10/18] Use convo view instead of convo state for driving the UI (#10290) --- src/components/dms/MessagesListHeader.tsx | 96 +++++++--------- src/screens/Messages/Conversation.tsx | 47 ++++++-- src/screens/Messages/ConversationSettings.tsx | 104 +++++++++--------- 3 files changed, 127 insertions(+), 120 deletions(-) diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index c8fa7116dd..13e747231f 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -2,6 +2,7 @@ import {useMemo} from 'react' import {View} from 'react-native' import { type AppBskyActorDefs, + ChatBskyConvoDefs, type ModerationCause, type ModerationDecision, } from '@atproto/api' @@ -11,14 +12,7 @@ import {useNavigation} from '@react-navigation/native' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' -import {logger} from '#/logger' import {type Shadow} from '#/state/cache/profile-shadow' -import { - type ActiveConvoStates, - isConvoActive, - useConvo, -} from '#/state/messages/convo' -import {type ConvoItem} from '#/state/messages/convo/types' import {useSession} from '#/state/session' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' @@ -32,20 +26,22 @@ import {Link} from '#/components/Link' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {IS_LIQUID_GLASS, IS_WEB} from '#/env' +import {type ConvoWithDetails} from './util' const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE export function MessagesListHeader({ + convo, profile, moderation, }: { + convo?: ConvoWithDetails | null profile?: Shadow moderation?: ModerationDecision | null }) { const t = useTheme() - const convoState = useConvo() - const isGroupChat = convoState?.isGroup?.() + const isGroupChat = convo?.kind === 'group' const blockInfo = useMemo(() => { if (!moderation) return @@ -65,17 +61,17 @@ export function MessagesListHeader({ - {isConvoActive(convoState) ? ( + {convo ? ( moderation && blockInfo && profile && !isGroupChat ? ( ) : ( @@ -111,12 +107,12 @@ export function MessagesListHeader({ } function ProfileHeaderReady({ - convoState, + convo, profile, moderation, blockInfo, }: { - convoState: ActiveConvoStates + convo: ConvoWithDetails profile: Shadow moderation: ModerationDecision blockInfo: { @@ -132,15 +128,10 @@ function ProfileHeaderReady({ ? l`Deleted Account` : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) - const latestMessageFromOther = convoState.items.findLast( - (item: ConvoItem) => - item.type === 'message' && - item.message.sender.did !== currentAccount?.did, - ) - const latestReportableMessage = - latestMessageFromOther?.type === 'message' - ? latestMessageFromOther.message + ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) && + convo.view.lastMessage.sender?.did !== currentAccount?.did + ? convo.view.lastMessage : undefined return ( @@ -164,28 +155,26 @@ function ProfileHeaderReady({ } - muted={convoState.convo?.muted} + muted={convo.view.muted} settings={ - isConvoActive(convoState) ? ( - - ) : null + } /> ) } function GroupHeaderReady({ - convoState, + convo, profile, moderation, }: { - convoState: ActiveConvoStates + convo: ConvoWithDetails profile?: Shadow moderation?: ModerationDecision | null }) { @@ -193,7 +182,7 @@ function GroupHeaderReady({ const navigation = useNavigation() - const groupInfo = convoState.getGroupInfo?.() + const groupInfo = convo.kind === 'group' ? convo.details : undefined const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount @@ -206,40 +195,33 @@ function GroupHeaderReady({ (displayName ? l`${displayName}’s group chat` : l`Group chat`) const handleNavigateToSettings = () => { - const convoId = convoState.convo?.id - if (convoId) { - navigation.navigate('MessagesConversationSettings', { - conversation: convoId, - }) - } else { - logger.error(`handleNavigateToSettings: missing convo ID`) - } + navigation.navigate('MessagesConversationSettings', { + conversation: convo.view.id, + }) } return ( - + {groupName} } - muted={convoState.convo?.muted} + muted={convo.view.muted} settings={ - isConvoActive(convoState) ? ( - - ) : null + } /> ) diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index f56f612e28..5e390f7a0c 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -35,6 +35,7 @@ import {ConvoStatus} from '#/state/messages/convo/types' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import {MessagesList} from '#/screens/Messages/components/MessagesList' import {atoms as a, useTheme, web} from '#/alf' @@ -46,6 +47,7 @@ import { } from '#/components/dialogs/EmailDialog' import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' import {MessagesListHeader} from '#/components/dms/MessagesListHeader' +import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' import {Error} from '#/components/Error' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' @@ -104,9 +106,14 @@ function Inner() { const t = useTheme() const convoState = useConvo() const {_} = useLingui() + const {currentAccount} = useSession() const isFocused = useIsFocused() const {top: topInset} = useSafeAreaInsets() + const convo = convoState.convo + ? parseConvoView(convoState.convo, currentAccount?.did) + : null + const moderationOpts = useModerationOpts() const {data: recipientUnshadowed} = useProfileQuery({ did: convoState.getPrimaryMember?.()?.did, @@ -144,9 +151,13 @@ function Inner() { {moderation ? ( - + ) : ( - + )} {moderation ? ( - + ) : ( - + )} )} @@ -178,6 +193,9 @@ function Inner() { recipient={recipient} hasScrolled={hasScrolled} setHasScrolled={setHasScrolled} + convo={convo} + isActive={isConvoActive(convoState)} + hasMessages={isConvoActive(convoState) && convoState.items.length > 0} /> {!readyToShow && ( | undefined hasScrolled: boolean setHasScrolled: React.Dispatch> + convo: ConvoWithDetails | null + isActive: boolean + hasMessages: boolean }) { - const convoState = useConvo() const navigation = useNavigation() const {top: topInset} = useSafeAreaInsets() const [headerHeight, setHeaderHeight] = useState(0) @@ -262,7 +285,11 @@ function InnerReady({ }, [maybeBlockForEmailVerification]) const header = ( - + ) return ( @@ -277,7 +304,7 @@ function InnerReady({ ) : ( header )} - {isConvoActive(convoState) && ( + {isActive && ( 0} + convoId={convo.view.id} + hasMessages={hasMessages} moderation={moderation} /> ) : null diff --git a/src/screens/Messages/ConversationSettings.tsx b/src/screens/Messages/ConversationSettings.tsx index c926afb62d..8da97a31ec 100644 --- a/src/screens/Messages/ConversationSettings.tsx +++ b/src/screens/Messages/ConversationSettings.tsx @@ -1,6 +1,6 @@ import {useMemo, useState} from 'react' import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native' -import {type ChatBskyConvoDefs, moderateProfile} from '@atproto/api' +import {moderateProfile} from '@atproto/api' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {StackActions, useNavigation} from '@react-navigation/native' @@ -34,6 +34,7 @@ import {AvatarBubbles} from '#/components/AvatarBubbles' import {Button, type ButtonColor, ButtonIcon} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {AddMembersFlow} from '#/components/dms/AddMembersFlow' +import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' import {Error} from '#/components/Error' import * as TextField from '#/components/forms/TextField' import {useInteractionState} from '#/components/hooks/useInteractionState' @@ -126,9 +127,14 @@ function SettingsInner() { const convoState = useConvo() const {currentAccount} = useSession() - const primaryMember = convoState?.getPrimaryMember?.() - const data: bsky.profile.AnyProfileView[] = convoState.convo?.members ?? [] + const convo = convoState.convo + ? parseConvoView(convoState.convo, currentAccount?.did) + : null + const primaryMember = convo?.primaryMember + const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did + + const data: bsky.profile.AnyProfileView[] = convo?.members ?? [] const invites: string[] = [] const items = [ @@ -163,11 +169,23 @@ function SettingsInner() { function renderItem({item}: {item: Item}) { switch (item.type) { case 'MEMBERS_AND_REQUESTS': - return + return ( + + ) case 'ADD_MEMBERS_LINK': - return + return case 'CHAT_MEMBER': - return + return ( + + ) default: return null } @@ -194,8 +212,8 @@ function SettingsInner() { initialNumToRender={initialNumToRender} keyExtractor={keyExtractor} ListHeaderComponent={ - convoState.convo ? ( - + convo ? ( + ) : ( ) @@ -211,21 +229,15 @@ function SettingsInner() { function MembersAndRequests({ memberCount, requestCount, + isOwner, }: { memberCount: number requestCount: number + isOwner: boolean }) { const t = useTheme() const {t: l} = useLingui() - const convoState = useConvo() - const {currentAccount} = useSession() - - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - return ( @@ -254,20 +266,12 @@ function MembersAndRequests({ ) } -function AddMembersLink() { +function AddMembersLink({isOwner}: {isOwner: boolean}) { const t = useTheme() const {t: l} = useLingui() - const convoState = useConvo() - const {currentAccount} = useSession() - const addMembersControl = Dialog.useDialogControl() - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - if (!isOwner) { return null } @@ -354,9 +358,11 @@ function AddMembersLink() { function Member({ profile, status, + isOwner, }: { profile: Shadow status: 'owner' | 'member' | 'invited' + isOwner: boolean }) { const navigation = useNavigation() const t = useTheme() @@ -388,7 +394,9 @@ function Member({ break } } else { - statusBadge = + statusBadge = ( + + ) } return ( @@ -496,9 +504,11 @@ function StatusButton({ function MemberMenu({ profile, type, + isOwner, }: { profile: Shadow type: 'owner' | 'member' | 'invited' + isOwner: boolean }) { const navigation = useNavigation() const t = useTheme() @@ -506,16 +516,9 @@ function MemberMenu({ const ax = useAnalytics() const requireEmailVerification = useRequireEmailVerification() - const convoState = useConvo() - const {currentAccount} = useSession() const blockMemberPrompt = Prompt.usePromptControl() - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did) const {mutate: initiateConvo} = useGetConvoForMembers({ onSuccess: ({convo}) => { @@ -706,29 +709,22 @@ function MemberMenu({ function SettingsHeader({ convo, - profiles, + isOwner, }: { - convo: ChatBskyConvoDefs.ConvoView - profiles: bsky.profile.AnyProfileView[] + convo: ConvoWithDetails + isOwner: boolean }) { const t = useTheme() const {t: l} = useLingui() const navigation = useNavigation() - const convoState = useConvo() - const {currentAccount} = useSession() - const groupName = convoState.getGroupInfo?.()?.name ?? '' + const groupName = convo.kind === 'group' ? convo.details.name : '' const [newGroupName, setNewGroupName] = useState(groupName) const [isLocked, setIsLocked] = useState(false) - const isOwner = - currentAccount?.did == null - ? false - : convoState.getPrimaryMember?.()?.did === currentAccount.did - - const {mutate: editGroupName} = useEditGroupName(convo.id, { + const {mutate: editGroupName} = useEditGroupName(convo.view.id, { onError: e => { setNewGroupName(groupName) logger.error('Failed to edit group chat name', {message: e}) @@ -738,7 +734,7 @@ function SettingsHeader({ }, }) - const {mutate: muteConvo} = useMuteConvo(convo.id, { + const {mutate: muteConvo} = useMuteConvo(convo.view.id, { onSuccess: data => { if (data.convo.muted) { Toast.show(l({message: 'Group chat muted', context: 'toast'})) @@ -754,7 +750,7 @@ function SettingsHeader({ }, }) - const {mutate: leaveConvo} = useLeaveConvo(convo.id, { + const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, { onMutate: () => { navigation.dispatch(StackActions.pop(2)) }, @@ -772,7 +768,7 @@ function SettingsHeader({ const leaveChatPrompt = Prompt.usePromptControl() const handleToggleMute = () => { - muteConvo({mute: !convo?.muted}) + muteConvo({mute: !convo.view.muted}) } const handleLeaveChat = () => { @@ -815,7 +811,7 @@ function SettingsHeader({ - + {isOwner ? ( From 35411e88c98f54fb2ee293c7f8380d052d1a4124 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Sat, 18 Apr 2026 03:13:09 +0000 Subject: [PATCH 11/18] Nightly source-language update --- src/locale/locales/en/messages.po | 464 +++++++++++++++--------------- 1 file changed, 237 insertions(+), 227 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 16fde51ee6..0e033396b9 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -13,9 +13,9 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/screens/Messages/ConversationSettings.tsx:924 -#: src/screens/Messages/ConversationSettings.tsx:934 -#: src/screens/Messages/ConversationSettings.tsx:1011 +#: src/screens/Messages/ConversationSettings.tsx:931 +#: src/screens/Messages/ConversationSettings.tsx:941 +#: src/screens/Messages/ConversationSettings.tsx:1018 msgid "…" msgstr "…" @@ -265,11 +265,6 @@ msgstr "" msgid "{0}’s avatar" msgstr "{0}’s avatar" -#. placeholder {0}: profile.handle -#: src/components/dms/MessagesListHeader.tsx:121 -msgid "{0}'s group chat" -msgstr "{0}'s group chat" - #. How many days have passed, displayed in a narrow form #. placeholder {0}: diff.value #: src/lib/hooks/useTimeAgo.ts:171 @@ -302,6 +297,10 @@ msgstr "" msgid "{date} at {time}" msgstr "{date} at {time}" +#: src/components/dms/MessagesListHeader.tsx:195 +msgid "{displayName}’s group chat" +msgstr "{displayName}’s group chat" + #: src/lib/generate-starterpack.ts:104 #: src/screens/StarterPack/Wizard/index.tsx:200 msgid "{displayName}'s Starter Pack" @@ -510,7 +509,7 @@ msgstr "{MAX_DISPLAY_NAME, plural, other {Display name is too long. The maximum msgid "{MAX_HIDDEN_REPLIES, plural, other {You can hide a maximum of # replies.}}" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:239 +#: src/screens/Messages/ConversationSettings.tsx:252 msgid "{memberCount}/{MEMBER_LIMIT}" msgstr "{memberCount}/{MEMBER_LIMIT}" @@ -551,7 +550,7 @@ msgstr "" msgid "{rank}." msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:246 +#: src/screens/Messages/ConversationSettings.tsx:259 msgid "{requestCount, plural, one {# request} other {# requests}}" msgstr "{requestCount, plural, one {# request} other {# requests}}" @@ -782,7 +781,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:426 #: src/screens/Messages/components/RequestButtons.tsx:101 -#: src/screens/Messages/ConversationSettings.tsx:571 +#: src/screens/Messages/ConversationSettings.tsx:575 #: src/view/com/profile/ProfileMenu.tsx:188 msgctxt "toast" msgid "Account blocked" @@ -828,7 +827,7 @@ msgstr "" msgid "Account removed from quick access" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:558 +#: src/screens/Messages/ConversationSettings.tsx:562 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:88 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:310 #: src/view/com/profile/ProfileMenu.tsx:176 @@ -919,11 +918,11 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:1329 +#: src/view/com/composer/Composer.tsx:1317 msgid "Add another post" msgstr "" -#: src/view/com/composer/Composer.tsx:1987 +#: src/view/com/composer/Composer.tsx:1981 msgid "Add another post to thread" msgstr "" @@ -941,7 +940,7 @@ msgstr "" msgid "Add automation label to account" msgstr "Add automation label to account" -#: src/components/dms/EmojiReactionPicker.web.tsx:35 +#: src/components/dms/EmojiReactionPicker.web.tsx:31 msgid "Add emoji reaction" msgstr "" @@ -958,8 +957,8 @@ msgstr "" msgid "Add media to post" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:325 -#: src/screens/Messages/ConversationSettings.tsx:342 +#: src/screens/Messages/ConversationSettings.tsx:330 +#: src/screens/Messages/ConversationSettings.tsx:347 msgid "Add members" msgstr "Add members" @@ -1061,8 +1060,8 @@ msgstr "" msgid "Additional details (limit 300 characters)" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:386 -#: src/screens/Messages/ConversationSettings.tsx:609 +#: src/screens/Messages/ConversationSettings.tsx:393 +#: src/screens/Messages/ConversationSettings.tsx:613 msgid "Admin" msgstr "Admin" @@ -1220,7 +1219,7 @@ msgid "Already signed in as @{0}" msgstr "" #: src/components/images/AutoSizedImage.tsx:190 -#: src/components/images/Gallery/index.tsx:514 +#: src/components/images/Gallery/index.tsx:518 #: src/components/images/ImageLayoutGridItem.tsx:120 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94 #: src/view/com/composer/GifAltText.tsx:100 @@ -1258,7 +1257,7 @@ msgstr "" msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." msgstr "" -#: src/components/dialogs/GifSelect.tsx:255 +#: src/components/dialogs/GifSelect.tsx:269 #: src/components/dialogs/LanguageSelectDialog.tsx:344 msgid "An error has occurred" msgstr "" @@ -1341,7 +1340,7 @@ msgstr "" msgid "An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts." msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:1079 +#: src/screens/Messages/ConversationSettings.tsx:1087 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. Your name, avatar, and the name of the group chat will be visible to everyone" 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. Your name, avatar, and the name of the group chat will be visible to everyone" @@ -1532,7 +1531,7 @@ msgstr "" msgid "Archived post" msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:334 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:327 msgid "Are you really, really sure?" msgstr "" @@ -1554,7 +1553,7 @@ msgstr "" msgid "Are you sure you want to discard your changes?" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:1122 +#: src/screens/Messages/ConversationSettings.tsx:1130 msgid "Are you sure you want to leave {groupName}?" msgstr "Are you sure you want to leave {groupName}?" @@ -1570,7 +1569,7 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:1469 +#: src/view/com/composer/Composer.tsx:1457 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1599,6 +1598,10 @@ msgstr "" msgid "At least 8 characters" msgstr "" +#: src/screens/Settings/components/DeleteAccountDialog.tsx:338 +msgid "AT Protocol FAQ" +msgstr "AT Protocol FAQ" + #: src/screens/Settings/AppIconSettings/useAppIconSets.ts:48 msgctxt "Name of app icon variant" msgid "Aurora" @@ -1696,8 +1699,8 @@ msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:94 #: src/components/dms/MessageProfileButton.tsx:60 #: src/screens/Messages/ChatList.tsx:376 -#: src/screens/Messages/Conversation.tsx:247 -#: src/screens/Messages/ConversationSettings.tsx:548 +#: src/screens/Messages/Conversation.tsx:266 +#: src/screens/Messages/ConversationSettings.tsx:552 msgid "Before you can message another user, you must first verify your email." msgstr "" @@ -1726,14 +1729,14 @@ msgid "Birthday" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:853 -#: src/screens/Messages/ConversationSettings.tsx:670 -#: src/screens/Messages/ConversationSettings.tsx:1147 +#: src/screens/Messages/ConversationSettings.tsx:674 +#: src/screens/Messages/ConversationSettings.tsx:1155 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:207 #: src/view/com/profile/ProfileMenu.tsx:563 msgid "Block" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:666 +#: src/screens/Messages/ConversationSettings.tsx:670 msgid "Block {displayName}" msgstr "Block {displayName}" @@ -1748,7 +1751,7 @@ msgstr "Block {displayName}" msgid "Block account" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:1144 +#: src/screens/Messages/ConversationSettings.tsx:1152 msgid "Block account?" msgstr "Block account?" @@ -1791,7 +1794,7 @@ msgstr "" msgid "Block user and/or delete this conversation" msgstr "" -#: src/components/Post/Embed/index.tsx:187 +#: src/components/Post/Embed/index.tsx:182 msgid "Blocked" msgstr "" @@ -1805,7 +1808,7 @@ msgid "Blocked Accounts" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:851 -#: src/screens/Messages/ConversationSettings.tsx:1145 +#: src/screens/Messages/ConversationSettings.tsx:1153 #: src/view/com/profile/ProfileMenu.tsx:558 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" @@ -2040,9 +2043,9 @@ msgstr "" #: src/features/liveNow/components/GoLiveDialog.tsx:254 #: src/lib/media/picker.tsx:38 #: src/screens/Deactivated.tsx:150 -#: src/screens/Messages/ConversationSettings.tsx:1081 -#: src/screens/Messages/ConversationSettings.tsx:1102 -#: src/screens/Messages/ConversationSettings.tsx:1126 +#: src/screens/Messages/ConversationSettings.tsx:1089 +#: src/screens/Messages/ConversationSettings.tsx:1110 +#: src/screens/Messages/ConversationSettings.tsx:1134 #: src/screens/Profile/Header/EditProfileDialog.tsx:215 #: src/screens/Profile/Header/EditProfileDialog.tsx:223 #: src/screens/Search/Shell.tsx:399 @@ -2055,8 +2058,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:300 #: src/screens/Takendown.tsx:102 #: src/screens/Takendown.tsx:105 -#: src/view/com/composer/Composer.tsx:1547 -#: src/view/com/composer/Composer.tsx:1557 +#: src/view/com/composer/Composer.tsx:1535 +#: src/view/com/composer/Composer.tsx:1545 #: src/view/com/composer/photos/EditImageDialog.web.tsx:44 #: src/view/com/composer/photos/EditImageDialog.web.tsx:53 #: src/view/shell/desktop/LeftNav.tsx:215 @@ -2078,7 +2081,7 @@ msgstr "" #: src/components/PostControls/index.tsx:110 #: src/components/PostControls/index.tsx:141 #: src/components/PostControls/index.tsx:169 -#: src/state/shell/composer/index.tsx:107 +#: src/state/shell/composer/index.tsx:105 msgid "Cannot interact with a blocked user" msgstr "" @@ -2091,7 +2094,7 @@ msgstr "" msgid "Captions & alt text" msgstr "" -#: src/components/images/Gallery/index.tsx:251 +#: src/components/images/Gallery/index.tsx:255 msgid "carousel" msgstr "carousel" @@ -2231,7 +2234,7 @@ msgstr "" msgid "Chats" msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:236 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:233 msgid "Check <0>{currentEmail} for an email with the confirmation code to enter below:" msgstr "" @@ -2342,6 +2345,11 @@ msgstr "Click here to delete your account" msgid "Click here to log out" msgstr "" +#: src/screens/Settings/components/DeleteAccountDialog.tsx:263 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:272 +msgid "Click here to resend the email" +msgstr "Click here to resend the email" + #: src/components/dialogs/EmailDialog/screens/Verify.tsx:384 msgid "Click here to restart the verification process." msgstr "" @@ -2384,7 +2392,7 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:180 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:233 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:239 -#: src/components/dialogs/GifSelect.tsx:271 +#: src/components/dialogs/GifSelect.tsx:283 #: src/components/dialogs/LanguageSelectDialog.tsx:359 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:159 #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:168 @@ -2438,7 +2446,7 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:223 #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:229 -#: src/components/dialogs/GifSelect.tsx:265 +#: src/components/dialogs/GifSelect.tsx:277 #: src/components/dialogs/LanguageSelectDialog.tsx:221 #: src/components/dialogs/LanguageSelectDialog.tsx:322 #: src/components/dialogs/LanguageSelectDialog.tsx:354 @@ -2451,12 +2459,7 @@ msgstr "" msgid "Close drawer menu" msgstr "" -#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:136 -#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:172 -msgid "Close emoji picker" -msgstr "" - -#: src/components/dialogs/GifSelect.tsx:161 +#: src/components/dialogs/GifSelect.tsx:173 msgid "Close GIF dialog" msgstr "" @@ -2487,15 +2490,10 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:1555 +#: src/view/com/composer/Composer.tsx:1543 msgid "Closes post composer and discards post draft" msgstr "" -#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:137 -#: src/view/com/composer/text-input/web/EmojiPicker.web.tsx:173 -msgid "Closes the emoji picker" -msgstr "" - #: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:32 msgid "Closes viewer for header image" msgstr "" @@ -2546,7 +2544,7 @@ msgid "Compose new post" msgstr "" #. placeholder {0}: MAX_GRAPHEME_LENGTH || 0 -#: src/view/com/composer/Composer.tsx:1431 +#: src/view/com/composer/Composer.tsx:1419 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -2554,11 +2552,11 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:2383 +#: src/view/com/composer/Composer.tsx:2377 msgid "Compressing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2385 +#: src/view/com/composer/Composer.tsx:2379 msgid "Compressing video..." msgstr "" @@ -2592,7 +2590,7 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:284 #: src/screens/Settings/components/ChangePasswordDialog.tsx:187 #: src/screens/Settings/components/ChangePasswordDialog.tsx:191 -#: src/screens/Settings/components/DeleteAccountDialog.tsx:247 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:244 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:145 #: src/screens/Settings/components/DisableEmail2FADialog.tsx:151 msgid "Confirmation code" @@ -2729,7 +2727,7 @@ msgstr "Continue to group name" msgid "Continue to next step" msgstr "" -#: src/screens/Messages/Conversation.tsx:64 +#: src/screens/Messages/Conversation.tsx:66 msgid "Conversation" msgstr "" @@ -2993,7 +2991,7 @@ msgstr "" msgid "Create an avatar instead" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:858 +#: src/screens/Messages/ConversationSettings.tsx:865 msgid "Create an invite link for this group chat" msgstr "Create an invite link for this group chat" @@ -3140,8 +3138,8 @@ msgstr "" msgid "Delete account" msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:186 -#: src/screens/Settings/components/DeleteAccountDialog.tsx:233 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:183 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:230 msgid "Delete account “{currentHandle}”" msgstr "" @@ -3194,13 +3192,13 @@ msgstr "" msgid "Delete message for me" msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:316 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:309 msgid "Delete my account" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:787 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:789 -#: src/view/com/composer/Composer.tsx:1443 +#: src/view/com/composer/Composer.tsx:1431 msgid "Delete post" msgstr "" @@ -3221,14 +3219,15 @@ msgstr "" msgid "Delete this post?" msgstr "" -#: src/components/Post/Embed/index.tsx:180 +#: src/components/Post/Embed/index.tsx:175 msgid "Deleted" msgstr "" -#: src/components/dms/MessagesListHeader.tsx:123 +#: src/components/dms/MessagesListHeader.tsx:128 +#: src/components/dms/MessagesListHeader.tsx:189 #: src/screens/Messages/components/ChatListItem.tsx:127 -#: src/screens/Messages/ConversationSettings.tsx:376 -#: src/screens/Messages/ConversationSettings.tsx:595 +#: src/screens/Messages/ConversationSettings.tsx:383 +#: src/screens/Messages/ConversationSettings.tsx:599 msgid "Deleted Account" msgstr "" @@ -3331,9 +3330,9 @@ msgstr "" #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:101 #: src/screens/Profile/Header/EditProfileDialog.tsx:79 -#: src/view/com/composer/Composer.tsx:1231 -#: src/view/com/composer/Composer.tsx:1275 -#: src/view/com/composer/Composer.tsx:1476 +#: src/view/com/composer/Composer.tsx:1219 +#: src/view/com/composer/Composer.tsx:1263 +#: src/view/com/composer/Composer.tsx:1464 #: src/view/com/composer/drafts/DraftItem.tsx:242 #: src/view/com/composer/drafts/DraftsButton.tsx:131 msgid "Discard" @@ -3344,14 +3343,14 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1229 +#: src/view/com/composer/Composer.tsx:1217 #: 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:1246 -#: src/view/com/composer/Composer.tsx:1468 +#: src/view/com/composer/Composer.tsx:1234 +#: src/view/com/composer/Composer.tsx:1456 msgid "Discard post?" msgstr "" @@ -3386,7 +3385,7 @@ msgstr "" msgid "Dismiss banner" msgstr "" -#: src/view/com/composer/Composer.tsx:2304 +#: src/view/com/composer/Composer.tsx:2298 msgid "Dismiss error" msgstr "" @@ -3446,7 +3445,7 @@ msgstr "" msgid "Don't have a code or need a new one? <0>Click here." msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:274 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:269 msgid "Don’t see a code? <0>Click here to resend." msgstr "" @@ -3606,8 +3605,8 @@ msgstr "" msgid "Edit Feeds" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:1035 -#: src/screens/Messages/ConversationSettings.tsx:1040 +#: src/screens/Messages/ConversationSettings.tsx:1042 +#: src/screens/Messages/ConversationSettings.tsx:1047 msgid "Edit group name" msgstr "Edit group name" @@ -3646,7 +3645,7 @@ msgstr "" msgid "Edit My Feeds" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:852 +#: src/screens/Messages/ConversationSettings.tsx:859 msgid "Edit name" msgstr "Edit name" @@ -3680,7 +3679,7 @@ msgstr "" msgid "Edit starter pack" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:851 +#: src/screens/Messages/ConversationSettings.tsx:858 msgid "Edit this group chat’s name" msgstr "Edit this group chat’s name" @@ -3730,7 +3729,7 @@ msgstr "" msgid "Email sent!" msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:263 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:260 msgid "Email sent! <0>Click here to resend." msgstr "" @@ -3884,7 +3883,7 @@ msgid "Enter your email address" msgstr "" #: src/screens/Login/LoginForm.tsx:243 -#: src/screens/Settings/components/DeleteAccountDialog.tsx:298 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:291 msgid "Enter your password" msgstr "" @@ -3900,7 +3899,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:2403 +#: src/view/com/composer/Composer.tsx:2397 #: src/view/com/util/error/ErrorScreen.tsx:43 msgid "Error" msgstr "" @@ -4093,7 +4092,7 @@ msgid "Failed to create app password. Please try again." msgstr "" #: src/components/dms/MessageProfileButton.tsx:38 -#: src/screens/Messages/ConversationSettings.tsx:525 +#: src/screens/Messages/ConversationSettings.tsx:529 msgid "Failed to create conversation" msgstr "" @@ -4125,6 +4124,10 @@ msgstr "" msgid "Failed to disconnect Germ DM. Error: {0}" msgstr "" +#: src/screens/Messages/ConversationSettings.tsx:731 +msgid "Failed to edit group chat name" +msgstr "Failed to edit group chat name" + #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:142 msgid "Failed to follow all suggested accounts, please try again" msgstr "" @@ -4141,7 +4144,7 @@ msgstr "" msgid "Failed to launch SMS app" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:747 +#: src/screens/Messages/ConversationSettings.tsx:759 msgctxt "toast" msgid "Failed to leave group chat" msgstr "Failed to leave group chat" @@ -4164,7 +4167,7 @@ msgstr "" msgid "Failed to load feeds preferences" msgstr "" -#: src/components/dialogs/GifSelect.tsx:215 +#: src/components/dialogs/GifSelect.tsx:227 msgid "Failed to load GIFs" msgstr "" @@ -4210,7 +4213,7 @@ msgstr "" msgid "Failed to mark all requests as read" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:735 +#: src/screens/Messages/ConversationSettings.tsx:747 msgid "Failed to mute group chat" msgstr "Failed to mute group chat" @@ -4250,7 +4253,7 @@ msgstr "" msgid "Failed to resolve location. Please try again." msgstr "" -#: src/view/com/composer/Composer.tsx:563 +#: src/view/com/composer/Composer.tsx:562 msgid "Failed to save draft" msgstr "" @@ -4707,7 +4710,7 @@ msgstr "" msgid "For organizational accounts, use the birthdate of the person who is responsible for the account." msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:189 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:186 msgid "For security reasons, we’ll need to send a confirmation code to your email address <0>{currentEmail}." msgstr "" @@ -4849,7 +4852,7 @@ msgstr "" #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:77 #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:87 -#: src/screens/Messages/ConversationSettings.tsx:1080 +#: src/screens/Messages/ConversationSettings.tsx:1088 msgid "Get started" msgstr "" @@ -4858,7 +4861,7 @@ msgstr "" msgid "GIF" msgstr "" -#: src/view/com/composer/Composer.tsx:2408 +#: src/view/com/composer/Composer.tsx:2402 msgid "GIF uploaded" msgstr "" @@ -4965,7 +4968,7 @@ msgid "Go to next" msgstr "" #: src/components/dms/ConvoMenu.tsx:255 -#: src/screens/Messages/ConversationSettings.tsx:646 +#: src/screens/Messages/ConversationSettings.tsx:650 #: src/view/shell/desktop/LeftNav.tsx:319 #: src/view/shell/desktop/LeftNav.tsx:325 msgid "Go to profile" @@ -5003,17 +5006,21 @@ msgstr "" msgid "Grooming or predatory behavior" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:728 +#: src/components/dms/MessagesListHeader.tsx:195 +msgid "Group chat" +msgstr "Group chat" + +#: src/screens/Messages/ConversationSettings.tsx:740 msgctxt "toast" msgid "Group chat muted" msgstr "Group chat muted" #: src/Navigation.tsx:575 -#: src/screens/Messages/ConversationSettings.tsx:104 +#: src/screens/Messages/ConversationSettings.tsx:106 msgid "Group chat settings" msgstr "Group chat settings" -#: src/screens/Messages/ConversationSettings.tsx:730 +#: src/screens/Messages/ConversationSettings.tsx:742 msgctxt "toast" msgid "Group chat unmuted" msgstr "Group chat unmuted" @@ -5024,7 +5031,7 @@ msgstr "Group is locked" #: src/components/dms/InitiateChatFlow.tsx:231 #: src/components/dms/InitiateChatFlow.tsx:549 -#: src/screens/Messages/ConversationSettings.tsx:1041 +#: src/screens/Messages/ConversationSettings.tsx:1048 msgid "Group name" msgstr "Group name" @@ -5414,7 +5421,7 @@ msgid "Image" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:424 +#: src/components/images/Gallery/index.tsx:428 msgid "Image {0}" msgstr "Image {0}" @@ -5425,7 +5432,7 @@ msgid "Image {0} of {1}" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:415 +#: src/components/images/Gallery/index.tsx:419 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5440,7 +5447,7 @@ msgid "Image cache cleared, freed {0}" msgstr "" #. placeholder {0}: images.length -#: src/components/images/Gallery/index.tsx:252 +#: src/components/images/Gallery/index.tsx:256 msgid "Image gallery, {0} images" msgstr "Image gallery, {0} images" @@ -5629,8 +5636,8 @@ msgid "Invite friends <0/>" msgstr "" #: src/screens/Messages/components/MessagesListInfoPanel.tsx:113 -#: src/screens/Messages/ConversationSettings.tsx:859 -#: src/screens/Messages/ConversationSettings.tsx:1078 +#: src/screens/Messages/ConversationSettings.tsx:866 +#: src/screens/Messages/ConversationSettings.tsx:1086 msgid "Invite link" msgstr "Invite link" @@ -5642,7 +5649,7 @@ msgstr "" msgid "Invite your friends to follow your favorite feeds and people" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:609 +#: src/screens/Messages/ConversationSettings.tsx:613 msgid "Invited" msgstr "Invited" @@ -5673,7 +5680,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:2323 +#: src/view/com/composer/Composer.tsx:2317 msgid "Job ID: {0}" msgstr "" @@ -5698,7 +5705,7 @@ msgstr "" msgid "Journalism" msgstr "" -#: src/view/com/composer/Composer.tsx:1279 +#: src/view/com/composer/Composer.tsx:1267 #: src/view/com/composer/drafts/DraftsButton.tsx:135 msgid "Keep editing" msgstr "" @@ -5808,10 +5815,6 @@ msgstr "" msgid "Learn more about self hosting your PDS." msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:344 -msgid "Learn more about the AT Protocol." -msgstr "" - #: src/components/moderation/ContentHider.tsx:169 #: src/components/moderation/ContentHider.tsx:235 msgid "Learn more about the moderation applied to this content" @@ -5855,7 +5858,7 @@ msgid "Learn more." msgstr "" #: src/components/dms/LeaveConvoPrompt.tsx:52 -#: src/screens/Messages/ConversationSettings.tsx:887 +#: src/screens/Messages/ConversationSettings.tsx:894 msgid "Leave" msgstr "" @@ -5872,11 +5875,11 @@ msgstr "" msgid "Leave conversation" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:1124 +#: src/screens/Messages/ConversationSettings.tsx:1132 msgid "Leave group chat" msgstr "Leave group chat" -#: src/screens/Messages/ConversationSettings.tsx:886 +#: src/screens/Messages/ConversationSettings.tsx:893 msgid "Leave this group chat" msgstr "Leave this group chat" @@ -6170,27 +6173,27 @@ msgstr "" msgid "Loading..." msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:999 +#: src/screens/Messages/ConversationSettings.tsx:1006 msgid "Loading…" msgstr "Loading…" -#: src/screens/Messages/ConversationSettings.tsx:869 +#: src/screens/Messages/ConversationSettings.tsx:876 msgid "Lock" msgstr "Lock" -#: src/screens/Messages/ConversationSettings.tsx:1101 +#: src/screens/Messages/ConversationSettings.tsx:1109 msgid "Lock group chat" msgstr "Lock group chat" -#: src/screens/Messages/ConversationSettings.tsx:1099 +#: src/screens/Messages/ConversationSettings.tsx:1107 msgid "Lock group chat?" msgstr "Lock group chat?" -#: src/screens/Messages/ConversationSettings.tsx:867 +#: src/screens/Messages/ConversationSettings.tsx:874 msgid "Lock this group chat" msgstr "Lock this group chat" -#: src/screens/Messages/ConversationSettings.tsx:869 +#: src/screens/Messages/ConversationSettings.tsx:876 msgid "Locked" msgstr "Locked" @@ -6300,11 +6303,11 @@ msgstr "" msgid "Media that may be disturbing or inappropriate for some audiences." msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:232 +#: src/screens/Messages/ConversationSettings.tsx:245 msgid "Members" msgstr "Members" -#: src/screens/Messages/ConversationSettings.tsx:1100 +#: src/screens/Messages/ConversationSettings.tsx:1108 msgid "Members can still read chat history but can’t send new messages." msgstr "Members can still read chat history but can’t send new messages." @@ -6327,10 +6330,10 @@ msgstr "" msgid "Menu" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:198 -#: src/screens/Messages/components/MessageInput.tsx:173 -#: src/screens/Messages/components/MessageInput.web.tsx:212 -#: src/screens/Messages/ConversationSettings.tsx:654 +#: src/screens/Messages/components/MessageComposer.tsx:168 +#: src/screens/Messages/components/MessageInput.tsx:171 +#: src/screens/Messages/components/MessageInput.web.tsx:195 +#: src/screens/Messages/ConversationSettings.tsx:658 msgid "Message" msgstr "Message" @@ -6339,7 +6342,7 @@ msgstr "Message" msgid "Message {0}" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:651 +#: src/screens/Messages/ConversationSettings.tsx:655 msgid "Message {displayName}" msgstr "Message {displayName}" @@ -6367,17 +6370,17 @@ msgstr "" msgid "Message from server: {0}" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:197 -#: src/screens/Messages/components/MessageInput.tsx:171 +#: src/screens/Messages/components/MessageComposer.tsx:167 +#: src/screens/Messages/components/MessageInput.tsx:169 msgid "Message input field" msgstr "" -#: src/screens/Messages/components/MessageInput.tsx:84 -#: src/screens/Messages/components/MessageInput.web.tsx:59 +#: src/screens/Messages/components/MessageInput.tsx:82 +#: src/screens/Messages/components/MessageInput.web.tsx:53 msgid "Message is too long" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:98 +#: src/screens/Messages/components/MessageComposer.tsx:85 msgid "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" @@ -6509,7 +6512,7 @@ msgstr "" msgid "Music" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:845 +#: src/screens/Messages/ConversationSettings.tsx:852 msgid "Mute" msgstr "Mute" @@ -6554,7 +6557,7 @@ msgstr "" msgid "Mute these accounts?" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:843 +#: src/screens/Messages/ConversationSettings.tsx:850 msgid "Mute this group chat" msgstr "Mute this group chat" @@ -6592,7 +6595,7 @@ msgstr "" msgid "Mute words & tags" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:845 +#: src/screens/Messages/ConversationSettings.tsx:852 msgid "Muted" msgstr "Muted" @@ -6875,7 +6878,11 @@ msgstr "" msgid "No expiry set" msgstr "" -#: src/components/dialogs/GifSelect.tsx:221 +#: src/components/dialogs/GifSelect.tsx:237 +msgid "No featured GIFs found. There may be an issue with KLIPY." +msgstr "No featured GIFs found. There may be an issue with KLIPY." + +#: src/components/dialogs/GifSelect.tsx:238 msgid "No featured GIFs found. There may be an issue with Tenor." msgstr "" @@ -7002,7 +7009,7 @@ msgstr "No results found for “<0>{query}”." msgid "No results." msgstr "" -#: src/components/dialogs/GifSelect.tsx:219 +#: src/components/dialogs/GifSelect.tsx:235 msgid "No search results found for \"{search}\"." msgstr "" @@ -7155,7 +7162,7 @@ msgstr "" msgid "Off" msgstr "" -#: src/components/dialogs/GifSelect.tsx:258 +#: src/components/dialogs/GifSelect.tsx:272 #: src/components/dialogs/LanguageSelectDialog.tsx:347 #: src/view/com/util/ErrorBoundary.tsx:57 msgid "Oh no!" @@ -7196,11 +7203,11 @@ msgstr "" msgid "Onboarding reset" msgstr "" -#: src/view/com/composer/Composer.tsx:772 +#: src/view/com/composer/Composer.tsx:771 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:769 +#: src/view/com/composer/Composer.tsx:768 msgid "One or more images is missing alt text." msgstr "" @@ -7212,11 +7219,11 @@ msgstr "" msgid "One or more of your selected files are too large. Maximum size is 100 MB." msgstr "" -#: src/view/com/composer/Composer.tsx:574 +#: src/view/com/composer/Composer.tsx:573 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:779 +#: src/view/com/composer/Composer.tsx:778 msgid "One or more videos is missing alt text." msgstr "" @@ -7260,7 +7267,7 @@ msgstr "" msgid "Open camera" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:604 +#: src/screens/Messages/ConversationSettings.tsx:608 msgid "Open chat member options for {displayName}" msgstr "Open chat member options for {displayName}" @@ -7277,9 +7284,9 @@ msgstr "" msgid "Open drawer menu" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:177 -#: src/screens/Messages/components/MessageInput.web.tsx:179 -#: src/view/com/composer/Composer.tsx:1972 +#: src/screens/Messages/components/MessageComposer.tsx:129 +#: src/screens/Messages/components/MessageInput.web.tsx:145 +#: src/view/com/composer/Composer.tsx:1958 msgid "Open emoji picker" msgstr "" @@ -7300,11 +7307,11 @@ msgstr "" msgid "Open Germ DM" msgstr "" -#: src/components/dms/MessagesListHeader.tsx:204 +#: src/components/dms/MessagesListHeader.tsx:216 msgid "Open group chat settings" msgstr "Open group chat settings" -#: src/components/Post/Embed/ExternalEmbed/index.tsx:79 +#: src/components/Post/Embed/ExternalEmbed/index.tsx:82 msgid "Open link to {niceUrl}" msgstr "" @@ -7401,10 +7408,6 @@ msgstr "" msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." msgstr "" -#: src/view/com/composer/Composer.tsx:1973 -msgid "Opens emoji picker" -msgstr "" - #: src/view/com/auth/SplashScreen.tsx:102 #: src/view/com/auth/SplashScreen.web.tsx:116 msgid "Opens flow to create a new Bluesky account" @@ -7415,7 +7418,7 @@ msgstr "" msgid "Opens flow to sign in to your existing Bluesky account" msgstr "" -#: src/components/images/Gallery/index.tsx:425 +#: src/components/images/Gallery/index.tsx:429 msgid "Opens full image" msgstr "Opens full image" @@ -7558,7 +7561,7 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:228 #: src/screens/Settings/AccountSettings.tsx:126 #: src/screens/Settings/AccountSettings.tsx:130 -#: src/screens/Settings/components/DeleteAccountDialog.tsx:291 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:284 #: src/screens/Signup/StepInfo/index.tsx:255 msgid "Password" msgstr "" @@ -7897,7 +7900,7 @@ msgstr "" msgid "Porn" msgstr "" -#: src/view/com/composer/Composer.tsx:1621 +#: src/view/com/composer/Composer.tsx:1609 msgctxt "action" msgid "Post" msgstr "" @@ -7917,7 +7920,7 @@ msgstr "" msgid "Post a video" msgstr "" -#: src/view/com/composer/Composer.tsx:1619 +#: src/view/com/composer/Composer.tsx:1607 msgctxt "action" msgid "Post All" msgstr "" @@ -8091,11 +8094,11 @@ msgstr "" msgid "Privacy violation of a minor" msgstr "" -#: src/view/com/composer/Composer.tsx:2397 +#: src/view/com/composer/Composer.tsx:2391 msgid "Processing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2399 +#: src/view/com/composer/Composer.tsx:2393 msgid "Processing video..." msgstr "" @@ -8141,22 +8144,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:1605 +#: src/view/com/composer/Composer.tsx:1593 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1600 +#: src/view/com/composer/Composer.tsx:1588 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:1589 +#: src/view/com/composer/Composer.tsx:1577 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:1594 +#: src/view/com/composer/Composer.tsx:1582 msgid "Publish reply" msgstr "" @@ -8371,7 +8374,7 @@ msgstr "Remove {displayName} from group chat" msgid "Remove {displayName} from starter pack" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:677 +#: src/screens/Messages/ConversationSettings.tsx:681 msgid "Remove {displayName} from this group chat" msgstr "Remove {displayName} from this group chat" @@ -8421,7 +8424,7 @@ msgstr "" msgid "Remove feed?" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:680 +#: src/screens/Messages/ConversationSettings.tsx:684 msgid "Remove from chat" msgstr "Remove from chat" @@ -8501,11 +8504,11 @@ msgstr "" msgid "Remove your verification for this account?" msgstr "" -#: src/components/Post/Embed/index.tsx:215 +#: src/components/Post/Embed/index.tsx:210 msgid "Removed by author" msgstr "" -#: src/components/Post/Embed/index.tsx:213 +#: src/components/Post/Embed/index.tsx:208 msgid "Removed by you" msgstr "" @@ -8596,7 +8599,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1617 +#: src/view/com/composer/Composer.tsx:1605 msgctxt "action" msgid "Reply" msgstr "" @@ -8642,7 +8645,7 @@ msgstr "" #: src/components/dms/MessagesListBlockedFooter.tsx:86 #: src/components/dms/MessagesListBlockedFooter.tsx:93 #: src/features/liveNow/components/LiveStatusDialog.tsx:266 -#: src/screens/Messages/ConversationSettings.tsx:878 +#: src/screens/Messages/ConversationSettings.tsx:885 msgid "Report" msgstr "" @@ -8701,7 +8704,7 @@ msgstr "" msgid "Report this feed" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:877 +#: src/screens/Messages/ConversationSettings.tsx:884 msgid "Report this group chat" msgstr "Report this group chat" @@ -8812,8 +8815,6 @@ msgid "Required in your region" msgstr "" #: src/components/dialogs/EmailDialog/components/ResendEmailText.tsx:41 -#: src/screens/Settings/components/DeleteAccountDialog.tsx:266 -#: src/screens/Settings/components/DeleteAccountDialog.tsx:277 msgid "Resend" msgstr "" @@ -8934,7 +8935,7 @@ msgstr "" #: src/components/StarterPack/QrCodeDialog.tsx:207 #: src/features/liveNow/components/EditLiveDialog.tsx:204 #: src/features/liveNow/components/EditLiveDialog.tsx:211 -#: src/screens/Messages/ConversationSettings.tsx:1055 +#: src/screens/Messages/ConversationSettings.tsx:1063 #: src/screens/Profile/Header/EditProfileDialog.tsx:233 #: src/screens/Profile/Header/EditProfileDialog.tsx:247 #: src/screens/SavedFeeds.tsx:132 @@ -8949,7 +8950,7 @@ msgstr "" msgid "Save" msgstr "" -#: src/view/com/lightbox/ImageViewing/index.tsx:664 +#: src/view/com/lightbox/ImageViewing/index.tsx:671 msgctxt "action" msgid "Save" msgstr "" @@ -8964,22 +8965,22 @@ msgstr "" #: src/screens/SavedFeeds.tsx:132 #: src/screens/SavedFeeds.tsx:326 #: src/screens/SavedFeeds.tsx:330 -#: src/view/com/composer/Composer.tsx:1269 +#: src/view/com/composer/Composer.tsx:1257 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save changes" msgstr "" -#: src/view/com/composer/Composer.tsx:1241 +#: src/view/com/composer/Composer.tsx:1229 #: src/view/com/composer/drafts/DraftsButton.tsx:93 msgid "Save changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1269 +#: src/view/com/composer/Composer.tsx:1257 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save draft" msgstr "" -#: src/view/com/composer/Composer.tsx:1243 +#: src/view/com/composer/Composer.tsx:1231 #: src/view/com/composer/drafts/DraftsButton.tsx:95 msgid "Save draft?" msgstr "" @@ -9118,7 +9119,7 @@ msgstr "Search for people" msgid "Search for posts, users, or feeds" msgstr "" -#: src/components/dialogs/GifSelect.tsx:169 +#: src/components/dialogs/GifSelect.tsx:181 msgid "Search GIFs" msgstr "" @@ -9127,6 +9128,10 @@ msgstr "" msgid "Search is currently unavailable when logged out" msgstr "" +#: src/components/dialogs/GifSelect.tsx:182 +msgid "Search KLIPY" +msgstr "Search KLIPY" + #: src/components/dialogs/LanguageSelectDialog.tsx:232 #: src/components/dialogs/LanguageSelectDialog.tsx:233 msgid "Search languages" @@ -9147,7 +9152,7 @@ msgstr "" msgid "Search profiles" msgstr "" -#: src/components/dialogs/GifSelect.tsx:170 +#: src/components/dialogs/GifSelect.tsx:182 msgid "Search Tenor" msgstr "" @@ -9300,7 +9305,7 @@ msgid "Select GIF" msgstr "" #. placeholder {0}: gif.title -#: src/components/dialogs/GifSelect.tsx:297 +#: src/components/dialogs/GifSelect.tsx:309 msgid "Select GIF \"{0}\"" msgstr "" @@ -9404,7 +9409,7 @@ msgstr "" #: src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx:175 #: src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx:182 #: src/components/dialogs/EmailDialog/screens/Verify.tsx:303 -#: src/screens/Settings/components/DeleteAccountDialog.tsx:202 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:199 msgid "Send email" msgstr "" @@ -9412,9 +9417,9 @@ msgstr "" msgid "Send feedback" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:264 -#: src/screens/Messages/components/MessageInput.tsx:227 -#: src/screens/Messages/components/MessageInput.web.tsx:233 +#: src/screens/Messages/components/MessageComposer.tsx:226 +#: src/screens/Messages/components/MessageInput.tsx:225 +#: src/screens/Messages/components/MessageInput.web.tsx:216 msgid "Send message" msgstr "" @@ -9555,7 +9560,7 @@ msgstr "" msgid "Share" msgstr "" -#: src/view/com/lightbox/ImageViewing/index.tsx:673 +#: src/view/com/lightbox/ImageViewing/index.tsx:680 msgctxt "action" msgid "Share" msgstr "" @@ -9875,7 +9880,7 @@ msgstr "" msgid "Skip to next step" msgstr "" -#: src/components/images/Gallery/index.tsx:414 +#: src/components/images/Gallery/index.tsx:418 msgid "slide" msgstr "slide" @@ -9930,8 +9935,8 @@ msgstr "" msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" -#: src/screens/Messages/Conversation.tsx:153 -#: src/screens/Messages/ConversationSettings.tsx:179 +#: src/screens/Messages/Conversation.tsx:164 +#: src/screens/Messages/ConversationSettings.tsx:198 msgid "Something went wrong" msgstr "" @@ -10330,6 +10335,10 @@ msgstr "" msgid "Tell us a little more" msgstr "" +#: src/screens/Settings/components/DeleteAccountDialog.tsx:214 +msgid "Temporarily deactivate your account" +msgstr "Temporarily deactivate your account" + #: src/view/shell/desktop/RightNav.tsx:124 #: src/view/shell/desktop/RightNav.tsx:125 msgid "Terms" @@ -10523,7 +10532,11 @@ msgstr "" msgid "There was a problem with your internet connection, please try again" msgstr "" -#: src/components/dialogs/GifSelect.tsx:216 +#: src/components/dialogs/GifSelect.tsx:230 +msgid "There was an issue connecting to KLIPY." +msgstr "There was an issue connecting to KLIPY." + +#: src/components/dialogs/GifSelect.tsx:231 msgid "There was an issue connecting to Tenor." msgstr "" @@ -10580,8 +10593,8 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:431 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:454 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:474 -#: src/screens/Messages/ConversationSettings.tsx:563 -#: src/screens/Messages/ConversationSettings.tsx:576 +#: src/screens/Messages/ConversationSettings.tsx:567 +#: src/screens/Messages/ConversationSettings.tsx:580 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:117 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:130 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:93 @@ -10610,7 +10623,7 @@ msgstr "" msgid "There was an issue. Please check your internet connection and try again." msgstr "" -#: src/components/dialogs/GifSelect.tsx:260 +#: src/components/dialogs/GifSelect.tsx:273 #: src/components/dialogs/LanguageSelectDialog.tsx:349 #: src/view/com/util/ErrorBoundary.tsx:59 msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" @@ -10834,7 +10847,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:898 +#: src/view/com/composer/Composer.tsx:897 msgid "This post's author has disabled quote posts." msgstr "" @@ -10910,7 +10923,7 @@ msgstr "" msgid "This will delete \"{0}\" from your muted words. You can always add it back later." msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:337 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:330 msgid "This will irreversibly delete your Bluesky account <0>{currentHandle} and all associated data. Note that this will affect any other <1>AT Protocol services you use with this account." msgstr "" @@ -11088,7 +11101,7 @@ msgstr "" msgid "Two-factor authentication (2FA)" msgstr "" -#: src/screens/Messages/components/MessageInput.tsx:172 +#: src/screens/Messages/components/MessageInput.tsx:170 msgid "Type your message here" msgstr "" @@ -11160,7 +11173,7 @@ msgctxt "action" msgid "Unblock" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:665 +#: src/screens/Messages/ConversationSettings.tsx:669 msgid "Unblock {displayName}" msgstr "Unblock {displayName}" @@ -11235,11 +11248,11 @@ msgstr "" msgid "Unfortunately, your declared age indicates that you are not old enough to access Bluesky in your region." msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:690 +#: src/screens/Messages/ConversationSettings.tsx:694 msgid "Uninvite" msgstr "Uninvite" -#: src/screens/Messages/ConversationSettings.tsx:687 +#: src/screens/Messages/ConversationSettings.tsx:691 msgid "Uninvite {displayName} from this group chat" msgstr "Uninvite {displayName} from this group chat" @@ -11265,7 +11278,7 @@ msgstr "" msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:867 +#: src/screens/Messages/ConversationSettings.tsx:874 msgid "Unlock this group chat" msgstr "Unlock this group chat" @@ -11302,7 +11315,7 @@ msgstr "" msgid "Unmute list" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:843 +#: src/screens/Messages/ConversationSettings.tsx:849 msgid "Unmute this group chat" msgstr "Unmute this group chat" @@ -11381,7 +11394,7 @@ msgstr "" msgid "Unsupported clipboard content" msgstr "Unsupported clipboard content" -#: src/view/com/composer/Composer.tsx:1370 +#: src/view/com/composer/Composer.tsx:1358 msgid "Unsupported video type: {mimeType}" msgstr "" @@ -11447,7 +11460,7 @@ msgstr "" msgid "Upload from Library" msgstr "" -#: src/view/com/composer/Composer.tsx:2390 +#: src/view/com/composer/Composer.tsx:2384 msgid "Uploading GIF..." msgstr "" @@ -11460,7 +11473,7 @@ msgstr "" msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:2392 +#: src/view/com/composer/Composer.tsx:2386 msgid "Uploading video..." msgstr "" @@ -11739,7 +11752,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:2410 +#: src/view/com/composer/Composer.tsx:2404 msgid "Video uploaded" msgstr "" @@ -11756,7 +11769,7 @@ msgstr "" msgid "Videos must be less than 3 minutes long." msgstr "" -#: src/view/com/composer/Composer.tsx:990 +#: src/view/com/composer/Composer.tsx:989 msgctxt "Action to view the post the user just created" msgid "View" msgstr "" @@ -11781,11 +11794,8 @@ msgstr "" msgid "View {0}’s profile" msgstr "View {0}’s profile" -#: src/components/dms/MessagesListHeader.tsx:164 -msgid "View {displayName}'s profile" -msgstr "" - -#: src/screens/Messages/ConversationSettings.tsx:641 +#: src/components/dms/MessagesListHeader.tsx:141 +#: src/screens/Messages/ConversationSettings.tsx:645 msgid "View {displayName}’s profile" msgstr "View {displayName}’s profile" @@ -11810,7 +11820,7 @@ msgstr "" msgid "View full thread" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:243 +#: src/screens/Messages/ConversationSettings.tsx:256 msgid "View incoming group chat requests" msgstr "View incoming group chat requests" @@ -11829,7 +11839,7 @@ msgstr "" msgid "View more trending videos" msgstr "" -#: src/view/com/composer/Composer.tsx:985 +#: src/view/com/composer/Composer.tsx:984 msgid "View post" msgstr "" @@ -11961,11 +11971,11 @@ msgstr "" msgid "We couldn't find any results for that topic." msgstr "" -#: src/screens/Messages/Conversation.tsx:154 +#: src/screens/Messages/Conversation.tsx:165 msgid "We couldn't load this conversation" msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:180 +#: src/screens/Messages/ConversationSettings.tsx:199 msgid "We couldn’t load this conversation’s settings" msgstr "We couldn’t load this conversation’s settings" @@ -12110,7 +12120,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:896 +#: src/view/com/composer/Composer.tsx:895 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -12161,7 +12171,7 @@ msgid "What do you want to call your starter pack?" msgstr "" #: src/view/com/auth/SplashScreen.web.tsx:104 -#: src/view/com/composer/Composer.tsx:1330 +#: src/view/com/composer/Composer.tsx:1318 #: src/view/com/feeds/ComposerPrompt.tsx:193 msgid "What's up?" msgstr "" @@ -12238,7 +12248,7 @@ msgstr "" msgid "Would you like to save this as a draft before viewing your drafts?" msgstr "" -#: src/view/com/composer/Composer.tsx:1257 +#: src/view/com/composer/Composer.tsx:1245 msgid "Would you like to save this as a draft to edit later?" msgstr "" @@ -12247,12 +12257,12 @@ msgstr "" msgid "Write a post" msgstr "" -#: src/view/com/composer/Composer.tsx:1430 +#: src/view/com/composer/Composer.tsx:1418 msgid "Write post" msgstr "" #: src/screens/PostThread/components/ThreadComposePrompt.tsx:91 -#: src/view/com/composer/Composer.tsx:1328 +#: src/view/com/composer/Composer.tsx:1316 msgid "Write your reply" msgstr "" @@ -12279,7 +12289,7 @@ msgstr "" msgid "Yes, deactivate" msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:356 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:348 msgid "Yes, delete my account" msgstr "" @@ -12365,7 +12375,7 @@ msgstr "" msgid "You can adjust your interests at any time from \"Content and media\" settings." msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:214 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:211 msgid "You can also <0>temporarily deactivate your account instead. 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 "" @@ -12399,7 +12409,7 @@ msgstr "" msgid "You can now sign in with your new password." msgstr "" -#: src/view/com/composer/Composer.tsx:1262 +#: src/view/com/composer/Composer.tsx:1250 msgid "You can only save drafts up to 1000 characters." msgstr "" @@ -12538,7 +12548,7 @@ msgstr "" msgid "You have temporarily reached the limit for video uploads. Please try again later." msgstr "" -#: src/view/com/composer/Composer.tsx:1252 +#: src/view/com/composer/Composer.tsx:1240 msgid "You have unsaved changes to this draft, would you like to save them?" msgstr "" @@ -12663,7 +12673,7 @@ msgstr "" msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." msgstr "" -#: src/screens/Messages/ConversationSettings.tsx:1123 +#: src/screens/Messages/ConversationSettings.tsx:1131 msgid "You won’t be able to rejoin unless you’re invited." msgstr "You won’t be able to rejoin unless you’re invited." @@ -12744,7 +12754,7 @@ msgstr "" msgid "You've reached the end of your feed! Find some more accounts to follow." msgstr "" -#: src/view/com/composer/Composer.tsx:561 +#: src/view/com/composer/Composer.tsx:560 msgid "You've reached the maximum number of drafts" msgstr "" @@ -12772,7 +12782,7 @@ msgstr "" msgid "Your account" msgstr "" -#: src/screens/Settings/components/DeleteAccountDialog.tsx:131 +#: src/screens/Settings/components/DeleteAccountDialog.tsx:128 msgid "Your account has been deleted, see ya! ✌️" msgstr "" @@ -12904,11 +12914,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:981 +#: src/view/com/composer/Composer.tsx:980 msgid "Your post was sent" msgstr "" -#: src/view/com/composer/Composer.tsx:978 +#: src/view/com/composer/Composer.tsx:977 msgid "Your posts were sent" msgstr "" @@ -12929,7 +12939,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:980 +#: src/view/com/composer/Composer.tsx:979 msgid "Your reply was sent" msgstr "" From bc3672ceebb7feed4674dddad6e93307fd2216f4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 18 Apr 2026 12:58:16 -0700 Subject: [PATCH 12/18] [Chat] Remove convoState dependency from header (#10293) --- src/components/dms/ConvoMenu.tsx | 2 +- src/components/dms/MessagesListHeader.tsx | 97 ++++++------------- src/components/dms/util.ts | 4 +- src/screens/Messages/Conversation.tsx | 86 ++++++---------- .../Messages/components/ChatListItem.tsx | 1 + .../Messages/components/MessagesList.tsx | 9 +- src/state/queries/messages/conversation.ts | 9 +- 7 files changed, 66 insertions(+), 142 deletions(-) diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 587a25e95b..8ff4cf55bc 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -190,7 +190,7 @@ function MenuContent({ const isDeletedAccount = profile.handle === 'missing.invalid' const convoId = initialConvo.id - const {data: convo} = useConvoQuery(initialConvo) + const {data: convo} = useConvoQuery({convoId}) const onNavigateToProfile = useCallback(() => { navigation.navigate('Profile', {name: profile.did}) diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 13e747231f..0806f3a116 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -1,10 +1,9 @@ import {useMemo} from 'react' import {View} from 'react-native' import { - type AppBskyActorDefs, ChatBskyConvoDefs, - type ModerationCause, - type ModerationDecision, + moderateProfile, + type ModerationOpts, } from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -12,7 +11,8 @@ import {useNavigation} from '@react-navigation/native' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' -import {type Shadow} from '#/state/cache/profile-shadow' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' @@ -30,30 +30,9 @@ import {type ConvoWithDetails} from './util' const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE -export function MessagesListHeader({ - convo, - profile, - moderation, -}: { - convo?: ConvoWithDetails | null - profile?: Shadow - moderation?: ModerationDecision | null -}) { +export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) { const t = useTheme() - - const isGroupChat = convo?.kind === 'group' - - const blockInfo = useMemo(() => { - if (!moderation) return - const modui = moderation.ui('profileView') - const blocks = modui.alerts.filter(alert => alert.type === 'blocking') - const listBlocks = blocks.filter(alert => alert.source.type === 'list') - const userBlock = blocks.find(alert => alert.source.type === 'user') - return { - listBlocks, - userBlock, - } - }, [moderation]) + const moderationOpts = useModerationOpts() return ( @@ -61,20 +40,11 @@ export function MessagesListHeader({ - {convo ? ( - moderation && blockInfo && profile && !isGroupChat ? ( - + {convo && moderationOpts ? ( + convo.kind === 'direct' ? ( + ) : ( - + ) ) : ( <> @@ -108,20 +78,27 @@ export function MessagesListHeader({ function ProfileHeaderReady({ convo, - profile, - moderation, - blockInfo, + moderationOpts, }: { - convo: ConvoWithDetails - profile: Shadow - moderation: ModerationDecision - blockInfo: { - listBlocks: ModerationCause[] - userBlock?: ModerationCause - } + convo: Extract + moderationOpts: ModerationOpts }) { const {t: l} = useLingui() const {currentAccount} = useSession() + const profile = useProfileShadow(convo.primaryMember) + + const moderation = moderateProfile(profile, moderationOpts) + + const blockInfo = useMemo(() => { + const modui = moderation.ui('profileView') + const blocks = modui.alerts.filter(alert => alert.type === 'blocking') + const listBlocks = blocks.filter(alert => alert.source.type === 'list') + const userBlock = blocks.find(alert => alert.source.type === 'user') + return { + listBlocks, + userBlock, + } + }, [moderation]) const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount @@ -171,29 +148,13 @@ function ProfileHeaderReady({ function GroupHeaderReady({ convo, - profile, - moderation, }: { - convo: ConvoWithDetails - profile?: Shadow - moderation?: ModerationDecision | null + convo: Extract }) { const {t: l} = useLingui() const navigation = useNavigation() - const groupInfo = convo.kind === 'group' ? convo.details : undefined - - const isDeletedAccount = profile?.handle === 'missing.invalid' - const displayName = isDeletedAccount - ? l`Deleted Account` - : profile - ? createSanitizedDisplayName(profile, true, moderation?.ui('displayName')) - : undefined - const groupName = - groupInfo?.name ?? - (displayName ? l`${displayName}’s group chat` : l`Group chat`) - const handleNavigateToSettings = () => { navigation.navigate('MessagesConversationSettings', { conversation: convo.view.id, @@ -206,7 +167,7 @@ function GroupHeaderReady({ <> - {groupName} + {convo.details.name} } diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts index 491023cf2f..64e6bb16c6 100644 --- a/src/components/dms/util.ts +++ b/src/components/dms/util.ts @@ -56,12 +56,12 @@ export function hasReachedReactionLimit( return myReactions.length >= EMOJI_REACTION_LIMIT } -type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & { +export type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & { // can be missing if account deleted kind?: $Typed } -type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & { +export type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & { kind: $Typed } diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index 5e390f7a0c..0926770eb3 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -1,11 +1,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {type LayoutChangeEvent, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import { - type AppBskyActorDefs, - moderateProfile, - type ModerationDecision, -} from '@atproto/api' +import {moderateProfile} from '@atproto/api' import { ScrollEdgeEffect, ScrollEdgeEffectProvider, @@ -28,13 +24,13 @@ import { type CommonNavigatorParams, type NavigationProp, } from '#/lib/routes/types' -import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' +import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useEmail} from '#/state/email-verification' import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' import {ConvoStatus} from '#/state/messages/convo/types' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useProfileQuery} from '#/state/queries/profile' +import {useConvoQuery} from '#/state/queries/messages/conversation' import {useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import {MessagesList} from '#/screens/Messages/components/MessagesList' @@ -52,6 +48,7 @@ import {Error} from '#/components/Error' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {IS_LIQUID_GLASS, IS_WEB} from '#/env' +import {ChatDisabled} from './components/ChatDisabled' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -95,36 +92,26 @@ export function MessagesConversationScreenInner({route}: Props) { style={web([{minHeight: 0}, a.flex_1])}> - + ) } -function Inner() { +function Inner({convoId}: {convoId: string}) { const t = useTheme() const convoState = useConvo() const {_} = useLingui() const {currentAccount} = useSession() const isFocused = useIsFocused() const {top: topInset} = useSafeAreaInsets() + const {data: convoData} = useConvoQuery({convoId}) - const convo = convoState.convo - ? parseConvoView(convoState.convo, currentAccount?.did) + const convo = convoData + ? parseConvoView(convoData, currentAccount?.did) : null - const moderationOpts = useModerationOpts() - const {data: recipientUnshadowed} = useProfileQuery({ - did: convoState.getPrimaryMember?.()?.did, - }) - const recipient = useMaybeProfileShadow(recipientUnshadowed) - - const moderation = useMemo(() => { - if (!recipient || !moderationOpts) return null - return moderateProfile(recipient, moderationOpts) - }, [recipient, moderationOpts]) - // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be // empty. So, we also check for that possible state as well and render once we can. @@ -150,15 +137,7 @@ function Inner() { <> - {moderation ? ( - - ) : ( - - )} + } {!readyToShow && ( - {moderation ? ( - - ) : ( - - )} + )} 0} /> {!readyToShow && ( @@ -219,20 +189,18 @@ function Inner() { } function InnerReady({ - moderation, - recipient, hasScrolled, setHasScrolled, convo, isActive, + isDisabled, hasMessages, }: { - moderation: ModerationDecision | null - recipient: Shadow | undefined hasScrolled: boolean setHasScrolled: React.Dispatch> convo: ConvoWithDetails | null isActive: boolean + isDisabled: boolean hasMessages: boolean }) { const navigation = useNavigation() @@ -284,13 +252,14 @@ function InnerReady({ maybeBlockForEmailVerification() }, [maybeBlockForEmailVerification]) - const header = ( - - ) + const primaryMember = useMaybeProfileShadow(convo?.primaryMember) + const moderationOpts = useModerationOpts() + const primaryMemberModeration = useMemo(() => { + if (!primaryMember || !moderationOpts) return null + return moderateProfile(primaryMember, moderationOpts) + }, [primaryMember, moderationOpts]) + + const header = return ( <> @@ -308,16 +277,17 @@ function InnerReady({ + ) : convo && primaryMember && primaryMemberModeration?.blocked ? ( ) : null } diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 1f51656b1c..eaa0694822 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -482,6 +482,7 @@ function BaseChatItem({ ] : undefined } + onPressIn={() => precacheConvoQuery(queryClient, convo)} onPress={onPress} onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined} onAccessibilityAction={showMenu ? onLongPress : undefined}> diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index bdff2412c1..e7121602ef 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -50,7 +50,6 @@ import { import {useGetPost} from '#/state/queries/post' import {useAgent} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' -import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageInput} from '#/screens/Messages/components/MessageInput' import {MessageListError} from '#/screens/Messages/components/MessageListError' @@ -93,14 +92,12 @@ function onScrollToIndexFailed() { export function MessagesList({ hasScrolled, setHasScrolled, - blocked, footer, hasAcceptOverride, transparentHeaderHeight, }: { hasScrolled: boolean setHasScrolled: React.Dispatch> - blocked?: boolean footer?: React.ReactNode hasAcceptOverride?: boolean transparentHeaderHeight?: number @@ -489,11 +486,7 @@ export function MessagesList({ }), opened: 0, }}> - {convoState.status === ConvoStatus.Disabled ? ( - - ) : blocked ? ( - footer - ) : ( + {footer ?? ( diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts index 393bf9e520..b8f26cc88c 100644 --- a/src/state/queries/messages/conversation.ts +++ b/src/state/queries/messages/conversation.ts @@ -19,19 +19,18 @@ import { const RQKEY_ROOT = 'convo' export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] -export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) { +export function useConvoQuery({convoId}: {convoId: string}) { const agent = useAgent() return useQuery({ - queryKey: RQKEY(convo.id), + queryKey: RQKEY(convoId), queryFn: async () => { const {data} = await agent.chat.bsky.convo.getConvo( - {convoId: convo.id}, + {convoId}, {headers: DM_SERVICE_HEADERS}, ) return data.convo }, - initialData: convo, staleTime: STALE.INFINITY, }) } @@ -58,7 +57,7 @@ export function useMarkAsReadMutation() { }) => { if (!convoId) throw new Error('No convoId provided') - await agent.api.chat.bsky.convo.updateRead( + await agent.chat.bsky.convo.updateRead( { convoId, messageId, From 5df51bdea71d10cf26063cf0be02b6bf37e9f370 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Sun, 19 Apr 2026 03:14:38 +0000 Subject: [PATCH 13/18] Nightly source-language update --- src/locale/locales/en/messages.po | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 0e033396b9..56dfe112b1 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -297,10 +297,6 @@ msgstr "" msgid "{date} at {time}" msgstr "{date} at {time}" -#: src/components/dms/MessagesListHeader.tsx:195 -msgid "{displayName}’s group chat" -msgstr "{displayName}’s group chat" - #: src/lib/generate-starterpack.ts:104 #: src/screens/StarterPack/Wizard/index.tsx:200 msgid "{displayName}'s Starter Pack" @@ -1699,7 +1695,7 @@ msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:94 #: src/components/dms/MessageProfileButton.tsx:60 #: src/screens/Messages/ChatList.tsx:376 -#: src/screens/Messages/Conversation.tsx:266 +#: src/screens/Messages/Conversation.tsx:234 #: src/screens/Messages/ConversationSettings.tsx:552 msgid "Before you can message another user, you must first verify your email." msgstr "" @@ -2727,7 +2723,7 @@ msgstr "Continue to group name" msgid "Continue to next step" msgstr "" -#: src/screens/Messages/Conversation.tsx:66 +#: src/screens/Messages/Conversation.tsx:63 msgid "Conversation" msgstr "" @@ -3223,8 +3219,7 @@ msgstr "" msgid "Deleted" msgstr "" -#: src/components/dms/MessagesListHeader.tsx:128 -#: src/components/dms/MessagesListHeader.tsx:189 +#: src/components/dms/MessagesListHeader.tsx:105 #: src/screens/Messages/components/ChatListItem.tsx:127 #: src/screens/Messages/ConversationSettings.tsx:383 #: src/screens/Messages/ConversationSettings.tsx:599 @@ -5006,10 +5001,6 @@ msgstr "" msgid "Grooming or predatory behavior" msgstr "" -#: src/components/dms/MessagesListHeader.tsx:195 -msgid "Group chat" -msgstr "Group chat" - #: src/screens/Messages/ConversationSettings.tsx:740 msgctxt "toast" msgid "Group chat muted" @@ -7307,7 +7298,7 @@ msgstr "" msgid "Open Germ DM" msgstr "" -#: src/components/dms/MessagesListHeader.tsx:216 +#: src/components/dms/MessagesListHeader.tsx:177 msgid "Open group chat settings" msgstr "Open group chat settings" @@ -9935,7 +9926,7 @@ msgstr "" msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" -#: src/screens/Messages/Conversation.tsx:164 +#: src/screens/Messages/Conversation.tsx:143 #: src/screens/Messages/ConversationSettings.tsx:198 msgid "Something went wrong" msgstr "" @@ -11794,7 +11785,7 @@ msgstr "" msgid "View {0}’s profile" msgstr "View {0}’s profile" -#: src/components/dms/MessagesListHeader.tsx:141 +#: src/components/dms/MessagesListHeader.tsx:118 #: src/screens/Messages/ConversationSettings.tsx:645 msgid "View {displayName}’s profile" msgstr "View {displayName}’s profile" @@ -11971,7 +11962,7 @@ msgstr "" msgid "We couldn't find any results for that topic." msgstr "" -#: src/screens/Messages/Conversation.tsx:165 +#: src/screens/Messages/Conversation.tsx:144 msgid "We couldn't load this conversation" msgstr "" From dddc022747d0a87a5cb9f3267455d29235114639 Mon Sep 17 00:00:00 2001 From: Thomas May Date: Mon, 20 Apr 2026 15:15:05 +0200 Subject: [PATCH 14/18] nit: fix perf regression in hotkeys (#10302) Co-authored-by: Samuel Newman --- src/lib/hotkeys/index.native.tsx | 15 +++++++++++---- src/state/shell/drawer-open.tsx | 21 ++++++++++++--------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/lib/hotkeys/index.native.tsx b/src/lib/hotkeys/index.native.tsx index b5562c9c62..a0fe0e105a 100644 --- a/src/lib/hotkeys/index.native.tsx +++ b/src/lib/hotkeys/index.native.tsx @@ -1,10 +1,17 @@ +import {useMemo} from 'react' + export function Provider({children}: {children: React.ReactNode}) { return children } +const noop = () => {} + export function useHotkeysContext() { - return { - enableScope: () => {}, - disableScope: () => {}, - } + return useMemo( + () => ({ + enableScope: noop, + disableScope: noop, + }), + [], + ) } diff --git a/src/state/shell/drawer-open.tsx b/src/state/shell/drawer-open.tsx index fecf93f003..8ddb43469f 100644 --- a/src/state/shell/drawer-open.tsx +++ b/src/state/shell/drawer-open.tsx @@ -1,4 +1,4 @@ -import {createContext, useContext, useState} from 'react' +import {createContext, useCallback, useContext, useState} from 'react' import {useHotkeysContext} from '#/lib/hotkeys' @@ -14,14 +14,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const [state, setState] = useState(false) const {disableScope, enableScope} = useHotkeysContext() - const setDrawerOpen = (open: boolean) => { - if (open) { - disableScope('global') - } else { - enableScope('global') - } - setState(open) - } + const setDrawerOpen = useCallback( + (open: boolean) => { + if (open) { + disableScope('global') + } else { + enableScope('global') + } + setState(open) + }, + [disableScope, enableScope], + ) return ( From 52b8201d2f213dcb130a52e7903dde40a55d57a2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 20 Apr 2026 06:53:04 -0700 Subject: [PATCH 15/18] Add READMEs to modules (#10306) --- modules/BlueskyClip/README.md | 134 ++++++++++ modules/BlueskyNSE/README.md | 135 ++++++++++ modules/Share-with-Bluesky/README.md | 140 ++++++++++ modules/bottom-sheet/README.md | 248 ++++++++++++++++++ .../README.md | 162 ++++++++++++ modules/expo-bluesky-gif-view/README.md | 167 ++++++++++++ modules/expo-bluesky-swiss-army/README.md | 231 ++++++++++++++++ modules/expo-emoji-picker/README.md | 114 +++++++- .../expo-receive-android-intents/README.md | 123 ++++++++- modules/expo-scroll-forwarder/README.md | 116 ++++++++ 10 files changed, 1564 insertions(+), 6 deletions(-) create mode 100644 modules/BlueskyClip/README.md create mode 100644 modules/BlueskyNSE/README.md create mode 100644 modules/Share-with-Bluesky/README.md create mode 100644 modules/bottom-sheet/README.md create mode 100644 modules/expo-background-notification-handler/README.md create mode 100644 modules/expo-bluesky-gif-view/README.md create mode 100644 modules/expo-bluesky-swiss-army/README.md create mode 100644 modules/expo-scroll-forwarder/README.md diff --git a/modules/BlueskyClip/README.md b/modules/BlueskyClip/README.md new file mode 100644 index 0000000000..147ee50e6d --- /dev/null +++ b/modules/BlueskyClip/README.md @@ -0,0 +1,134 @@ +# BlueskyClip + +An iOS App Clip implementation for Bluesky starter packs. App Clips are lightweight app experiences that allow users to preview and join Bluesky through starter packs without installing the full app. + +## What It Does + +BlueskyClip provides a minimal, on-demand iOS app experience for viewing and joining Bluesky starter packs. When a user encounters a starter pack link (e.g., `bsky.app/start/...` or `go.bsky.app/...`), iOS can present the App Clip instead of requiring a full app install. The App Clip: + +1. Loads the starter pack web page in a WKWebView +2. Allows users to browse the starter pack content +3. Presents the App Store overlay when the user decides to join +4. Passes the starter pack URI to the main app via shared UserDefaults + +## Architecture + +### Native iOS Implementation + +The App Clip is a standalone iOS target with its own minimal Swift implementation: + +- **AppDelegate.swift**: Standard app delegate that sets up the view controller and handles URL routing (both direct URL opens and universal links) +- **ViewController.swift**: Main view controller that manages the WKWebView, detects starter pack URLs, and communicates with the web layer + +### Communication Flow + +``` +User taps starter pack link + ↓ +iOS presents BlueskyClip App Clip + ↓ +WKWebView loads bsky.app with ?clip=true parameter + ↓ +Web app detects clip mode and sends actions via postMessage + ↓ +ViewController receives messages and: + - Presents App Store overlay (action: "present") + - Stores starter pack URI in shared UserDefaults (action: "store") + ↓ +User downloads main app + ↓ +Main app reads starterPackUri from shared UserDefaults + ↓ +Main app displays starter pack onboarding flow +``` + +### Key Implementation Details + +**URL Detection** (`isStarterPackUrl`): +- Matches `bsky.app/start/*` and `bsky.app/starter-pack/*` paths (4 path components) +- Matches short links `go.bsky.app/*` (2 path components) + +**WebView Communication** (`WKScriptMessageHandler`): +- Listens for messages on the "onMessage" channel +- Handles two action types: + - `present`: Shows the App Store overlay using `SKOverlay` + - `store`: Writes JSON data to shared UserDefaults with the specified key + +**Data Sharing**: +- Uses UserDefaults suite `group.app.bsky` (App Group) +- Primary key: `starterPackUri` - stores the starter pack URL +- The main app reads this value on launch via `SharedPrefs.getString('starterPackUri')` (see `src/components/hooks/useStarterPackEntry.native.ts`) + +## Configuration + +### Build Configuration + +The App Clip target is automatically configured via Expo config plugins located in `/plugins/starterPackAppClipExtension/`: + +- **withStarterPackAppClip.js**: Main plugin that orchestrates all configuration +- **withXcodeTarget.js**: Creates the App Clip target in Xcode with proper build settings +- **withAppEntitlements.js**: Configures main app entitlements for App Clip association +- **withClipEntitlements.js**: Sets up App Clip entitlements (App Groups, parent app identifier, associated domains) +- **withClipInfoPlist.js**: Generates the Info.plist for the App Clip target +- **withFiles.js**: Copies Swift source files and assets from `modules/BlueskyClip/` to the iOS build directory + +### Entitlements + +**Main App** (`app.entitlements`): +- `com.apple.security.application-groups`: `group.app.bsky` +- `com.apple.developer.associated-appclip-app-identifiers`: Links to the App Clip bundle ID + +**App Clip** (`BlueskyClip.entitlements`): +- `com.apple.security.application-groups`: `group.app.bsky` (for data sharing) +- `com.apple.developer.parent-application-identifiers`: Links to the main app bundle ID +- `com.apple.developer.associated-domains`: Inherits from main app config (for universal links) + +### Build Settings + +- Deployment target: iOS 15.1+ +- Bundle ID: `[main-app-bundle-id].AppClip` +- Product type: `com.apple.product-type.application.on-demand-install-capable` +- Development team: `B3LX46C5HS` +- Device family: iPhone only (1) + +## Platform Support + +- **iOS**: Full support via native App Clip +- **Android**: Not applicable (no App Clip equivalent) +- **Web**: Not applicable (web uses standard starter pack landing pages) + +## Integration with Main App + +The main app detects App Clip-originated starter packs through `useStarterPackEntry` hook: + +**Native** (`src/components/hooks/useStarterPackEntry.native.ts`): +- Reads `starterPackUri` from `SharedPrefs` (App Group) +- Clears the value after reading to prevent re-use +- Sets active starter pack in app state + +**Web** (`src/components/hooks/useStarterPackEntry.ts`): +- Detects `?clip=true` URL parameter +- Extracts starter pack URI from URL +- Sets active starter pack with `isClip: true` flag + +## Files + +``` +modules/BlueskyClip/ +├── AppDelegate.swift # App lifecycle and URL handling +├── ViewController.swift # WebView management and message handling +└── Images.xcassets/ # App Clip icon assets + ├── AppIcon.appiconset/ + │ ├── App-Icon-1024x1024@1x.png + │ └── Contents.json + └── Contents.json +``` + +## Development Notes + +- The App Clip is built as part of the main Xcode project when running `yarn prebuild` +- Source files are copied during the prebuild process, not directly referenced +- Changes to Swift files require running `yarn prebuild` to take effect +- The App Clip shares the same version number as the main app +- App Clips have a 15MB size limit (enforced by Apple) +- Users can convert an App Clip session into a full app install without losing data (via shared App Group) diff --git a/modules/BlueskyNSE/README.md b/modules/BlueskyNSE/README.md new file mode 100644 index 0000000000..63136141cc --- /dev/null +++ b/modules/BlueskyNSE/README.md @@ -0,0 +1,135 @@ +# BlueskyNSE + +BlueskyNSE is an iOS Notification Service Extension that processes push notifications before they are displayed to the user. NSE stands for "Notification Service Extension", a native iOS app extension type. + +## What It Does + +This extension intercepts incoming push notifications and performs processing before displaying them: + +1. Manages badge counts for app icon +2. Applies custom notification sounds based on user preferences +3. Enables notification customization without requiring the main app to be running + +## How It Works + +When a push notification arrives on iOS, the system can invoke this extension to modify the notification content before displaying it. The extension runs in a separate process from the main app and has strict time limits (approximately 30 seconds) to complete its work. + +### Architecture + +The extension uses shared UserDefaults (via App Groups) to access preferences set by the main app: + +- **App Group**: `group.app.bsky` allows data sharing between the main app and the extension +- **Shared Preferences**: Stored in UserDefaults suite accessible by both processes +- **Thread Safety**: Uses a dedicated serial DispatchQueue (`NSEPrefsQueue`) to prevent race conditions when multiple notifications arrive simultaneously + +### Notification Processing Flow + +1. System receives push notification +2. `NotificationService.didReceive()` is called +3. Extension creates mutable copy of notification content +4. Based on notification type (determined by `reason` field): + - **Chat messages** (`reason == "chat-message"`): Applies custom DM sound if user preference `playSoundChat` is enabled + - **Other notifications**: Increments and applies badge count +5. Extension delivers modified notification to system via `contentHandler` + +### Badge Count Management + +Badge counts are managed centrally by the extension: +- Each non-chat notification increments the badge count +- Count is synchronized across notification instances using the serial queue +- Main app can reset the count via the `expo-background-notification-handler` module + +### Notification Sounds + +Two sound types are supported: +- **Default system sound**: Standard iOS notification sound +- **DM sound**: Custom `dm.aiff` sound file for chat messages + +DM sound only plays if the user has enabled the `playSoundChat` preference in the main app's chat settings. + +## Key Files + +| File | Purpose | +|------|---------| +| `NotificationService.swift` | Main service extension implementation | +| `BlueskyNSE.entitlements` | iOS entitlements configuration for App Group access | +| `Info.plist` | Extension metadata and configuration | + +### NotificationService.swift + +Contains two main classes: + +**NotificationService**: The main extension class that implements `UNNotificationServiceExtension` +- `didReceive(_:withContentHandler:)`: Processes incoming notifications +- `serviceExtensionTimeWillExpire()`: Handles timeout scenarios +- Mutation methods for modifying notification content + +**NSEUtil**: Singleton utility class for shared state management +- Provides shared `UserDefaults` instance for the App Group +- Manages serial queue for thread-safe preference access +- Helper methods for notification content manipulation + +## Configuration + +### App Group Setup + +The extension requires the `group.app.bsky` App Group to be configured in: +1. Main app target capabilities +2. Extension target capabilities (defined in `BlueskyNSE.entitlements`) + +### Shared Preferences + +The following preferences are shared between the main app and extension: + +| Preference Key | Type | Purpose | +|----------------|------|---------| +| `badgeCount` | Int | Current badge count for app icon | +| `playSoundChat` | Bool | Whether to play sound for chat notifications | + +These are managed by the `expo-background-notification-handler` module in the main app. + +### Sound Files + +The custom DM sound file (`dm.aiff`) must be included in the extension's bundle. The iOS project configuration handles copying this resource during the build. + +## Platform Support + +- **iOS**: Fully supported (primary platform for this extension) +- **Android**: Not applicable (Android uses different notification handling mechanisms) +- **Web**: Not applicable (web notifications are handled by browser APIs) + +## Integration with Main App + +The extension coordinates with the main app through: + +1. **expo-background-notification-handler** module: Provides JavaScript API for managing shared preferences +2. **App Group shared storage**: Enables data synchronization between processes +3. **Push notification payload**: Must include `reason` field to determine notification type + +### Setting User Preferences + +Users can control notification sounds via the Chat Settings screen (`src/screens/Messages/Settings.tsx`): + +```typescript +import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' + +const {preferences, setPref} = useBackgroundNotificationPreferences() +setPref('playSoundChat', true) // Enable DM sounds +``` + +## Limitations + +1. **Time constraints**: Extension must complete processing within ~30 seconds or the system will terminate it +2. **Process isolation**: Runs in separate process with limited memory and resources +3. **iOS only**: Notification Service Extensions are an iOS-specific feature +4. **Concurrent processing**: Multiple notifications may arrive simultaneously, requiring careful state management + +## Best Practices + +When modifying this extension: + +1. Keep processing fast and synchronous when possible +2. Use the shared serial queue for any UserDefaults mutations +3. Avoid network requests that could cause timeouts +4. Always call `contentHandler` with modified content, even on errors +5. Test with multiple concurrent notifications to verify thread safety diff --git a/modules/Share-with-Bluesky/README.md b/modules/Share-with-Bluesky/README.md new file mode 100644 index 0000000000..9ba7a1314b --- /dev/null +++ b/modules/Share-with-Bluesky/README.md @@ -0,0 +1,140 @@ +# Share-with-Bluesky + +iOS Share Extension for the Bluesky Social app that enables users to share content from other apps directly to Bluesky. + +## Overview + +This module implements an iOS Share Extension (Action Extension) that appears in the system share sheet when users tap the share button in other iOS apps. It allows sharing text, URLs, images, and videos to create a new Bluesky post. + +## Features + +- Share plain text +- Share URLs (web links) +- Share images (up to 4 images, supports PNG, JPG, JPEG, GIF, HEIC) +- Share videos (single video, supports MOV, MP4, M4V) +- Automatic image dimension extraction +- Automatic video dimension extraction +- App group file sharing for media access + +## Architecture + +### iOS Share Extension + +The extension is implemented as a native iOS Share Extension using Swift. When a user shares content: + +1. The `ShareViewController` receives the shared content from the extension context +2. Content is processed based on its type (text, URL, image, or video) +3. Media files are copied to a shared App Group container (`group.app.bsky`) for access by the main app +4. Image and video dimensions are extracted and encoded into the URI +5. The extension constructs a deep link URL with the content encoded in query parameters +6. The main Bluesky app is opened with the deep link +7. The extension completes and dismisses + +### Deep Link Format + +The extension communicates with the main app using deep links with the `bluesky://` scheme: + +``` +bluesky://intent/compose?text= +bluesky://intent/compose?imageUris=||,|| +bluesky://intent/compose?videoUri=|| +``` + +The scheme can be customized by setting the `MainAppScheme` key in `Info.plist` to support forks. + +### Main App Integration + +The main app handles these deep links in `src/lib/hooks/useIntentHandler.ts`: + +- Parses the deep link parameters +- Validates image/video URIs for security (filters out external URLs) +- Opens the composer with the pre-populated content +- Supports up to 4 images or 1 video per share + +## Key Files + +### Module Files + +- `ShareViewController.swift` - Main view controller that handles share requests and processes content +- `Info.plist` - Extension configuration (activation rules, supported content types) +- `Share-with-Bluesky.entitlements` - App group entitlements for shared file access + +### App Integration + +- `src/lib/hooks/useIntentHandler.ts` - Main app hook that handles incoming deep links +- `android/app/src/main/AndroidManifest.xml` - Android share intent configuration (lines 57-76) + +## Configuration + +### Supported Content Types + +Defined in `Info.plist` under `NSExtensionActivationRule`: + +- Text: Plain text strings +- Web URLs: Up to 1 URL +- Images: Up to 10 images +- Videos: Up to 1 video + +### App Group + +The extension uses the `group.app.bsky` App Group identifier to share files with the main app. This is configured in: + +- `Share-with-Bluesky.entitlements` +- Main app's entitlements file + +### Custom Scheme + +The `MainAppScheme` in `Info.plist` defaults to `bluesky` but can be changed for forks to use a custom URL scheme. + +## Platform Support + +- iOS: Native Share Extension (this module) +- Android: Native share intents handled via MainActivity intent filters in AndroidManifest.xml +- Web: Not applicable (browser share APIs use different mechanisms) + +## Implementation Details + +### Image Processing + +When images are shared: + +1. Images are loaded from the extension's temporary directory or as UIImage objects +2. Images are converted to JPEG format at maximum quality +3. Dimensions are extracted from the UIImage +4. Files are saved to the App Group container with unique names +5. URIs are formatted as `||` + +### Video Processing + +When videos are shared: + +1. Videos are copied from the source URL to the App Group container +2. AVURLAsset is used to extract video track dimensions +3. Track dimensions are adjusted for video rotation using preferredTransform +4. URI is formatted as `||` + +### Security + +- External URLs in image URIs are filtered out in the main app to prevent potential security issues +- Only file:// URLs from the App Group container are accepted +- URI format is validated with a regex pattern before processing + +## Development + +This module is built as part of the main Xcode project. The extension target is included in the iOS build configuration. + +To modify the extension: + +1. Open the Xcode project in `/ios` +2. Navigate to the Share-with-Bluesky target +3. Edit `ShareViewController.swift` for logic changes +4. Edit `Info.plist` for configuration changes +5. Rebuild the iOS app + +## Limitations + +- Images: Maximum of 4 images per share (limited in main app handler) +- Videos: Only 1 video per share +- Mixed media: Cannot share images and videos together +- File size: No explicit limits, but large files may cause issues +- Formats: Only supports common image/video formats listed in constants diff --git a/modules/bottom-sheet/README.md b/modules/bottom-sheet/README.md new file mode 100644 index 0000000000..49007d97b5 --- /dev/null +++ b/modules/bottom-sheet/README.md @@ -0,0 +1,248 @@ +# Bottom Sheet Expo Module + +A custom Expo module that provides native bottom sheet functionality for iOS and Android, using platform-specific native bottom sheet implementations (UISheetPresentationController on iOS, Material BottomSheetDialog on Android). + +## Overview + +This module wraps native bottom sheet components to provide a React Native interface with cross-platform consistency. It uses native presentation APIs rather than JavaScript-based animations for better performance and native behavior. + +Key features: +- Native bottom sheet presentation on iOS and Android +- Automatic content height detection (no JS bridge round-trip) +- Configurable snap points (hidden, partial, full) +- Drag-to-dismiss with prevention controls +- Portal-based rendering for proper z-index layering +- Edge-to-edge support on modern Android versions +- iOS 26+ zoom transition support + +## Platform Support + +- **iOS**: Uses `UISheetPresentationController` (iOS 15+) +- **Android**: Uses Material Design `BottomSheetDialog` with `BottomSheetBehavior` +- **Web**: Not supported (throws error) + +## Architecture + +### TypeScript Layer + +The module exposes a React component that handles rendering and state management: + +- **BottomSheet.tsx** (Native): Main component wrapping the native view +- **BottomSheet.web.tsx** (Web): Stub that throws an error +- **BottomSheetNativeComponent.tsx**: React wrapper with portal integration +- **BottomSheetPortal.tsx**: Portal system for rendering sheets above app content +- **Portal.tsx**: Generic portal implementation for managing component hierarchy + +The component uses a class-based approach to expose imperative methods (`present()`, `dismiss()`, `dismissAll()`). + +### Native Layer + +#### iOS Implementation + +- **BottomSheetModule.swift**: Expo module definition with event handlers and prop bindings +- **SheetView.swift**: Main view component that creates and manages `SheetViewController` + - Observes content height via KVO (Key-Value Observing) on bounds + - Manages sheet lifecycle and state transitions + - Implements `UISheetPresentationControllerDelegate` for drag events +- **SheetViewController.swift**: UIViewController subclass with sheet presentation + - Configures detents (snap points) based on content height + - Handles iOS 26+ safe area adjustments for floating sheet style + - Animates detent changes when content resizes +- **SheetManager.swift**: Singleton that tracks all active sheets with weak references +- **Util.swift**: Helper for calculating screen height minus safe area insets + +#### Android Implementation + +- **BottomSheetModule.kt**: Expo module definition mirroring iOS functionality +- **BottomSheetView.kt**: Main view component managing Material BottomSheetDialog + - Uses `OnLayoutChangeListener` to observe content height natively + - Configures `BottomSheetBehavior` for drag and snap behavior + - Handles edge-to-edge display across Android versions (API 29-35+) + - Preserves status/nav bar appearance from host activity +- **DialogRootViewGroup.kt**: Custom ViewGroup acting as RootView for the dialog + - Forwards touch events to React Native event system + - Updates shadow node size to match window dimensions + - Based on React Native's ReactModalHostView pattern +- **SheetManager.kt**: Singleton for tracking sheets (same pattern as iOS) + +### Content Height Detection + +Both platforms detect content height changes natively without JS bridge round-trips: + +- **iOS**: KVO observation on the content view's `bounds` property +- **Android**: `OnLayoutChangeListener` on child views (catches React Native's direct `layout()` calls) + +This eliminates layout jank when content changes (e.g., keyboard appearance, dynamic content loading). + +## Props + +```typescript +interface BottomSheetViewProps { + children: React.ReactNode + + // Appearance + cornerRadius?: number + backgroundColor?: ColorValue + containerBackgroundColor?: ColorValue + + // Behavior + preventDismiss?: boolean // Disable swipe-to-dismiss + preventExpansion?: boolean // Lock to initial height (no full-screen) + disableDrag?: boolean // Disable drag handle (Android only) + fullHeight?: boolean // Start at full screen height + + // Height constraints + minHeight?: number // Minimum height in dp + maxHeight?: number // Maximum height in dp + + // iOS 26+ transition + sourceViewTag?: number // View tag for zoom transition origin + + // Events + onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void + onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void + onStateChange?: (event: BottomSheetStateChangeEvent) => void +} +``` + +## States and Snap Points + +### States +- `closed`: Sheet is dismissed +- `closing`: Sheet is animating closed +- `open`: Sheet is fully visible +- `opening`: Sheet is animating open + +### Snap Points +- `Hidden` (0): Dismissed +- `Partial` (1): Half-expanded / content height +- `Full` (2): Expanded to screen height + +## Usage + +### Basic Example + +```tsx +import {BottomSheet, BottomSheetProvider, BottomSheetOutlet} from '@modules/bottom-sheet' + +// In your app root: +function App() { + return ( + + + + + ) +} + +// In a component: +function MyComponent() { + const sheetRef = useRef(null) + + const openSheet = () => { + sheetRef.current?.present() + } + + const closeSheet = () => { + sheetRef.current?.dismiss() + } + + return ( + <> +