From 153772b760ef1f67220efe40ca6b4c66d33af71b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 4 Jun 2026 01:03:11 +0300 Subject: [PATCH 01/16] `expo-image` tweaks (#10710) --- src/components/ContextMenu/index.tsx | 1 + .../pager/ImageItem/ImageItem.ios.tsx | 1 + src/components/Lightbox/pager/ImagePager.tsx | 4 ++++ src/components/MediaPreview.tsx | 1 + .../Post/Embed/ExternalEmbed/index.tsx | 1 + .../Post/Embed/StandardSiteEmbed/index.tsx | 1 + .../contacts/FindContactsBannerNUX.tsx | 1 + .../contacts/components/HeroImage.tsx | 1 + .../dialogs/nuxs/ActivitySubscriptions.tsx | 1 + .../dialogs/nuxs/BookmarksAnnouncement.tsx | 1 + .../dialogs/nuxs/DraftsAnnouncement.tsx | 1 + .../dialogs/nuxs/FindContactsAnnouncement.tsx | 1 + .../nuxs/InitialVerificationAnnouncement.tsx | 2 ++ .../dialogs/nuxs/LiveNowBetaDialog.tsx | 1 + src/components/images/AutoSizedImage.tsx | 1 + src/components/images/Gallery/index.tsx | 1 + src/components/images/ImageLayoutGridItem.tsx | 1 + .../verification/VerifierDialog.tsx | 1 + .../components/LiveEventFeedCardCompact.tsx | 1 + .../components/LiveEventFeedCardWide.tsx | 1 + .../liveNow/components/LinkPreview.tsx | 1 + .../liveNow/components/LiveStatusDialog.tsx | 1 + .../StepFinished/ValuePropositionPager.tsx | 2 ++ src/screens/Profile/components/GermButton.tsx | 1 + src/view/com/composer/ComposerReplyTo.tsx | 20 +++++++++---------- src/view/com/composer/photos/Gallery.tsx | 1 + src/view/com/util/UserAvatar.tsx | 1 + src/view/com/util/UserBanner.tsx | 2 ++ 28 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index 8d3d39a749..51bf83309f 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -499,6 +499,7 @@ function TriggerClone({ accessibilityLabel={label} accessibilityHint={_(msg`The subject of the context menu`)} accessibilityIgnoresInvertColors={false} + cachePolicy="none" /> ) diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx index c0bb99c89e..19373cbe1a 100644 --- a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx +++ b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx @@ -246,6 +246,7 @@ const ImageItem = ({ } } cachePolicy="memory" + useAppleWebpCodec /> diff --git a/src/components/Lightbox/pager/ImagePager.tsx b/src/components/Lightbox/pager/ImagePager.tsx index 1c8836a0fd..7b8815f276 100644 --- a/src/components/Lightbox/pager/ImagePager.tsx +++ b/src/components/Lightbox/pager/ImagePager.tsx @@ -32,6 +32,7 @@ import Animated, { withSpring, type WithSpringConfig, } from 'react-native-reanimated' +import {Image} from 'expo-image' import * as ScreenOrientation from 'expo-screen-orientation' import {type Dimensions} from '#/lib/media/types' @@ -136,6 +137,9 @@ export default function ImageViewRoot({ 'worklet' thumbRects.set({}) })() + requestIdleCallback(() => { + void Image.clearMemoryCache() + }) }, [thumbRects]) useAnimatedReaction( diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index 9aad6da864..b36705d840 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -129,6 +129,7 @@ export function ImageItem({ contentFit="cover" accessible={true} accessibilityIgnoresInvertColors + useAppleWebpCodec /> {children} diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index 43bfbb4747..fe0be2bd25 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -108,6 +108,7 @@ export const ExternalEmbed = ({ source={{uri: imageUri}} accessibilityIgnoresInvertColors loading="lazy" + useAppleWebpCodec /> ) : undefined} diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 70fce8ad77..5f7fc1f5e7 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -166,6 +166,7 @@ export const StandardSiteEmbed = ({ source={{uri: imageUri}} accessibilityIgnoresInvertColors loading="lazy" + useAppleWebpCodec /> ) : undefined} diff --git a/src/components/contacts/FindContactsBannerNUX.tsx b/src/components/contacts/FindContactsBannerNUX.tsx index 2997e00d4e..417379fa2b 100644 --- a/src/components/contacts/FindContactsBannerNUX.tsx +++ b/src/components/contacts/FindContactsBannerNUX.tsx @@ -60,6 +60,7 @@ export function FindContactsBannerNUX() { a.self_end, a.mt_sm, ]} + useAppleWebpCodec /> ) diff --git a/src/components/dialogs/nuxs/ActivitySubscriptions.tsx b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx index ed4306459c..318b130414 100644 --- a/src/components/dialogs/nuxs/ActivitySubscriptions.tsx +++ b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx @@ -113,6 +113,7 @@ export function ActivitySubscriptionsNUX() { alt={_( msg`A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature.`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx index f7e4bf04c6..153130f8ec 100644 --- a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx +++ b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx @@ -124,6 +124,7 @@ export function BookmarksAnnouncement() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/DraftsAnnouncement.tsx b/src/components/dialogs/nuxs/DraftsAnnouncement.tsx index 79488fe4d9..37b93bda70 100644 --- a/src/components/dialogs/nuxs/DraftsAnnouncement.tsx +++ b/src/components/dialogs/nuxs/DraftsAnnouncement.tsx @@ -101,6 +101,7 @@ export function DraftsAnnouncement() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx index 8fd7d2d2c5..70f29ef157 100644 --- a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx +++ b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx @@ -78,6 +78,7 @@ export function FindContactsAnnouncement() { alt={_( msg`An illustration depicting user avatars flowing from a contact book into the Bluesky app`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx index 85bc29a48f..cbe2555aac 100644 --- a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx +++ b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx @@ -85,6 +85,7 @@ export function InitialVerificationAnnouncement() { alt={_( msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, )} + useAppleWebpCodec /> @@ -119,6 +120,7 @@ export function InitialVerificationAnnouncement() { alt={_( msg`An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name.`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx index 49c43a1e41..e389e11175 100644 --- a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx +++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx @@ -150,6 +150,7 @@ export function LiveNowBetaDialog() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/images/AutoSizedImage.tsx b/src/components/images/AutoSizedImage.tsx index 79b41311cb..182bfe661b 100644 --- a/src/components/images/AutoSizedImage.tsx +++ b/src/components/images/AutoSizedImage.tsx @@ -142,6 +142,7 @@ export function AutoSizedImage({ } }} loading="lazy" + useAppleWebpCodec /> diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index fe2b63e606..9ae677c2d2 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -480,6 +480,7 @@ function GalleryImage({ height: e.source.height, }) }} + useAppleWebpCodec /> {(hasAlt || isCropped) && !hideBadges ? ( diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index 0c349b6612..d6cbc0aded 100644 --- a/src/components/images/ImageLayoutGridItem.tsx +++ b/src/components/images/ImageLayoutGridItem.tsx @@ -106,6 +106,7 @@ export function GalleryItem({ } }} loading="lazy" + useAppleWebpCodec /> diff --git a/src/components/verification/VerifierDialog.tsx b/src/components/verification/VerifierDialog.tsx index 570441ecd0..e8944024ca 100644 --- a/src/components/verification/VerifierDialog.tsx +++ b/src/components/verification/VerifierDialog.tsx @@ -87,6 +87,7 @@ function Inner({ alt={_( msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, )} + useAppleWebpCodec /> diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx index df2bb17f83..9d2f948eb6 100644 --- a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx @@ -69,6 +69,7 @@ export function LiveEventFeedCardCompact({ style={[a.absolute, a.inset_0, a.w_full, a.h_full]} contentFit="cover" placeholderContentFit="cover" + useAppleWebpCodec /> setImageLoadError(false)} onError={() => setImageLoadError(true)} + useAppleWebpCodec /> )} {linkMeta && (!linkMeta.image || imageLoadError) && ( diff --git a/src/features/liveNow/components/LiveStatusDialog.tsx b/src/features/liveNow/components/LiveStatusDialog.tsx index 545e8ab943..9e916678f2 100644 --- a/src/features/liveNow/components/LiveStatusDialog.tsx +++ b/src/features/liveNow/components/LiveStatusDialog.tsx @@ -147,6 +147,7 @@ export function LiveStatus({ contentFit="cover" style={[a.absolute, a.inset_0]} accessibilityIgnoresInvertColors + useAppleWebpCodec /> {page === 1 && ( {_(msg`Your )} diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx index 0bef3a22c3..e37e57edbf 100644 --- a/src/screens/Profile/components/GermButton.tsx +++ b/src/screens/Profile/components/GermButton.tsx @@ -105,6 +105,7 @@ function GermLogo({size}: {size: 'small' | 'large'}) { source={require('../../../../assets/images/germ_logo.webp')} accessibilityIgnoresInvertColors={false} contentFit="cover" + useAppleWebpCodec style={[ a.rounded_full, size === 'large' ? {width: 32, height: 32} : {width: 16, height: 16}, diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index a6ef1a344c..bac3e74d31 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -151,8 +151,8 @@ function ComposerReplyToImages({ )) || (images.length === 2 && ( @@ -160,14 +160,14 @@ function ComposerReplyToImages({ )) || @@ -176,21 +176,21 @@ function ComposerReplyToImages({ @@ -201,28 +201,28 @@ function ComposerReplyToImages({ diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 7624b585f6..2cc000019f 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -245,6 +245,7 @@ const GalleryItem = ({ }} accessible={true} accessibilityIgnoresInvertColors + enforceEarlyResizing cachePolicy="none" autoplay={false} contentFit="cover" diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 420fd0270f..9db4914b21 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -330,6 +330,7 @@ let UserAvatar = ({ }} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} onLoad={onLoad} + useAppleWebpCodec /> )} {!noBorder && } diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 8146b7ad9d..033e8c14fe 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -129,6 +129,7 @@ export function UserBanner({ source={{uri: banner}} accessible={true} accessibilityIgnoresInvertColors + useAppleWebpCodec /> ) : ( ) : ( Date: Thu, 4 Jun 2026 03:21:17 +0000 Subject: [PATCH 02/16] Nightly source-language update --- src/locale/locales/en/messages.po | 424 +++++++++++++++--------------- 1 file changed, 212 insertions(+), 212 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index b1fd0337d7..5830797c52 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -93,7 +93,7 @@ msgstr "{0, plural, one {# person} other {# people}}" #. placeholder {0}: reactions.length #. placeholder {1}: groupedReactions.map(g => g.value).join(' ') -#: src/components/dms/MessageItem.tsx:297 +#: src/components/dms/MessageItem.tsx:273 msgid "{0, plural, one {# person} other {# people}} reacted – {1}" msgstr "{0, plural, one {# person} other {# people}} reacted – {1}" @@ -122,7 +122,7 @@ msgstr "" #. How long it takes to read an article, in minutes. Displayed in a short form, e.g. "5m" for 5 minutes. #. placeholder {0}: view.readingTime -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:232 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:233 msgid "{0, plural, one {#m} other {#m}}" msgstr "{0, plural, one {#m} other {#m}}" @@ -211,7 +211,7 @@ msgid "{0} following" msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:640 +#: src/components/dms/MessageItem.tsx:609 msgid "{0} is blocking you" msgstr "{0} is blocking you" @@ -269,7 +269,7 @@ msgstr "" #. placeholder {0}: createSanitizedDisplayName(memberSender) #. placeholder {1}: reaction.value -#: src/components/dms/MessageItem.tsx:292 +#: src/components/dms/MessageItem.tsx:268 msgid "{0} reacted {1}" msgstr "" @@ -314,8 +314,8 @@ msgstr "" #. placeholder {0}: sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), ) #. placeholder {0}: sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), ) -#: src/view/com/util/UserAvatar.tsx:595 -#: src/view/com/util/UserAvatar.tsx:613 +#: src/view/com/util/UserAvatar.tsx:596 +#: src/view/com/util/UserAvatar.tsx:614 msgid "{0}'s avatar" msgstr "" @@ -839,7 +839,7 @@ msgstr "" msgid "A new code has been sent" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:93 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:94 msgid "A new form of verification" msgstr "" @@ -897,7 +897,7 @@ msgid "Accept join request" msgstr "Accept join request" #. Accept a chat request -#: src/screens/Messages/components/RequestListItem.tsx:48 +#: src/screens/Messages/components/RequestListItem.tsx:51 msgid "Accept Request" msgstr "" @@ -999,7 +999,7 @@ msgctxt "toast" msgid "Account unmuted" msgstr "" -#: src/components/verification/VerifierDialog.tsx:99 +#: src/components/verification/VerifierDialog.tsx:100 msgid "Accounts with a scalloped blue check mark <0><1/> can verify others. These trusted verifiers are selected by Bluesky." msgstr "" @@ -1085,7 +1085,7 @@ msgstr "" msgid "Add App Password" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:166 +#: src/screens/Settings/AutomationLabelSettings.tsx:170 msgid "Add automation label to account" msgstr "Add automation label to account" @@ -1112,8 +1112,8 @@ msgstr "" msgid "Add members" msgstr "Add members" -#: src/components/moderation/ReportDialog/index.tsx:526 -#: src/components/moderation/ReportDialog/index.tsx:530 +#: src/components/moderation/ReportDialog/index.tsx:528 +#: src/components/moderation/ReportDialog/index.tsx:532 msgid "Add more details (optional)" msgstr "" @@ -1215,7 +1215,7 @@ msgstr "" msgid "Additional details (limit 1000 characters)" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:544 +#: src/components/moderation/ReportDialog/index.tsx:546 msgid "Additional details (limit 300 characters)" msgstr "" @@ -1283,7 +1283,7 @@ msgid "alice@example.com" msgstr "" #. the default tab in the interests tab bar -#: src/components/dms/ReactionsDialog.tsx:284 +#: src/components/dms/ReactionsDialog.tsx:288 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:201 #: src/view/screens/Notifications.tsx:86 msgid "All" @@ -1291,7 +1291,7 @@ msgstr "" #. Tab label showing the total count of reactions on a chat message. #. placeholder {0}: tab.count -#: src/components/dms/ReactionsDialog.tsx:381 +#: src/components/dms/ReactionsDialog.tsx:385 msgid "All {0}" msgstr "All {0}" @@ -1388,9 +1388,9 @@ msgstr "" msgid "Already signed in as @{0}" msgstr "" -#: src/components/images/AutoSizedImage.tsx:203 -#: src/components/images/Gallery/index.tsx:531 -#: src/components/images/ImageLayoutGridItem.tsx:138 +#: src/components/images/AutoSizedImage.tsx:204 +#: src/components/images/Gallery/index.tsx:539 +#: src/components/images/ImageLayoutGridItem.tsx:139 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94 #: src/view/com/composer/GifAltText.tsx:100 #: src/view/com/composer/photos/Gallery.tsx:209 @@ -1540,7 +1540,7 @@ msgstr "" msgid "An issue occurred, please try again." msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:120 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:121 msgid "An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name." msgstr "" @@ -1729,7 +1729,7 @@ msgstr "" msgid "Are you sure you want to delete the app password \"{0}\"?" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:223 +#: src/components/dms/MessageOverlays.tsx:167 msgid "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants." msgstr "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants." @@ -1968,7 +1968,7 @@ msgstr "" msgid "Block accounts" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:143 +#: src/components/dms/AfterReportDialog.tsx:148 msgid "Block and delete" msgstr "Block and delete" @@ -1992,9 +1992,9 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:221 #: src/components/dms/AfterReportConversationDialog.tsx:224 -#: src/components/dms/AfterReportDialog.tsx:149 -#: src/components/dms/AfterReportDialog.tsx:184 -#: src/components/dms/AfterReportDialog.tsx:187 +#: src/components/dms/AfterReportDialog.tsx:154 +#: src/components/dms/AfterReportDialog.tsx:189 +#: src/components/dms/AfterReportDialog.tsx:192 msgid "Block user" msgstr "" @@ -2004,7 +2004,7 @@ msgctxt "button" msgid "Block user" msgstr "Block user" -#: src/components/dms/AfterReportDialog.tsx:180 +#: src/components/dms/AfterReportDialog.tsx:185 msgid "Block user and/or delete this conversation" msgstr "" @@ -2085,7 +2085,7 @@ msgstr "" msgid "Bluesky is more fun with friends" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:107 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:108 msgid "Bluesky is more fun with friends! Import your contacts to see who’s already here." msgstr "" @@ -2109,7 +2109,7 @@ msgstr "" msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:134 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:136 msgid "Bluesky will proactively verify notable and authentic accounts." msgstr "" @@ -2198,7 +2198,7 @@ msgstr "" #. placeholder {0}: sanitizeHandle(handle, '@') #. placeholder {0}: sanitizeHandle(item.feed.creator.handle, '@') #: src/components/LabelingServiceCard/index.tsx:62 -#: src/components/moderation/ReportDialog/index.tsx:845 +#: src/components/moderation/ReportDialog/index.tsx:847 #: src/screens/Messages/JoinRequest.tsx:166 #: src/screens/Search/components/StarterPackCard.tsx:107 #: src/screens/Search/Explore.tsx:971 @@ -2322,7 +2322,7 @@ msgstr "" msgid "Captions & alt text" msgstr "" -#: src/components/images/Gallery/index.tsx:256 +#: src/components/images/Gallery/index.tsx:263 msgid "carousel" msgstr "carousel" @@ -2355,7 +2355,7 @@ msgstr "" msgid "Change Handle" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:443 +#: src/components/moderation/ReportDialog/index.tsx:445 msgid "Change moderation service" msgstr "" @@ -2368,11 +2368,11 @@ msgstr "" msgid "Change password dialog" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:310 +#: src/components/moderation/ReportDialog/index.tsx:312 msgid "Change report category" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:388 +#: src/components/moderation/ReportDialog/index.tsx:390 msgid "Change report reason" msgstr "" @@ -2643,7 +2643,7 @@ msgstr "Click here to view the invite link for this group chat" msgid "Click to open tag menu for {0}" msgstr "" -#: src/components/dms/MessageItem.tsx:559 +#: src/components/dms/MessageItem.tsx:528 msgid "Click to retry failed message" msgstr "" @@ -2657,15 +2657,15 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:233 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:239 #: src/components/dialogs/LanguageSelectDialog.tsx:359 -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:159 -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:168 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:164 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:172 -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:139 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:176 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:185 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:191 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:199 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:160 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:169 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:165 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:173 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:140 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:187 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:192 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:200 #: src/components/dialogs/SearchablePeopleList.tsx:340 #: src/components/dialogs/StarterPackDialog.tsx:187 #: src/components/dms/AddMembersFlow.tsx:367 @@ -2673,10 +2673,10 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:96 #: src/components/dms/AfterReportConversationDialog.tsx:247 #: src/components/dms/AfterReportConversationDialog.tsx:252 -#: src/components/dms/AfterReportDialog.tsx:89 #: src/components/dms/AfterReportDialog.tsx:94 -#: src/components/dms/AfterReportDialog.tsx:207 +#: src/components/dms/AfterReportDialog.tsx:99 #: src/components/dms/AfterReportDialog.tsx:212 +#: src/components/dms/AfterReportDialog.tsx:217 #: src/components/dms/EmojiPopup.android.tsx:59 #: src/components/dms/InitiateChatFlow.tsx:534 #: src/components/intents/GroupChatJoinDialog.tsx:204 @@ -2688,7 +2688,7 @@ msgstr "" #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:122 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:128 #: src/components/verification/VerificationsDialog.tsx:146 -#: src/components/verification/VerifierDialog.tsx:146 +#: src/components/verification/VerifierDialog.tsx:147 #: src/components/WhoCanReply.tsx:236 #: src/components/WhoCanReply.tsx:243 #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:45 @@ -2724,7 +2724,7 @@ msgstr "" #: src/components/dialogs/LanguageSelectDialog.tsx:354 #: src/components/dialogs/NotificationSettingsDialog.tsx:94 #: src/components/verification/VerificationsDialog.tsx:138 -#: src/components/verification/VerifierDialog.tsx:139 +#: src/components/verification/VerifierDialog.tsx:140 #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:36 msgid "Close dialog" msgstr "" @@ -2999,8 +2999,8 @@ msgstr "" msgid "Conversation deleted" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:144 -#: src/components/dms/AfterReportDialog.tsx:147 +#: src/components/dms/AfterReportDialog.tsx:149 +#: src/components/dms/AfterReportDialog.tsx:152 msgctxt "toast" msgid "Conversation deleted" msgstr "" @@ -3020,7 +3020,7 @@ msgstr "Conversation not found." msgid "Copied build version to clipboard" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:79 +#: src/components/dms/MessageContextMenu.tsx:71 #: src/components/PostControls/DiscoverDebug.tsx:36 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:272 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:77 @@ -3102,8 +3102,8 @@ msgstr "" msgid "Copy link to starter pack" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:169 -#: src/components/dms/MessageContextMenu.tsx:173 +#: src/components/dms/MessageContextMenu.tsx:152 +#: src/components/dms/MessageContextMenu.tsx:156 msgid "Copy message text" msgstr "" @@ -3160,7 +3160,7 @@ msgid "Could not follow all matches. {0}" msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:158 -#: src/components/dms/AfterReportDialog.tsx:134 +#: src/components/dms/AfterReportDialog.tsx:139 #: src/components/dms/LeaveConvoPrompt.tsx:34 msgid "Could not leave chat" msgstr "" @@ -3316,8 +3316,8 @@ msgstr "Create or modify an invite link for this group chat" #. Accessibility label for button to create a moderation report for the selected option #. placeholder {0}: option.title -#: src/components/moderation/ReportDialog/index.tsx:707 -#: src/components/moderation/ReportDialog/index.tsx:752 +#: src/components/moderation/ReportDialog/index.tsx:709 +#: src/components/moderation/ReportDialog/index.tsx:754 msgid "Create report for {0}" msgstr "" @@ -3418,7 +3418,7 @@ msgstr "" msgid "Default icons" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:224 +#: src/components/dms/MessageOverlays.tsx:168 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:803 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:275 #: src/screens/Settings/AppPasswords.tsx:213 @@ -3459,15 +3459,15 @@ msgstr "" msgid "Delete contacts" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:146 -#: src/components/dms/AfterReportDialog.tsx:190 -#: src/components/dms/AfterReportDialog.tsx:193 +#: src/components/dms/AfterReportDialog.tsx:151 +#: src/components/dms/AfterReportDialog.tsx:195 +#: src/components/dms/AfterReportDialog.tsx:198 #: src/screens/Messages/components/RequestButtons.tsx:142 #: src/screens/Messages/components/RequestButtons.tsx:144 msgid "Delete conversation" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:184 +#: src/components/dms/MessageContextMenu.tsx:167 msgid "Delete for me" msgstr "" @@ -3476,11 +3476,11 @@ msgstr "" msgid "Delete list" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:222 +#: src/components/dms/MessageOverlays.tsx:166 msgid "Delete message" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:181 +#: src/components/dms/MessageContextMenu.tsx:164 msgid "Delete message for me" msgstr "" @@ -3661,8 +3661,8 @@ msgstr "" msgid "Discard post?" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:261 -#: src/screens/Profile/components/GermButton.tsx:268 +#: src/screens/Profile/components/GermButton.tsx:262 +#: src/screens/Profile/components/GermButton.tsx:269 msgid "Disconnect Germ DM" msgstr "" @@ -3690,7 +3690,7 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/components/contacts/FindContactsBannerNUX.tsx:77 +#: src/components/contacts/FindContactsBannerNUX.tsx:78 msgid "Dismiss banner" msgstr "" @@ -3784,7 +3784,7 @@ msgstr "" #: src/components/dialogs/ServerInput.tsx:237 #: src/components/dialogs/ServerInput.tsx:239 #: src/components/dms/AfterReportConversationDialog.tsx:164 -#: src/components/dms/AfterReportDialog.tsx:140 +#: src/components/dms/AfterReportDialog.tsx:145 #: src/components/forms/DateField/index.tsx:104 #: src/components/forms/DateField/index.tsx:110 #: src/lib/media/picker.tsx:37 @@ -3805,7 +3805,7 @@ msgctxt "action" msgid "Done" msgstr "" -#: src/components/dms/MessageItem.tsx:463 +#: src/components/dms/MessageItem.tsx:432 msgid "Double tap or long press the message to add a reaction" msgstr "" @@ -3848,7 +3848,7 @@ msgstr "Download profile data" msgid "Doxxing" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:119 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:120 #: src/view/com/composer/drafts/DraftsButton.tsx:70 #: src/view/com/composer/drafts/DraftsButton.tsx:79 #: src/view/com/composer/drafts/DraftsListDialog.tsx:119 @@ -3917,7 +3917,7 @@ msgctxt "action" msgid "Edit" msgstr "" -#: src/view/com/util/UserAvatar.tsx:460 +#: src/view/com/util/UserAvatar.tsx:461 #: src/view/com/util/UserBanner.tsx:122 msgid "Edit avatar" msgstr "" @@ -4299,7 +4299,7 @@ msgstr "" msgid "Exit fullscreen" msgstr "" -#: src/components/Lightbox/chrome/Footer.tsx:65 +#: src/components/Lightbox/chrome/Footer.tsx:59 #: src/components/Lightbox/Lightbox.web.tsx:241 msgid "Expand alt text" msgstr "" @@ -4420,7 +4420,7 @@ msgid "Failed to accept join request" msgstr "Failed to accept join request" #: src/components/dms/ActionsWrapper.web.tsx:77 -#: src/components/dms/MessageContextMenu.tsx:119 +#: src/components/dms/MessageContextMenu.tsx:103 msgid "Failed to add emoji reaction" msgstr "" @@ -4465,7 +4465,7 @@ msgctxt "toast" msgid "Failed to delete chat" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:101 +#: src/components/dms/MessageOverlays.tsx:112 msgid "Failed to delete message" msgstr "" @@ -4482,7 +4482,7 @@ msgid "Failed to disable invite link" msgstr "Failed to disable invite link" #. placeholder {0}: error?.message -#: src/screens/Profile/components/GermButton.tsx:195 +#: src/screens/Profile/components/GermButton.tsx:196 msgid "Failed to disconnect Germ DM. Error: {0}" msgstr "" @@ -4593,7 +4593,7 @@ msgid "Failed to pin post" msgstr "" #. placeholder {0}: e?.message -#: src/screens/Profile/components/GermButton.tsx:163 +#: src/screens/Profile/components/GermButton.tsx:164 msgid "Failed to reconnect Germ DM. Error: {0}" msgstr "" @@ -4607,8 +4607,8 @@ msgid "Failed to remove data. {0}" msgstr "" #: src/components/dms/ActionsWrapper.web.tsx:73 -#: src/components/dms/MessageContextMenu.tsx:115 -#: src/components/dms/ReactionsDialog.tsx:171 +#: src/components/dms/MessageContextMenu.tsx:99 +#: src/components/dms/ReactionsDialog.tsx:175 msgid "Failed to remove emoji reaction" msgstr "" @@ -4833,11 +4833,11 @@ msgstr "" msgid "Finalizing" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:145 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:146 msgid "Finally!" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:155 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:156 msgid "Finally! Keep track of posts that matter to you. Save them to revisit anytime." msgstr "" @@ -4885,7 +4885,7 @@ msgstr "" msgid "Find posts, users, and feeds on Bluesky" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:97 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:98 msgid "Find your friends" msgstr "" @@ -5191,20 +5191,20 @@ msgid "Generate new link" msgstr "Generate new link" #: src/screens/Profile/components/GermButton.tsx:86 -#: src/screens/Profile/components/GermButton.tsx:224 +#: src/screens/Profile/components/GermButton.tsx:225 msgid "Germ DM" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:182 +#: src/screens/Profile/components/GermButton.tsx:183 msgid "Germ DM disconnected" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:233 -#: src/screens/Profile/components/GermButton.tsx:238 +#: src/screens/Profile/components/GermButton.tsx:234 +#: src/screens/Profile/components/GermButton.tsx:239 msgid "Germ DM Link" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:159 +#: src/screens/Profile/components/GermButton.tsx:160 msgid "Germ DM reconnected" msgstr "" @@ -5268,7 +5268,7 @@ msgstr "" msgid "Get notified when {name} posts" msgstr "" -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:138 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:139 msgid "Get notified when someone posts" msgstr "" @@ -5279,7 +5279,7 @@ msgstr "" msgid "Get started" msgstr "" -#: src/components/MediaPreview.tsx:147 +#: src/components/MediaPreview.tsx:148 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:69 msgid "GIF" msgstr "" @@ -5413,8 +5413,8 @@ msgstr "Go to user’s profile" msgid "Going live is currently disabled for your account" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:252 -#: src/screens/Profile/components/GermButton.tsx:257 +#: src/screens/Profile/components/GermButton.tsx:253 +#: src/screens/Profile/components/GermButton.tsx:258 msgid "Got it" msgstr "" @@ -5524,7 +5524,7 @@ msgid "Handle too long. Please try a shorter one." msgstr "" #: src/components/FeedCard.tsx:156 -#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:122 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:123 msgid "Happening now" msgstr "" @@ -5903,7 +5903,7 @@ msgid "Image" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:436 +#: src/components/images/Gallery/index.tsx:443 msgid "Image {0}" msgstr "Image {0}" @@ -5914,7 +5914,7 @@ msgid "Image {0} of {1}" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:421 +#: src/components/images/Gallery/index.tsx:428 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5929,17 +5929,17 @@ msgid "Image cache cleared, freed {0}" msgstr "" #. placeholder {0}: images.length -#: src/components/images/Gallery/index.tsx:257 +#: src/components/images/Gallery/index.tsx:264 msgid "Image gallery, {0} images" msgstr "Image gallery, {0} images" #. Image has been moderated and user has the option of showing it temporarily -#: src/features/liveNow/components/LiveStatusDialog.tsx:299 +#: src/features/liveNow/components/LiveStatusDialog.tsx:300 msgid "Image is hidden due to your moderation settings." msgstr "Image is hidden due to your moderation settings." #. Image has been moderated and is not visible to the user -#: src/features/liveNow/components/LiveStatusDialog.tsx:309 +#: src/features/liveNow/components/LiveStatusDialog.tsx:310 msgid "Image is unavailable." msgstr "Image is unavailable." @@ -5974,13 +5974,13 @@ msgstr "" msgid "Import contacts" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:115 -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:126 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:116 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:127 msgid "Import Contacts" msgstr "" #: src/components/contacts/FindContactsBannerNUX.tsx:33 -#: src/components/contacts/FindContactsBannerNUX.tsx:71 +#: src/components/contacts/FindContactsBannerNUX.tsx:72 msgid "Import contacts to find your friends" msgstr "" @@ -6093,7 +6093,7 @@ msgstr "" msgid "Invalid phone number" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:98 +#: src/components/moderation/ReportDialog/index.tsx:100 msgid "Invalid report subject" msgstr "" @@ -6311,7 +6311,7 @@ msgid "Latest" msgstr "" #: src/components/verification/VerificationsDialog.tsx:168 -#: src/components/verification/VerifierDialog.tsx:135 +#: src/components/verification/VerifierDialog.tsx:136 #: src/screens/Moderation/VerificationSettings.tsx:50 #: src/screens/Profile/Header/EditProfileDialog.tsx:346 #: src/screens/Settings/components/ChangeHandleDialog.tsx:215 @@ -6366,7 +6366,7 @@ msgid "Learn more about this warning" msgstr "" #: src/components/verification/VerificationsDialog.tsx:153 -#: src/components/verification/VerifierDialog.tsx:121 +#: src/components/verification/VerifierDialog.tsx:122 msgctxt "english-only-resource" msgid "Learn more about verification on Bluesky" msgstr "" @@ -6376,7 +6376,7 @@ msgstr "" msgid "Learn more about what is public on Bluesky." msgstr "" -#: src/screens/Profile/components/GermButton.tsx:211 +#: src/screens/Profile/components/GermButton.tsx:212 msgid "Learn more about your Germ DM link" msgstr "" @@ -6674,7 +6674,7 @@ msgstr "" msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface." msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:244 +#: src/features/liveNow/components/LiveStatusDialog.tsx:245 msgid "Live feature is in beta" msgstr "" @@ -6904,18 +6904,18 @@ msgstr "Message {displayName}" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:100 +#: src/components/dms/MessageOverlays.tsx:111 msgctxt "toast" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageItem.tsx:553 +#: src/components/dms/MessageItem.tsx:522 msgid "Message failed to send." msgstr "Message failed to send." #. placeholder {0}: sender?.handle ?? 'unknown' #. placeholder {1}: message.text -#: src/components/dms/MessageContextMenu.tsx:146 +#: src/components/dms/MessageContextMenu.tsx:129 msgid "Message from @{0}: {1}" msgstr "" @@ -6938,7 +6938,7 @@ msgstr "" msgid "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" -#: src/components/dms/MessageContextMenu.tsx:145 +#: src/components/dms/MessageContextMenu.tsx:128 msgid "Message options" msgstr "" @@ -6946,11 +6946,11 @@ msgstr "" msgid "Messages" msgstr "" -#: src/components/dms/MessageItem.tsx:652 +#: src/components/dms/MessageItem.tsx:621 msgid "Messages from this person are hidden while they are blocking you." msgstr "Messages from this person are hidden while they are blocking you." -#: src/components/dms/MessageItem.tsx:647 +#: src/components/dms/MessageItem.tsx:616 msgid "Messages from this person are hidden while you are blocking them." msgstr "Messages from this person are hidden while you are blocking them." @@ -7229,8 +7229,8 @@ msgstr "" msgid "Navigates to your profile" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:340 -#: src/components/moderation/ReportDialog/index.tsx:356 +#: src/components/moderation/ReportDialog/index.tsx:342 +#: src/components/moderation/ReportDialog/index.tsx:358 msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?" msgstr "" @@ -7452,7 +7452,7 @@ msgstr "No GIFs found for \"{query}\"." msgid "No GIFs to show right now. Try again in a moment." msgstr "No GIFs to show right now. Try again in a moment." -#: src/features/liveNow/components/LinkPreview.tsx:63 +#: src/features/liveNow/components/LinkPreview.tsx:64 msgid "No image" msgstr "" @@ -7640,7 +7640,7 @@ msgstr "Not followed by anyone you’re following" msgid "Not Found" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:130 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:131 msgid "Not ready to hit post? Keep your best ideas in Drafts until the timing is just right." msgstr "" @@ -7724,7 +7724,7 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 #: src/components/dms/InitiateChatFlow.tsx:677 -#: src/components/dms/MessageItem.tsx:659 +#: src/components/dms/MessageItem.tsx:628 #: src/screens/Login/PasswordUpdatedForm.tsx:37 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:661 msgid "Okay" @@ -7916,8 +7916,8 @@ msgstr "" msgid "Open post options menu" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:218 -#: src/features/liveNow/components/LiveStatusDialog.tsx:228 +#: src/features/liveNow/components/LiveStatusDialog.tsx:219 +#: src/features/liveNow/components/LiveStatusDialog.tsx:229 msgid "Open profile" msgstr "" @@ -8005,7 +8005,7 @@ msgstr "" msgid "Opens flow to sign in to your existing Bluesky account" msgstr "" -#: src/components/images/Gallery/index.tsx:437 +#: src/components/images/Gallery/index.tsx:444 msgid "Opens full image" msgstr "Opens full image" @@ -8022,7 +8022,7 @@ msgstr "" msgid "Opens link {0}" msgstr "" -#: src/view/com/util/UserAvatar.tsx:599 +#: src/view/com/util/UserAvatar.tsx:600 msgid "Opens live status dialog" msgstr "" @@ -8053,7 +8053,7 @@ msgstr "" #: src/components/dms/MessageItem.tsx:229 #: src/view/com/notifications/NotificationFeedItem.tsx:1019 -#: src/view/com/util/UserAvatar.tsx:617 +#: src/view/com/util/UserAvatar.tsx:618 msgid "Opens this profile" msgstr "" @@ -8129,8 +8129,8 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:86 #: src/components/dms/AfterReportConversationDialog.tsx:213 -#: src/components/dms/AfterReportDialog.tsx:84 -#: src/components/dms/AfterReportDialog.tsx:176 +#: src/components/dms/AfterReportDialog.tsx:89 +#: src/components/dms/AfterReportDialog.tsx:181 msgid "Our moderation team has received your report." msgstr "" @@ -8661,7 +8661,7 @@ msgstr "" msgid "Press to view followers of this account that you also follow" msgstr "" -#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:120 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:121 msgid "Preview" msgstr "" @@ -8865,8 +8865,8 @@ msgstr "Re-enable link" msgid "React with {emoji}" msgstr "" -#: src/components/dms/ReactionsDialog.tsx:65 -#: src/components/dms/ReactionsDialog.tsx:90 +#: src/components/dms/ReactionsDialog.tsx:66 +#: src/components/dms/ReactionsDialog.tsx:94 msgid "Reactions" msgstr "Reactions" @@ -8884,8 +8884,8 @@ msgctxt "english-only-resource" msgid "read about how to use search filters" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:160 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:171 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:162 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:173 msgid "Read blog post" msgstr "" @@ -9020,13 +9020,13 @@ msgstr "" msgid "Remove attachment" msgstr "" -#: src/view/com/util/UserAvatar.tsx:519 -#: src/view/com/util/UserAvatar.tsx:522 +#: src/view/com/util/UserAvatar.tsx:520 +#: src/view/com/util/UserAvatar.tsx:523 msgid "Remove Avatar" msgstr "" -#: src/view/com/util/UserBanner.tsx:189 -#: src/view/com/util/UserBanner.tsx:192 +#: src/view/com/util/UserBanner.tsx:190 +#: src/view/com/util/UserBanner.tsx:193 msgid "Remove Banner" msgstr "" @@ -9261,8 +9261,8 @@ msgstr "" msgid "Reply was successfully hidden" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:193 -#: src/features/liveNow/components/LiveStatusDialog.tsx:266 +#: src/components/dms/MessageContextMenu.tsx:176 +#: src/features/liveNow/components/LiveStatusDialog.tsx:267 #: src/screens/Messages/ConversationSettings/index.tsx:517 msgid "Report" msgstr "" @@ -9279,8 +9279,8 @@ msgstr "" msgid "Report conversation" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:96 -#: src/components/moderation/ReportDialog/index.tsx:261 +#: src/components/moderation/ReportDialog/index.tsx:98 +#: src/components/moderation/ReportDialog/index.tsx:263 msgid "Report dialog" msgstr "" @@ -9294,7 +9294,7 @@ msgstr "" msgid "Report list" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:190 +#: src/components/dms/MessageContextMenu.tsx:173 msgid "Report message" msgstr "" @@ -9314,8 +9314,8 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:83 #: src/components/dms/AfterReportConversationDialog.tsx:210 -#: src/components/dms/AfterReportDialog.tsx:81 -#: src/components/dms/AfterReportDialog.tsx:173 +#: src/components/dms/AfterReportDialog.tsx:86 +#: src/components/dms/AfterReportDialog.tsx:178 msgid "Report submitted" msgstr "" @@ -9337,7 +9337,7 @@ msgid "Report this list" msgstr "" #: src/components/moderation/ReportDialog/copy.ts:19 -#: src/features/liveNow/components/LiveStatusDialog.tsx:249 +#: src/features/liveNow/components/LiveStatusDialog.tsx:250 msgid "Report this livestream" msgstr "" @@ -9530,7 +9530,7 @@ msgstr "" #: src/components/contacts/screens/VerifyNumber.tsx:355 #: src/components/Error.tsx:65 #: src/components/Lists.tsx:115 -#: src/components/moderation/ReportDialog/index.tsx:295 +#: src/components/moderation/ReportDialog/index.tsx:297 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:59 #: src/components/StarterPack/ProfileStarterPacks.tsx:377 @@ -9553,7 +9553,7 @@ msgstr "" msgid "Retry" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:292 +#: src/components/moderation/ReportDialog/index.tsx:294 #: src/view/screens/Storybook/Admonitions.tsx:61 msgid "Retry loading report options" msgstr "" @@ -9638,7 +9638,7 @@ msgid "Save draft?" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:99 -#: src/components/MediaPreview.tsx:196 +#: src/components/MediaPreview.tsx:197 #: src/components/Post/Embed/ImageContextMenu.tsx:70 #: src/components/StarterPack/ShareDialog.tsx:144 #: src/components/StarterPack/ShareDialog.tsx:150 @@ -9674,7 +9674,7 @@ msgstr "" msgid "Saved Feeds" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:144 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:145 #: src/Navigation.tsx:555 #: src/screens/Bookmarks/index.tsx:59 msgid "Saved Posts" @@ -9899,7 +9899,7 @@ msgstr "" msgid "Select a color" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:378 +#: src/components/moderation/ReportDialog/index.tsx:380 msgid "Select a reason" msgstr "" @@ -9990,7 +9990,7 @@ msgstr "" msgid "Select languages" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:428 +#: src/components/moderation/ReportDialog/index.tsx:430 msgid "Select moderation service" msgstr "" @@ -10092,7 +10092,7 @@ msgstr "" msgid "Send post to..." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:816 +#: src/components/moderation/ReportDialog/index.tsx:818 msgid "Send report to {title}" msgstr "" @@ -10114,7 +10114,7 @@ msgid "Send via direct message" msgstr "" #. placeholder {0}: i18n.date(new Date(message.sentAt), { timeStyle: 'short', }) -#: src/components/dms/MessageContextMenu.tsx:154 +#: src/components/dms/MessageContextMenu.tsx:137 msgid "Sent at {0}" msgstr "Sent at {0}" @@ -10219,7 +10219,7 @@ msgstr "" msgid "Sexually Suggestive" msgstr "" -#: src/components/MediaPreview.tsx:202 +#: src/components/MediaPreview.tsx:203 #: src/components/Post/Embed/ImageContextMenu.tsx:74 #: src/components/StarterPack/QrCodeDialog.tsx:195 #: src/screens/Hashtag.tsx:130 @@ -10309,16 +10309,16 @@ msgstr "" #: src/components/moderation/ScreenHider.tsx:179 #: src/components/moderation/ScreenHider.tsx:182 -#: src/features/liveNow/components/LiveStatusDialog.tsx:317 -#: src/features/liveNow/components/LiveStatusDialog.tsx:321 +#: src/features/liveNow/components/LiveStatusDialog.tsx:318 +#: src/features/liveNow/components/LiveStatusDialog.tsx:322 #: src/screens/List/ListHiddenScreen.tsx:194 #: src/screens/VideoFeed/index.tsx:646 #: src/screens/VideoFeed/index.tsx:652 msgid "Show anyway" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:181 -#: src/screens/Settings/AutomationLabelSettings.tsx:194 +#: src/screens/Settings/AutomationLabelSettings.tsx:185 +#: src/screens/Settings/AutomationLabelSettings.tsx:198 msgid "Show automation label" msgstr "Show automation label" @@ -10412,7 +10412,7 @@ msgid "Show warning and filter from feeds" msgstr "" #: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:60 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:170 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:171 msgid "Show when you’re live" msgstr "" @@ -10557,7 +10557,7 @@ msgstr "" msgid "Skip to next step" msgstr "" -#: src/components/images/Gallery/index.tsx:420 +#: src/components/images/Gallery/index.tsx:427 msgid "slide" msgstr "slide" @@ -10614,7 +10614,7 @@ msgid "Someone left the group" msgstr "Someone left the group" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:294 +#: src/components/dms/MessageItem.tsx:270 msgid "Someone reacted {0}" msgstr "" @@ -10639,7 +10639,7 @@ msgstr "Someone was removed" msgid "Someone was removed from the group" msgstr "Someone was removed from the group" -#: src/components/moderation/ReportDialog/index.tsx:101 +#: src/components/moderation/ReportDialog/index.tsx:103 msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" @@ -10650,7 +10650,7 @@ msgid "Something went wrong" msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:139 -#: src/components/moderation/ReportDialog/index.tsx:287 +#: src/components/moderation/ReportDialog/index.tsx:289 #: src/screens/Deactivated.tsx:86 #: src/screens/Settings/components/DeactivateAccountDialog.tsx:59 #: src/view/screens/Storybook/Admonitions.tsx:56 @@ -10670,7 +10670,7 @@ msgstr "" msgid "Something went wrong. Please try again in a moment." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:245 +#: src/components/moderation/ReportDialog/index.tsx:247 msgid "Something went wrong. Please try again." msgstr "" @@ -10812,7 +10812,7 @@ msgstr "" msgid "Storybook" msgstr "" -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:181 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:182 msgid "Streaming on Twitch? Set your live status on Bluesky to add a badge to your avatar. Tapping it takes people straight to your stream." msgstr "" @@ -10837,9 +10837,9 @@ msgstr "" msgid "Submit Appeal" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:506 -#: src/components/moderation/ReportDialog/index.tsx:567 -#: src/components/moderation/ReportDialog/index.tsx:574 +#: src/components/moderation/ReportDialog/index.tsx:508 +#: src/components/moderation/ReportDialog/index.tsx:569 +#: src/components/moderation/ReportDialog/index.tsx:576 msgid "Submit report" msgstr "" @@ -10848,13 +10848,13 @@ msgid "Subscribe" msgstr "" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:425 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:434 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:426 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:435 msgid "Subscribe on {0}" msgstr "Subscribe on {0}" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:433 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:434 msgid "Subscribe to {publicationTitle} on {0}" msgstr "Subscribe to {publicationTitle} on {0}" @@ -10985,7 +10985,7 @@ msgstr "" msgid "Tap below to allow Bluesky to access your GPS location. We will then use that data to more accurately determine the content and features available in your region." msgstr "" -#: src/components/dms/MessageItem.tsx:598 +#: src/components/dms/MessageItem.tsx:567 msgid "Tap for details" msgstr "Tap for details" @@ -11015,29 +11015,29 @@ msgstr "" msgid "Tap to dismiss" msgstr "" -#: src/components/dms/ReactionsDialog.tsx:192 +#: src/components/dms/ReactionsDialog.tsx:196 msgid "Tap to remove" msgstr "Tap to remove" #. placeholder {0}: reaction.value -#: src/components/dms/ReactionsDialog.tsx:208 +#: src/components/dms/ReactionsDialog.tsx:212 msgid "Tap to remove your {0} reaction" msgstr "Tap to remove your {0} reaction" -#: src/components/dms/MessageItem.tsx:563 +#: src/components/dms/MessageItem.tsx:532 msgid "Tap to retry" msgstr "Tap to retry" #. placeholder {0}: tab.value -#: src/components/dms/ReactionsDialog.tsx:354 +#: src/components/dms/ReactionsDialog.tsx:358 msgid "Tap to show {0} reactions" msgstr "Tap to show {0} reactions" -#: src/components/dms/ReactionsDialog.tsx:353 +#: src/components/dms/ReactionsDialog.tsx:357 msgid "Tap to show all reactions" msgstr "Tap to show all reactions" -#: src/components/dms/MessageItem.tsx:317 +#: src/components/dms/MessageItem.tsx:293 msgid "Tap to view reactions" msgstr "Tap to view reactions" @@ -11427,7 +11427,7 @@ msgstr "" msgid "This author has chosen to make their posts visible only to people who are signed in." msgstr "" -#: src/screens/Profile/components/GermButton.tsx:243 +#: src/screens/Profile/components/GermButton.tsx:244 msgid "This button lets others open the Germ DM app to send you a message. You can manage its visibility from the Germ DM app, or you can disconnect your Bluesky account from Germ DM altogether by clicking the button below." msgstr "" @@ -11561,7 +11561,7 @@ msgstr "" msgid "This is not a valid link" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:169 +#: src/screens/Settings/AutomationLabelSettings.tsx:173 msgid "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." msgstr "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." @@ -11594,13 +11594,13 @@ msgstr "" msgid "This list is empty." msgstr "" -#: src/components/dms/MessageItem.tsx:596 -#: src/components/dms/MessageItem.tsx:625 +#: src/components/dms/MessageItem.tsx:565 +#: src/components/dms/MessageItem.tsx:594 msgid "This message is hidden because this user is blocking you." msgstr "This message is hidden because this user is blocking you." -#: src/components/dms/MessageItem.tsx:595 -#: src/components/dms/MessageItem.tsx:621 +#: src/components/dms/MessageItem.tsx:564 +#: src/components/dms/MessageItem.tsx:590 msgid "This message is hidden because you are blocking this user." msgstr "This message is hidden because you are blocking this user." @@ -11835,8 +11835,8 @@ msgstr "" msgid "Topic" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:162 -#: src/components/dms/MessageContextMenu.tsx:165 +#: src/components/dms/MessageContextMenu.tsx:145 +#: src/components/dms/MessageContextMenu.tsx:148 #: src/components/Post/Translated/index.tsx:150 #: src/components/Post/Translated/index.tsx:157 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:553 @@ -11887,7 +11887,7 @@ msgstr "" msgid "Trolling" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:140 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:142 msgid "Trust emerges from relationships, communities, and shared context, so we’re also enabling <0>trusted verifiers: organizations that can directly issue verification." msgstr "" @@ -11980,7 +11980,7 @@ msgstr "" msgid "Unavailable feed information" msgstr "" -#: src/components/dms/MessageItem.tsx:663 +#: src/components/dms/MessageItem.tsx:632 #: src/components/dms/MessagesListBlockedFooter.tsx:97 #: src/components/dms/MessagesListBlockedFooter.tsx:104 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:222 @@ -12026,8 +12026,8 @@ msgstr "" #: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:79 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:40 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:46 -#: src/screens/Profile/components/GermButton.tsx:185 #: src/screens/Profile/components/GermButton.tsx:186 +#: src/screens/Profile/components/GermButton.tsx:187 msgid "Undo" msgstr "" @@ -12061,7 +12061,7 @@ msgstr "" msgid "Unfollows the user" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:490 +#: src/components/moderation/ReportDialog/index.tsx:492 msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" @@ -12287,22 +12287,22 @@ msgstr "" msgid "Upload a text file to:" msgstr "" -#: src/view/com/util/UserAvatar.tsx:490 -#: src/view/com/util/UserAvatar.tsx:493 -#: src/view/com/util/UserBanner.tsx:160 -#: src/view/com/util/UserBanner.tsx:163 +#: src/view/com/util/UserAvatar.tsx:491 +#: src/view/com/util/UserAvatar.tsx:494 +#: src/view/com/util/UserBanner.tsx:161 +#: src/view/com/util/UserBanner.tsx:164 msgid "Upload from Camera" msgstr "" -#: src/view/com/util/UserAvatar.tsx:507 -#: src/view/com/util/UserBanner.tsx:177 +#: src/view/com/util/UserAvatar.tsx:508 +#: src/view/com/util/UserBanner.tsx:178 msgid "Upload from Files" msgstr "" -#: src/view/com/util/UserAvatar.tsx:501 -#: src/view/com/util/UserAvatar.tsx:505 -#: src/view/com/util/UserBanner.tsx:171 -#: src/view/com/util/UserBanner.tsx:175 +#: src/view/com/util/UserAvatar.tsx:502 +#: src/view/com/util/UserAvatar.tsx:506 +#: src/view/com/util/UserBanner.tsx:172 +#: src/view/com/util/UserBanner.tsx:176 msgid "Upload from Library" msgstr "" @@ -12363,7 +12363,7 @@ msgid "user" msgstr "user" #: src/components/dms/AfterReportConversationDialog.tsx:187 -#: src/components/dms/AfterReportDialog.tsx:150 +#: src/components/dms/AfterReportDialog.tsx:155 msgctxt "toast" msgid "User blocked" msgstr "" @@ -12631,8 +12631,8 @@ msgid "View" msgstr "" #. placeholder {0}: view.source.title -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:319 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:588 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 msgid "View {0}" msgstr "View {0}" @@ -12661,7 +12661,7 @@ msgstr "View {0}’s profile" msgid "View {displayName}’s profile" msgstr "View {displayName}’s profile" -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:436 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:437 msgid "View {publicationTitle}" msgstr "View {publicationTitle}" @@ -12728,10 +12728,10 @@ msgstr "" msgid "View profile banner" msgstr "View profile banner" -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:319 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:426 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:437 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:588 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:427 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:438 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 msgid "View publication" msgstr "View publication" @@ -12785,8 +12785,8 @@ msgstr "" msgid "View your verifications" msgstr "" -#: src/components/images/AutoSizedImage.tsx:220 -#: src/components/images/AutoSizedImage.tsx:252 +#: src/components/images/AutoSizedImage.tsx:221 +#: src/components/images/AutoSizedImage.tsx:253 msgid "Views full image" msgstr "" @@ -12829,8 +12829,8 @@ msgstr "" msgid "Warn content and filter from feeds" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:189 -#: src/features/liveNow/components/LiveStatusDialog.tsx:198 +#: src/features/liveNow/components/LiveStatusDialog.tsx:190 +#: src/features/liveNow/components/LiveStatusDialog.tsx:199 msgid "Watch now" msgstr "" @@ -12977,7 +12977,7 @@ msgstr "" msgid "We’re having network issues, try again" msgstr "We’re having network issues, try again" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:96 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:97 msgid "We’re introducing a new layer of verification on Bluesky — an easy-to-see checkmark." msgstr "" @@ -13063,7 +13063,7 @@ msgstr "" msgid "What's up?" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:148 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:150 msgid "When you tap on a check, you’ll see which organizations have granted verification." msgstr "" @@ -13080,7 +13080,7 @@ msgstr "Who can join this group chat and how" msgid "Who can reply" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:129 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:131 msgid "Who can verify?" msgstr "" @@ -13132,7 +13132,7 @@ msgstr "" msgid "Why should this user be reviewed?" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:46 +#: src/components/dms/AfterReportDialog.tsx:51 msgid "Would you like to block this user and/or delete this conversation?" msgstr "" @@ -13224,7 +13224,7 @@ msgid "You are accessing Bluesky from a region that legally requires us to verif msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:636 +#: src/components/dms/MessageItem.tsx:605 msgid "You are blocking {0}" msgstr "You are blocking {0}" @@ -13316,7 +13316,7 @@ msgstr "" msgid "You can continue ongoing conversations regardless of which setting you choose." msgstr "" -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:149 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:150 msgid "You can now choose to be notified when specific people post. If there’s someone you want timely updates from, go to their profile and find the new bell icon near the follow button." msgstr "" @@ -13552,7 +13552,7 @@ msgid "You probably want to restart the app now." msgstr "" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:287 +#: src/components/dms/MessageItem.tsx:263 msgid "You reacted {0}" msgstr "" @@ -13841,7 +13841,7 @@ msgstr "" msgid "Your preferred language" msgstr "" -#: src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx:100 +#: src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx:102 #: src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx:53 msgid "Your profile picture" msgstr "" @@ -13859,7 +13859,7 @@ msgid "Your reply was sent" msgstr "" #. placeholder {0}: state.selectedLabeler?.creator.displayName -#: src/components/moderation/ReportDialog/index.tsx:517 +#: src/components/moderation/ReportDialog/index.tsx:519 msgid "Your report will be sent to <0>{0}." msgstr "" From a19d652a39b4e483a30a409c37a7d74b6552f7c6 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:50:56 +0100 Subject: [PATCH 03/16] Fix invalid `zero` plural category and add plural formatting (#10722) --- src/screens/Messages/JoinRequests.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/screens/Messages/JoinRequests.tsx b/src/screens/Messages/JoinRequests.tsx index 80d31299dc..98952aee34 100644 --- a/src/screens/Messages/JoinRequests.tsx +++ b/src/screens/Messages/JoinRequests.tsx @@ -411,7 +411,6 @@ function Header({ count?: number hasMoreRequests?: boolean }) { - const {t: l} = useLingui() return ( @@ -420,15 +419,15 @@ function Header({ {count === undefined ? ( Requests to join ) : hasMoreRequests ? ( - l({ - message: `${count}+ requests to join`, - comment: - 'Displayed when there are more requests to join a group chat than have been loaded', - }) + ) : ( From 9f200aab05e8f71f8e4fb74f59a9b400e89f8964 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:51:51 +0100 Subject: [PATCH 04/16] Use correct `Plural` macro in InviteLinkDialog (#10723) --- src/screens/Messages/components/InviteLinkDialog.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/screens/Messages/components/InviteLinkDialog.tsx b/src/screens/Messages/components/InviteLinkDialog.tsx index ad6d4a08e5..6e898a0a9c 100644 --- a/src/screens/Messages/components/InviteLinkDialog.tsx +++ b/src/screens/Messages/components/InviteLinkDialog.tsx @@ -5,8 +5,7 @@ import { moderateProfile, type ModerationOpts, } from '@atproto/api' -import {plural} from '@lingui/core/macro' -import {Trans, useLingui} from '@lingui/react/macro' +import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -178,11 +177,7 @@ export function InviteLinkDialog({ Group chats can only have a maximum of{' '} - {plural(convo.details.memberLimit, { - one: '# person', - other: '# people', - })} - . + . From 49f8e5031f42762805b1ab392c68e7bf75812e9d Mon Sep 17 00:00:00 2001 From: heikadog <41552394+heikadog@users.noreply.github.com> Date: Thu, 4 Jun 2026 06:28:40 -0400 Subject: [PATCH 05/16] Fix labeller highlight when selected on bottom bar (#10716) --- src/view/shell/bottom-bar/BottomBar.tsx | 5 +++-- src/view/shell/bottom-bar/BottomBarStyles.tsx | 9 +++++++++ src/view/shell/bottom-bar/BottomBarWeb.tsx | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index e6a1eeaad5..605542749e 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -144,6 +144,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { const [demoMode] = useDemoMode() const {isActive: live} = useActorStatus(profile) + const isLabeler = profile?.associated?.labeler return ( <> @@ -276,9 +277,9 @@ export function BottomBar({navigation}: BottomTabBarProps) { { closeAllActiveElements() @@ -186,9 +187,13 @@ export function BottomBarWeb() { From c79b5bfa08d6ddeb909dab0c0b392e5123d9c5bb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 4 Jun 2026 09:29:36 -0500 Subject: [PATCH 06/16] Disable overscroll on Android to fix settle discrepancy (#10717) --- src/components/images/Gallery/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 9ae677c2d2..fd18cb8a0b 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -40,7 +40,7 @@ import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' -import {IS_WEB} from '#/env' +import {IS_ANDROID, IS_WEB} from '#/env' export * from './const' export * from './maybeApplyGalleryOffsetStyles' @@ -264,6 +264,9 @@ export function Gallery({ aria-label={l`Image gallery, ${images.length} images`} horizontal pagingEnabled={false} + // Disable Android's stretch overscroll, which can leave the carousel + // settled just off the left edge instead of aligned to x = 0 + overScrollMode={IS_ANDROID ? 'never' : 'auto'} showsHorizontalScrollIndicator={false} directionalLockEnabled nestedScrollEnabled From 08cd4e58b023e1ffe2b3681f4f8c690a24af84a0 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:11:50 -0700 Subject: [PATCH 07/16] Add group chat join links to supported embed types (#10454) --- __tests__/lib/strings/url-helpers.test.ts | 45 +++ eslint-suppressions.json | 8 - src/components/Post/Embed/ChatInviteEmbed.tsx | 41 +++ .../Post/Embed/JoinRequestEmbed.tsx | 271 ++++++++++++++++++ src/components/Post/Embed/index.tsx | 17 ++ .../intents/GroupChatJoinDialog.tsx | 10 +- src/lib/api/index.ts | 18 +- src/lib/api/resolve.ts | 25 +- src/lib/strings/url-helpers.ts | 5 +- src/state/queries/join-links.ts | 6 +- src/view/com/composer/ExternalEmbed.tsx | 3 + .../com/composer/ExternalEmbedRemoveBtn.tsx | 7 +- 12 files changed, 434 insertions(+), 22 deletions(-) create mode 100644 src/components/Post/Embed/ChatInviteEmbed.tsx create mode 100644 src/components/Post/Embed/JoinRequestEmbed.tsx diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts index 0b1b750281..23ffaa2875 100644 --- a/__tests__/lib/strings/url-helpers.test.ts +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -1,6 +1,7 @@ import {describe, expect, it} from '@jest/globals' import { + getChatInviteCodeFromUrl, isPossiblyAUrl, isTrustedUrl, linkRequiresWarning, @@ -178,3 +179,47 @@ describe('isTrustedUrl', () => { expect(output).toEqual(expected) }) }) + +describe('getChatInviteCodeFromUrl', () => { + type Case = [string, string | undefined] + + const cases: Case[] = [ + ['https://bsky.app/c/abcdefg', 'abcdefg'], + ['https://bsky.app/c/abcdefghij', 'abcdefghij'], + // http is not recognized as a bsky.app url + ['http://bsky.app/c/abcdefg', undefined], + ['https://bsky.app/c/abcdefg?utm=foo', 'abcdefg'], + ['https://bsky.app/c/abcdefg#section', 'abcdefg'], + ['/c/abcdefg', 'abcdefg'], + ['/c/abcdefg?utm=foo', 'abcdefg'], + ['/c/abcdefg#section', 'abcdefg'], + + // too short + ['https://bsky.app/c/abcdef', undefined], + ['/c/abcdef', undefined], + // too long + ['https://bsky.app/c/abcdefghijk', undefined], + ['/c/abcdefghijk', undefined], + // invalid characters + ['https://bsky.app/c/abc-def', undefined], + ['/c/abc def', undefined], + // trailing path + ['https://bsky.app/c/abcdefg/extra', undefined], + ['/c/abcdefg/extra', undefined], + // wrong path + ['https://bsky.app/profile/abcdefg', undefined], + ['https://bsky.app/c', undefined], + // wrong host + ['https://example.com/c/abcdefg', undefined], + // not a url, not a path + ['c/abcdefg', undefined], + ['abcdefg', undefined], + ['', undefined], + // malformed url + ['https://[invalid/c/abcdefg', undefined], + ] + + it.each(cases)('given input %p, returns %p', (input, expected) => { + expect(getChatInviteCodeFromUrl(input)).toEqual(expected) + }) +}) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f2e677dc79..d8555ace5b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -811,14 +811,6 @@ "count": 1 } }, - "src/lib/api/index.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 - }, - "@typescript-eslint/no-unsafe-member-access": { - "count": 3 - } - }, "src/lib/async/retry.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 diff --git a/src/components/Post/Embed/ChatInviteEmbed.tsx b/src/components/Post/Embed/ChatInviteEmbed.tsx new file mode 100644 index 0000000000..744fcfb7fa --- /dev/null +++ b/src/components/Post/Embed/ChatInviteEmbed.tsx @@ -0,0 +1,41 @@ +import {type StyleProp, type ViewStyle} from 'react-native' +import {type AppBskyEmbedExternal} from '@atproto/api' + +import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links' +import {useSession} from '#/state/session' +import {atoms as a} from '#/alf' +import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' +import {JoinRequestEmbed} from '#/components/Post/Embed/JoinRequestEmbed' + +export function ChatInviteEmbed({ + code, + link, + onOpen, + style, +}: { + code: string + link: AppBskyEmbedExternal.ViewExternal + onOpen?: () => void + style?: StyleProp +}) { + const {hasSession} = useSession() + const {data, error, isPending} = useJoinLinkPreviewsQuery({ + codes: [code], + hasSession, + }) + + const preview = data?.joinLinkPreviews[0] + + if (error) { + return + } + + return ( + + ) +} diff --git a/src/components/Post/Embed/JoinRequestEmbed.tsx b/src/components/Post/Embed/JoinRequestEmbed.tsx new file mode 100644 index 0000000000..77a0223bec --- /dev/null +++ b/src/components/Post/Embed/JoinRequestEmbed.tsx @@ -0,0 +1,271 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' +import {type ChatBskyGroupDefs} from '@atproto/api' +import {Plural, Trans, useLingui} from '@lingui/react/macro' +import {useNavigation} from '@react-navigation/native' + +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import {type NavigationProp} from '#/lib/routes/types' +import {sanitizeHandle} from '#/lib/strings/handles' +import {atoms as a, useTheme} from '#/alf' +import {AvatarBubbles} from '#/components/AvatarBubbles' +import { + Button, + type ButtonColor, + ButtonIcon, + ButtonText, +} from '#/components/Button' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' +import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' +import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {useIntentDialogs} from '#/components/intents/IntentDialogs' +import {Loader} from '#/components/Loader' +import {ProfileBadges} from '#/components/ProfileBadges' +import {Text} from '#/components/Typography' + +const JOIN_REQUEST_EMBED_HEIGHT = 152 + +export function JoinRequestEmbed({ + loading = false, + preview, + style, + onOpen, +}: { + loading?: boolean + preview?: ChatBskyGroupDefs.JoinLinkPreviewView + style?: StyleProp + onOpen?: () => void +}) { + const t = useTheme() + + if (loading) { + return ( + + + + ) + } + + if (!preview) { + return ( + + + + Chat invite link no longer available + + + ) + } + + return ( + + ) +} + +function JoinRequestEmbedInner({ + preview, + style, + onOpen, +}: { + preview: ChatBskyGroupDefs.JoinLinkPreviewView + style?: StyleProp + onOpen?: () => void +}) { + const t = useTheme() + const {t: l} = useLingui() + const navigation = useNavigation() + + const {groupChatJoinDialogControl, setGroupChatJoinState} = useIntentDialogs() + + const ownerDisplayName = createSanitizedDisplayName(preview.owner) + const ownerHandle = sanitizeHandle(preview.owner.handle, '@') + + const avatarProfiles = preview.convo?.members ?? [preview.owner] + + const convoId = preview.convo?.id + const isFollowing = preview.owner.viewer?.following ?? false + const hasRequested = !convoId && preview.viewer?.requestedAt != null + + let canJoin = true + let ButtonIconImage = JoinIcon + let buttonText = preview.requireApproval ? l`Request to join` : l`Join` + let buttonColor: ButtonColor = 'primary' + if (preview.enabledStatus !== 'enabled') { + canJoin = false + ButtonIconImage = WarningIcon + buttonText = l`Chat invite link no longer available` + buttonColor = 'secondary' + } else if (preview.memberCount >= preview.memberLimit) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`This chat is full` + buttonColor = 'secondary' + } else if (preview.joinRule === 'followedByOwner' && !isFollowing) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`Only people the chat owner follows can join` + buttonColor = 'secondary' + } else if (hasRequested) { + ButtonIconImage = CheckIcon + buttonText = l`Requested` + buttonColor = 'secondary' + } + + return ( + + + + + + {preview.name} + + + + Group chat + + + + {preview.memberCount}/{preview.memberLimit}{' '} + + + + + + + + By{' '} + + {ownerDisplayName} + + + + + + {ownerHandle} + + + + + {convoId ? ( + + ) : ( + + )} + + ) +} diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index 054558ad83..ab6f5dc439 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -12,6 +12,7 @@ import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' +import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {unstableCacheProfileView} from '#/state/queries/profile' import {useSession} from '#/state/session' @@ -33,6 +34,7 @@ import { type EmbedType, parseEmbed, } from '#/types/bsky/post' +import {ChatInviteEmbed} from './ChatInviteEmbed' import {ExternalEmbed} from './ExternalEmbed' import {ModeratedFeedEmbed} from './FeedEmbed' import {ImageEmbed} from './ImageEmbed' @@ -110,6 +112,21 @@ function MediaEmbed({ ) } + const chatInviteCode = getChatInviteCodeFromUrl(embed.view.external.uri) + if (chatInviteCode) { + return ( + + + + ) + } return ( - Open chat + + Open chat + ) : ( @@ -416,8 +420,8 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { } accessibilityHint={ joinLinkPreview.requireApproval - ? l`Request access to join this group chat` - : l`Join this group chat` + ? l`Tap to request access to join this group chat` + : l`Tap to join this group chat immediately` } size="large" color={buttonColor} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index d9a026ca83..c5f6cc6eb5 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -177,7 +177,8 @@ export async function post( writes: writes, validate: true, }) - } catch (e: any) { + } catch (err) { + const e = err as Error logger.error(`Failed to create post`, { safeMessage: e.message, }) @@ -427,6 +428,16 @@ async function resolveMedia( }, } } + if (resolvedLink.type === 'chat-invite' && resolvedLink.view) { + return { + $type: 'app.bsky.embed.external', + external: { + uri: resolvedLink.uri, + title: resolvedLink.view.name, + description: `${resolvedLink.view.memberCount}/${resolvedLink.view.memberLimit}`, + }, + } + } } return undefined } @@ -470,6 +481,7 @@ async function computeCid(record: AppBskyFeedPost.Record): Promise { } // Returns a transformed version of the object for use in DAG-CBOR. +// eslint-disable-next-line @typescript-eslint/no-explicit-any function prepareForHashing(v: any): any { // IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing, // the API client will convert this for you but we're hashing in the client, @@ -492,9 +504,10 @@ function prepareForHashing(v: any): any { // Walk through plain objects if (isPlainObject(v)) { - const obj: any = {} + const obj: Record = {} let pure = true for (const key in v) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access let value = v[key] // `value` is undefined if (value === undefined) { @@ -513,6 +526,7 @@ function prepareForHashing(v: any): any { return v } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function isPlainObject(v: any): boolean { if (typeof v !== 'object' || v === null) { return false diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index f75cd74db2..d411bd8204 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -2,11 +2,12 @@ import { type AppBskyFeedDefs, type AppBskyGraphDefs, type BskyAgent, + type ChatBskyGroupDefs, type ComAtprotoRepoStrongRef, } from '@atproto/api' import {AtUri} from '@atproto/api' -import {POST_IMG_MAX} from '#/lib/constants' +import {DM_SERVICE_HEADERS, POST_IMG_MAX} from '#/lib/constants' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' import {downloadAndResize} from '#/lib/media/manip' @@ -16,6 +17,7 @@ import { } from '#/lib/strings/starter-pack' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, isBskyCustomFeedUrl, isBskyListUrl, isBskyPostUrl, @@ -71,12 +73,20 @@ type ResolvedStarterPackRecord = { view: AppBskyGraphDefs.StarterPackView } +type ResolvedChatInvite = { + type: 'chat-invite' + uri: string + code: string + view?: ChatBskyGroupDefs.JoinLinkPreviewView +} + export type ResolvedLink = | ResolvedExternalLink | ResolvedPostRecord | ResolvedFeedRecord | ResolvedListRecord | ResolvedStarterPackRecord + | ResolvedChatInvite export class EmbeddingDisabledError extends Error { constructor() { @@ -141,6 +151,19 @@ export async function resolveLink( view: res.data.list, } } + const chatInviteCode = getChatInviteCodeFromUrl(uri) + if (chatInviteCode) { + const res = await agent.chat.bsky.group.getJoinLinkPreviews( + {codes: [chatInviteCode]}, + {headers: DM_SERVICE_HEADERS}, + ) + return { + type: 'chat-invite', + uri, + code: chatInviteCode, + view: res.data.joinLinkPreviews[0], + } + } if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { const parsed = parseStarterPackUri(uri) if (!parsed) { diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index b5c278dacf..fd1a5ef7d9 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,5 +1,5 @@ import {AtUri} from '@atproto/api' -import psl from 'psl' +import {parse} from 'psl' import TLDs from 'tlds' import {BSKY_SERVICE} from '#/lib/constants' @@ -178,6 +178,7 @@ export function isBskyStarterPackUrl(url: string): boolean { return false } +// Invite codes are 7 alphanumeric characters long, supporting up to 10 here to future-proof. export const CHAT_INVITE_CODE_REGEX = /^\/c\/([a-zA-Z0-9]{7,10})$/ export function getChatInviteCodeFromUrl(url: string): string | undefined { @@ -328,7 +329,7 @@ export function isPossiblyAUrl(str: string): boolean { } export function splitApexDomain(hostname: string): [string, string] { - const hostnamep = psl.parse(hostname) + const hostnamep = parse(hostname) if (hostnamep.error || !hostnamep.listed || !hostnamep.domain) { return ['', hostname] } diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index 6aa10331ef..99a37b6b06 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -20,9 +20,11 @@ export const createJoinLinkPreviewQueryKey = (args: { export function useJoinLinkPreviewsQuery({ codes, hasSession, + staleTime = STALE.MINUTES.ONE, }: { codes?: string[] hasSession: boolean + staleTime?: number }) { const agent = useAgent() @@ -45,7 +47,7 @@ export function useJoinLinkPreviewsQuery({ } }, enabled: codes != null && codes.length > 0, - staleTime: STALE.SECONDS.FIFTEEN, + staleTime, }) } @@ -66,7 +68,7 @@ export function usePrefetchJoinLinkPreviews() { : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) return res.data }, - staleTime: STALE.SECONDS.FIFTEEN, + staleTime: STALE.MINUTES.ONE, }) } } diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index ecd22d7c39..5cc87c63f5 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -11,6 +11,7 @@ import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed' +import {JoinRequestEmbed} from '#/components/Post/Embed/JoinRequestEmbed' import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed' import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' @@ -115,6 +116,8 @@ export const ExternalEmbedLink = ({ hideAlt /> ) + } else if (data.type === 'chat-invite') { + return } else if (data.kind === 'feed') { return ( void} & ViewStyleProp) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() return ( - ) : ( - - )} + + ) } diff --git a/src/components/dms/ChatInvite/Card.tsx b/src/components/dms/ChatInvite/Card.tsx new file mode 100644 index 0000000000..518371325e --- /dev/null +++ b/src/components/dms/ChatInvite/Card.tsx @@ -0,0 +1,90 @@ +import {View} from 'react-native' +import {Plural, Trans} from '@lingui/react/macro' + +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import {sanitizeHandle} from '#/lib/strings/handles' +import {atoms as a, useTheme} from '#/alf' +import {AvatarBubbles} from '#/components/AvatarBubbles' +import {ProfileBadges} from '#/components/ProfileBadges' +import {Text} from '#/components/Typography' +import {useChatInvite} from './Context' + +/** + * Presentational preview of a chat invite: member avatars, group name, member + * count, and owner. Reads the preview from `ChatInvite.Root` context. Renders + * nothing if there's no preview (use a fallback alongside it for that case). + */ +export function Card({size}: {size: 'large' | 'small'}) { + const t = useTheme() + const {preview} = useChatInvite() + + if (!preview) return null + + const ownerDisplayName = createSanitizedDisplayName(preview.owner) + const ownerHandle = sanitizeHandle(preview.owner.handle, '@') + const avatarProfiles = preview.convo?.members ?? [preview.owner] + + return ( + + + + + {preview.name} + + + + Group chat + + + + {preview.memberCount}/{preview.memberLimit}{' '} + + + + + + + + By {ownerDisplayName} + + + + + {ownerHandle} + + + + + ) +} diff --git a/src/components/dms/ChatInvite/Context.tsx b/src/components/dms/ChatInvite/Context.tsx new file mode 100644 index 0000000000..53c7a6cc22 --- /dev/null +++ b/src/components/dms/ChatInvite/Context.tsx @@ -0,0 +1,47 @@ +import {createContext, useContext} from 'react' +import {type ChatBskyGroupDefs} from '@atproto/api' + +import {type ButtonColor} from '#/components/Button' +import {type Props as SVGIconProps} from '#/components/icons/common' + +/** + * The derived state of the join/open action for a chat invite, computed once in + * `Root` and consumed by `JoinButton` (or any custom action UI). + */ +export type ChatInviteAction = { + label: string + accessibilityHint: string + icon: React.ComponentType + color: ButtonColor + /** + * Whether the action can be performed. False when the link is disabled, the + * chat is full, or the viewer doesn't meet the join rule. + */ + disabled: boolean + onPress: () => void + side: 'left' | 'right' +} + +export type ChatInviteContextValue = { + code: string + loading: boolean + error: boolean + preview: ChatBskyGroupDefs.JoinLinkPreviewView | undefined + /** + * The derived action descriptor. Undefined while loading or when there's no + * preview to act on. + */ + action: ChatInviteAction | undefined +} + +const ChatInviteContext = createContext(null) + +export function useChatInvite(): ChatInviteContextValue { + const ctx = useContext(ChatInviteContext) + if (!ctx) { + throw new Error('useChatInvite must be used within a ChatInvite.Root') + } + return ctx +} + +export const ChatInviteProvider = ChatInviteContext.Provider diff --git a/src/components/dms/ChatInvite/JoinButton.tsx b/src/components/dms/ChatInvite/JoinButton.tsx new file mode 100644 index 0000000000..0036391b0e --- /dev/null +++ b/src/components/dms/ChatInvite/JoinButton.tsx @@ -0,0 +1,42 @@ +import {type StyleProp, type ViewStyle} from 'react-native' + +import {atoms as a} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useChatInvite} from './Context' + +/** + * The join/open action button for a chat invite. Reads the derived action from + * `ChatInvite.Root` context. Pass `onPress` to intercept (e.g. to close a + * surface before navigating); it runs before the default action. Renders + * nothing while loading or when there's no preview to act on. + */ +export function JoinButton({ + onPress, + style, +}: { + onPress?: () => void + style?: StyleProp +}) { + const {action} = useChatInvite() + + if (!action) return null + + return ( + + ) +} diff --git a/src/components/dms/ChatInvite/Root.tsx b/src/components/dms/ChatInvite/Root.tsx new file mode 100644 index 0000000000..a9f2c54e3a --- /dev/null +++ b/src/components/dms/ChatInvite/Root.tsx @@ -0,0 +1,144 @@ +import {setStringAsync} from 'expo-clipboard' +import {type ChatBskyGroupDefs} from '@atproto/api' +import {useLingui} from '@lingui/react/macro' +import {useNavigation} from '@react-navigation/native' + +import {type NavigationProp} from '#/lib/routes/types' +import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links' +import {useSession} from '#/state/session' +import {type ButtonColor} from '#/components/Button' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' +import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' +import {ChainLink_Stroke2_Corner0_Rounded as LinkIcon} from '#/components/icons/ChainLink' +import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' +import {type Props as SVGIconProps} from '#/components/icons/common' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {useIntentDialogs} from '#/components/intents/IntentDialogs' +import * as Toast from '#/components/Toast' +import {type ChatInviteAction, ChatInviteProvider} from './Context' + +/** + * Headless data + state owner for a chat invite. Fetches the join link preview + * by code and derives the join/open action, exposing both via context for the + * composable parts (`Card`, `JoinButton`) or any custom UI to consume. + * + * Pass `initialPreview` when the preview is already known (e.g. a DM message + * embed already carries the resolved view) to avoid a loading flash. + */ +export function Root({ + code, + initialPreview, + currentConvoId, + children, +}: { + code: string + initialPreview?: ChatBskyGroupDefs.JoinLinkPreviewView + /** + * The convo this invite is being viewed within, if any. When the invite + * links to the same chat, the action becomes "Copy link" instead of + * open/join (you're already here). + */ + currentConvoId?: string + children: React.ReactNode +}) { + const {hasSession} = useSession() + const {t: l} = useLingui() + const navigation = useNavigation() + const {groupChatJoinDialogControl, setGroupChatJoinState} = useIntentDialogs() + + const {data, error, isPending} = useJoinLinkPreviewsQuery({ + codes: [code], + hasSession, + // Seed the cache with the already-resolved preview so we don't refetch. + initialData: initialPreview + ? {joinLinkPreviews: [initialPreview]} + : undefined, + }) + + const preview = data?.joinLinkPreviews[0] + const loading = isPending && !preview + + let action: ChatInviteAction | undefined + if (preview) { + const convoId = preview.convo?.id + const isFollowing = preview.owner.viewer?.following ?? false + const hasRequested = !convoId && preview.viewer?.requestedAt != null + + if (convoId && convoId === currentConvoId) { + // You're already in the chat this invite links to - offer to copy the + // link rather than open/join. + action = { + label: l`Copy link`, + accessibilityHint: l`Tap to copy this invite link`, + icon: LinkIcon, + side: 'left', + color: 'primary', + disabled: false, + onPress: () => { + void setStringAsync(`https://bsky.app/c/${preview.code}`) + Toast.show(l`Copied to clipboard`, {type: 'success'}) + }, + } + } else if (convoId) { + action = { + label: l`Open chat`, + accessibilityHint: l`Tap to open this group chat`, + icon: ArrowRightIcon, + side: 'right', + color: 'primary', + disabled: false, + onPress: () => { + navigation.push('MessagesConversation', {conversation: convoId}) + }, + } + } else { + let canJoin = true + let icon: React.ComponentType = JoinIcon + let label = preview.requireApproval ? l`Request to join` : l`Join` + let color: ButtonColor = 'primary' + if (preview.enabledStatus !== 'enabled') { + canJoin = false + icon = WarningIcon + label = l`Chat invite link no longer available` + color = 'secondary' + } else if (preview.memberCount >= preview.memberLimit) { + canJoin = false + icon = HandIcon + label = l`This chat is full` + color = 'secondary' + } else if (preview.joinRule === 'followedByOwner' && !isFollowing) { + canJoin = false + icon = HandIcon + label = l`Only people the chat owner follows can join` + color = 'secondary' + } else if (hasRequested) { + icon = CheckIcon + label = l`Requested` + color = 'secondary' + } + + action = { + label, + side: 'left', + accessibilityHint: preview.requireApproval + ? l`Tap to request access to join this group chat` + : l`Tap to join this group chat immediately`, + icon, + color, + disabled: !canJoin, + onPress: () => { + setGroupChatJoinState({code: preview.code}) + groupChatJoinDialogControl.open() + }, + } + } + } + + return ( + + {children} + + ) +} diff --git a/src/components/dms/ChatInvite/index.tsx b/src/components/dms/ChatInvite/index.tsx new file mode 100644 index 0000000000..bcea3f7651 --- /dev/null +++ b/src/components/dms/ChatInvite/index.tsx @@ -0,0 +1,8 @@ +export {Card} from './Card' +export { + type ChatInviteAction, + type ChatInviteContextValue, + useChatInvite, +} from './Context' +export {JoinButton} from './JoinButton' +export {Root} from './Root' diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index ac6acb5891..f4f3cdacc3 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -20,6 +20,7 @@ import { AppBskyEmbedRecord, type ChatBskyActorDefs, ChatBskyConvoDefs, + ChatBskyEmbedJoinLink, RichText as RichTextAPI, } from '@atproto/api' import {plural} from '@lingui/core/macro' @@ -49,6 +50,7 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' +import {MessageItemInviteEmbed} from './MessageItemInviteEmbed' import {groupReactions} from './ReactionsDialog' import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util' @@ -185,8 +187,10 @@ let MessageItem = ({ const rt = new RichTextAPI({text: message.text, facets: message.facets}) - const hasEmbedAndText = - AppBskyEmbedRecord.isView(message.embed) && rt.text.length > 0 + const hasEmbed = + AppBskyEmbedRecord.isView(message.embed) || + ChatBskyEmbedJoinLink.isView(message.embed) + const hasEmbedAndText = hasEmbed && rt.text.length > 0 const targetBottomRadius = squaredBottomCorner ? SQUARED_BORDER_RADIUS @@ -427,6 +431,15 @@ let MessageItem = ({ squaredTopCorner={squaredTopCorner} /> )} + {ChatBskyEmbedJoinLink.isView(message.embed) && ( + + )} {rt.text.length > 0 && ( + isFromSelf: boolean + isGroupChat: boolean + squaredTopCorner: boolean + squaredBottomCorner: boolean +}): React.ReactNode => { + const t = useTheme() + const screen = useWindowDimensions() + const convo = useConvoActive() + + return ( + + + + + + + + + + + ) +} +MessageItemInviteEmbed = memo(MessageItemInviteEmbed) +export {MessageItemInviteEmbed} diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index 0aa132e232..c6899fb9d0 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -20,7 +20,7 @@ import {countGraphemes} from 'unicode-segmenter/grapheme' import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {isBskyPostUrl} from '#/lib/strings/url-helpers' +import {isBskyChatInviteUrl, isBskyPostUrl} from '#/lib/strings/url-helpers' import {useEmail} from '#/state/email-verification' import { useMessageDraft, @@ -233,7 +233,11 @@ export function MessageComposer({ }} onChange={handleChange} onFacetCommitted={facet => { - if (facet.type === 'url' && isBskyPostUrl(facet.value)) { + if ( + facet.type === 'url' && + (isBskyPostUrl(facet.value) || + isBskyChatInviteUrl(facet.value)) + ) { setEmbed(facet.value) } }} diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index 5bc2f38a05..a73a6564fb 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -18,6 +18,8 @@ import { } from '#/lib/routes/types' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, + isBskyChatInviteUrl, isBskyPostUrl, makeRecordUri, } from '#/lib/strings/url-helpers' @@ -26,6 +28,7 @@ import {usePostQuery} from '#/state/queries/post' import {PostMeta} from '#/view/com/util/PostMeta' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' +import * as ChatInvite from '#/components/dms/ChatInvite' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Loader} from '#/components/Loader' import * as MediaPreview from '#/components/MediaPreview' @@ -35,35 +38,56 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import * as bsky from '#/types/bsky' +/** + * The embed staged in the message composer. A message can carry at most one + * embed: either a quoted post or a group chat invite link. + */ +export type MessageEmbedState = + | {type: 'post'; uri: string} + | {type: 'invite'; code: string} + export function useMessageEmbed() { const route = useRoute>() const navigation = useNavigation() const embedFromParams = route.params.embed - const [embedUri, setEmbedUri] = useState(embedFromParams) + const [embed, setEmbedState] = useState( + embedFromParams ? {type: 'post', uri: embedFromParams} : undefined, + ) - if (embedFromParams && embedUri !== embedFromParams) { - setEmbedUri(embedFromParams) + if (embedFromParams && embed?.type !== 'post') { + setEmbedState({type: 'post', uri: embedFromParams}) } return { - embedUri, + embed, setEmbed: useCallback( (embedUrl: string | undefined) => { if (!embedUrl) { + // Only the post embed is reflected in the route param (used by the + // share-to-DM intent flow); invites are local-only. navigation.setParams({embed: ''}) - setEmbedUri(undefined) + setEmbedState(undefined) return } if (embedFromParams) return - const url = convertBskyAppUrlIfNeeded(embedUrl) - const [_0, user, _1, rkey] = url.split('/').filter(Boolean) - const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey) + if (isBskyChatInviteUrl(embedUrl)) { + const code = getChatInviteCodeFromUrl(embedUrl) + if (code) { + setEmbedState({type: 'invite', code}) + } + return + } - setEmbedUri(uri) + if (isBskyPostUrl(embedUrl)) { + const url = convertBskyAppUrlIfNeeded(embedUrl) + const [_0, user, _1, rkey] = url.split('/').filter(Boolean) + const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey) + setEmbedState({type: 'post', uri}) + } }, [embedFromParams, navigation], ), @@ -81,7 +105,10 @@ export function useExtractEmbedFromFacets( for (const facet of rt.facets ?? []) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) { + if ( + AppBskyRichtextFacet.isLink(feature) && + (isBskyPostUrl(feature.uri) || isBskyChatInviteUrl(feature.uri)) + ) { uriFromFacet = feature.uri break } @@ -96,16 +123,40 @@ export function useExtractEmbedFromFacets( } export function MessageInputEmbed({ - embedUri, + embed, setEmbed, }: { - embedUri: string | undefined + embed: MessageEmbedState | undefined setEmbed: (embedUrl: string | undefined) => void +}) { + const onRemove = useCallback(() => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + setEmbed(undefined) + }, [setEmbed]) + + if (!embed) { + return null + } + + switch (embed.type) { + case 'post': + return + case 'invite': + return + } +} + +function MessageInputPostEmbed({ + uri, + onRemove, +}: { + uri: string + onRemove: () => void }) { const t = useTheme() const {t: l} = useLingui() - const {data: post, status} = usePostQuery(embedUri) + const {data: post, status} = usePostQuery(uri) const moderationOpts = useModerationOpts() const moderation = useMemo( @@ -134,15 +185,6 @@ export function MessageInputEmbed({ return {rt: undefined, record: undefined} }, [post]) - if (!embedUri) { - return null - } - - const onRemove = () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setEmbed(undefined) - } - switch (status) { case 'pending': { return ( @@ -220,6 +262,71 @@ export function MessageInputEmbed({ } } +function MessageInputInviteEmbed({ + code, + onRemove, +}: { + code: string + onRemove: () => void +}) { + const t = useTheme() + const {t: l} = useLingui() + + return ( + + + + + + + ) +} + +function MessageInputInviteEmbedBody() { + const t = useTheme() + const {loading, preview} = ChatInvite.useChatInvite() + + if (loading) { + return ( + + + + ) + } + + if (!preview) { + return ( + + + Could not load invite + + + ) + } + + return +} + function SimpleContainer({ children, onRemove, diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index fbebdc0745..6f56c6a898 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -28,6 +28,7 @@ import { type AppBskyEmbedRecord, AppBskyRichtextFacet, ChatBskyConvoDefs, + type ChatBskyEmbedJoinLink, RichText, } from '@atproto/api' import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect' @@ -37,6 +38,7 @@ import {ScrollProvider} from '#/lib/ScrollContext' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, isBskyPostUrl, } from '#/lib/strings/url-helpers' import {logger} from '#/logger' @@ -46,9 +48,10 @@ import { useConvoActive, } from '#/state/messages/convo' import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' +import {useGetJoinLinkPreview} from '#/state/queries/join-links' import {useGetPost} from '#/state/queries/post' import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' -import {useAgent} from '#/state/session' +import {useAgent, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageInput} from '#/screens/Messages/components/MessageInput' @@ -131,8 +134,10 @@ export function MessagesList({ const ax = useAnalytics() const convoState = useConvoActive() const agent = useAgent() + const {hasSession} = useSession() const getPost = useGetPost() - const {embedUri, setEmbed} = useMessageEmbed() + const getJoinLinkPreview = useGetJoinLinkPreview() + const {embed: messageEmbed, setEmbed} = useMessageEmbed() const t = useTheme() const textInputId = 'chat-input-' + useId() @@ -348,12 +353,38 @@ export function MessagesList({ // we want to remove the post link from the text, re-trim, then detect facets rt.detectFacetsWithoutResolution() - let embed: $Typed | undefined - let embedView: $Typed | undefined + let embed: + | $Typed + | $Typed + | undefined + let embedView: + | $Typed + | $Typed + | undefined - if (embedUri) { + // Find the embedded link facet and, if it's at the start or end of the + // message, remove it from the text (the embed card replaces it). + const stripLinkFacet = (predicate: (uri: string) => boolean) => { + const linkFacet = rt.facets?.find(facet => + facet.features.find( + feature => + AppBskyRichtextFacet.isLink(feature) && predicate(feature.uri), + ), + ) + if (linkFacet) { + const isAtStart = linkFacet.index.byteStart === 0 + const isAtEnd = + linkFacet.index.byteEnd === rt.unicodeText.graphemeLength + if (isAtStart || isAtEnd) { + rt.delete(linkFacet.index.byteStart, linkFacet.index.byteEnd) + } + rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true}) + } + } + + if (messageEmbed?.type === 'post') { try { - const post = await getPost({uri: embedUri}) + const post = await getPost({uri: messageEmbed.uri}) if (post) { embed = { $type: 'app.bsky.embed.record', @@ -368,42 +399,34 @@ export function MessagesList({ record: createEmbedViewRecordFromPost(post), } - // look for the embed uri in the facets, so we can remove it from the text - const postLinkFacet = rt.facets?.find(facet => { - return facet.features.find(feature => { - if (AppBskyRichtextFacet.isLink(feature)) { - if (isBskyPostUrl(feature.uri)) { - const url = convertBskyAppUrlIfNeeded(feature.uri) - const [_0, _1, _2, rkey] = url.split('/').filter(Boolean) - - // this might have a handle instead of a DID - // so just compare the rkey - not particularly dangerous - return post.uri.endsWith(rkey) - } - } - return false - }) + stripLinkFacet(uri => { + if (!isBskyPostUrl(uri)) return false + const url = convertBskyAppUrlIfNeeded(uri) + const [_0, _1, _2, rkey] = url.split('/').filter(Boolean) + // this might have a handle instead of a DID + // so just compare the rkey - not particularly dangerous + return post.uri.endsWith(rkey) }) - - if (postLinkFacet) { - const isAtStart = postLinkFacet.index.byteStart === 0 - const isAtEnd = - postLinkFacet.index.byteEnd === rt.unicodeText.graphemeLength - - // remove the post link from the text - if (isAtStart || isAtEnd) { - rt.delete( - postLinkFacet.index.byteStart, - postLinkFacet.index.byteEnd, - ) - } - - rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true}) - } } } catch (error) { logger.error('Failed to get post as quote for DM', {error}) } + } else if (messageEmbed?.type === 'invite') { + const code = messageEmbed.code + embed = { + $type: 'chat.bsky.embed.joinLink', + code, + } + + const joinLinkPreview = await getJoinLinkPreview({code, hasSession}) + if (joinLinkPreview) { + embedView = { + $type: 'chat.bsky.embed.joinLink#view', + joinLinkPreview, + } + } + + stripLinkFacet(uri => getChatInviteCodeFromUrl(uri) === code) } await rt.detectFacets(agent) @@ -424,7 +447,16 @@ export function MessagesList({ embedView, ) }, - [agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled], + [ + agent, + convoState, + messageEmbed, + getPost, + getJoinLinkPreview, + hasSession, + hasScrolled, + setHasScrolled, + ], ) const scrollToEndOnPress = useCallback(() => { @@ -595,11 +627,11 @@ export function MessagesList({ onSendMessage={(message: string) => void onSendMessage(message) } - hasEmbed={!!embedUri} + hasEmbed={!!messageEmbed} setEmbed={setEmbed} loading={loading}> @@ -607,11 +639,11 @@ export function MessagesList({ diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 08b609f71a..e89965eb45 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -6,6 +6,7 @@ import { ChatBskyConvoDefs, type ChatBskyConvoGetLog, type ChatBskyConvoSendMessage, + type ChatBskyEmbedJoinLink, type ChatBskyGroupDefs, } from '@atproto/api' import {XRPCError} from '@atproto/api' @@ -109,7 +110,9 @@ export class Convo { { id: string message: ChatBskyConvoSendMessage.InputSchema['message'] - optimisticEmbedView?: $Typed + optimisticEmbedView?: + | $Typed + | $Typed } > = new Map() private deletedMessages: Set = new Set() @@ -942,7 +945,9 @@ export class Convo { sendMessage( message: ChatBskyConvoSendMessage.InputSchema['message'], - optimisticEmbedView?: $Typed, + optimisticEmbedView?: + | $Typed + | $Typed, ) { // Ignore empty messages for now since they have no other purpose atm if (!message.text.trim() && !message.embed) return diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index 51d111356a..269f32c515 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -5,6 +5,7 @@ import { type ChatBskyActorDefs, type ChatBskyConvoDefs, type ChatBskyConvoSendMessage, + type ChatBskyEmbedJoinLink, } from '@atproto/api' import {type MessagesEventBus} from '#/state/messages/events/agent' @@ -108,7 +109,10 @@ export type ConvoItem = type DeleteMessage = (messageId: string) => Promise type SendMessage = ( message: ChatBskyConvoSendMessage.InputSchema['message'], - optimisticEmbedView: $Typed | undefined, + optimisticEmbedView: + | $Typed + | $Typed + | undefined, ) => void type FetchMessageHistory = () => Promise type MarkConvoAccepted = () => void diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index 99a37b6b06..df87ec0a24 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -1,4 +1,9 @@ -import {AtpAgent} from '@atproto/api' +import {useCallback} from 'react' +import { + AtpAgent, + type ChatBskyGroupDefs, + type ChatBskyGroupGetJoinLinkPreviews, +} from '@atproto/api' import {useQuery, useQueryClient} from '@tanstack/react-query' import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants' @@ -17,14 +22,39 @@ export const createJoinLinkPreviewQueryKey = (args: { persistedVersion: 1, }) +async function fetchJoinLinkPreviews({ + agent, + codes, + hasSession, +}: { + agent: AtpAgent + codes: string[] + hasSession: boolean +}) { + const previewAgent = new AtpAgent({service: CHAT_SERVICE}) + const res = hasSession + ? await agent.chat.bsky.group.getJoinLinkPreviews( + {codes}, + {headers: DM_SERVICE_HEADERS}, + ) + : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) + return res.data +} + export function useJoinLinkPreviewsQuery({ codes, hasSession, staleTime = STALE.MINUTES.ONE, + initialData, }: { codes?: string[] hasSession: boolean staleTime?: number + /** + * Seed the query with an already-known preview (e.g. a DM message embed + * already carries the resolved view), avoiding a duplicate fetch. + */ + initialData?: ChatBskyGroupGetJoinLinkPreviews.OutputSchema }) { const agent = useAgent() @@ -33,14 +63,7 @@ export function useJoinLinkPreviewsQuery({ queryFn: async () => { if (!codes) throw new Error('No invite code') try { - const previewAgent = new AtpAgent({service: CHAT_SERVICE}) - const res = hasSession - ? await agent.chat.bsky.group.getJoinLinkPreviews( - {codes}, - {headers: DM_SERVICE_HEADERS}, - ) - : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) - return res.data + return await fetchJoinLinkPreviews({agent, codes, hasSession}) } catch (error) { logger.error('Failed to fetch join link preview', {safeMessage: error}) throw error @@ -48,6 +71,7 @@ export function useJoinLinkPreviewsQuery({ }, enabled: codes != null && codes.length > 0, staleTime, + initialData, }) } @@ -58,17 +82,42 @@ export function usePrefetchJoinLinkPreviews() { return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => { return queryClient.prefetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}), - queryFn: async () => { - const previewAgent = new AtpAgent({service: CHAT_SERVICE}) - const res = hasSession - ? await agent.chat.bsky.group.getJoinLinkPreviews( - {codes}, - {headers: DM_SERVICE_HEADERS}, - ) - : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) - return res.data - }, + queryFn: () => fetchJoinLinkPreviews({agent, codes, hasSession}), staleTime: STALE.MINUTES.ONE, }) } } + +/** + * Imperatively fetch (or read from cache) a single join link preview by code. + * Used when sending a DM invite embed so we can build an optimistic view. + * Returns undefined if the preview can't be resolved. + */ +export function useGetJoinLinkPreview() { + const agent = useAgent() + const queryClient = useQueryClient() + + return useCallback( + async ({ + code, + hasSession, + }: { + code: string + hasSession: boolean + }): Promise => { + try { + const data = await queryClient.fetchQuery({ + queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}), + queryFn: () => + fetchJoinLinkPreviews({agent, codes: [code], hasSession}), + staleTime: STALE.MINUTES.ONE, + }) + return data.joinLinkPreviews[0] + } catch (error) { + logger.error('Failed to fetch join link preview', {safeMessage: error}) + return undefined + } + }, + [agent, queryClient], + ) +} diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 5cc87c63f5..0b14f16ed3 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -117,7 +117,7 @@ export const ExternalEmbedLink = ({ /> ) } else if (data.type === 'chat-invite') { - return + return } else if (data.kind === 'feed') { return ( Date: Thu, 4 Jun 2026 15:56:37 -0500 Subject: [PATCH 16/16] V smol log cleanups (#10731) --- eslint-suppressions.json | 3 -- src/analytics/PassiveAnalytics.tsx | 28 +++++++++---------- src/analytics/metrics/client.ts | 4 --- .../Post/Embed/StandardSiteEmbed/index.tsx | 4 ++- .../additional-moderation-authorities.ts | 8 ------ src/view/shell/desktop/LeftNav.tsx | 1 + 6 files changed, 17 insertions(+), 31 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 483ed83e7d..b8e65eb265 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -43,9 +43,6 @@ } }, "src/analytics/PassiveAnalytics.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - }, "react-hooks/purity": { "count": 1 } diff --git a/src/analytics/PassiveAnalytics.tsx b/src/analytics/PassiveAnalytics.tsx index 34b5f2d275..c772ed09b8 100644 --- a/src/analytics/PassiveAnalytics.tsx +++ b/src/analytics/PassiveAnalytics.tsx @@ -2,8 +2,6 @@ import {useEffect, useRef} from 'react' import {getCurrentState, onAppStateChange} from '#/lib/appState' import {useAnalytics} from '#/analytics' -import {Features, features} from '#/analytics/features' -import {IS_DEV, IS_TESTFLIGHT} from '#/env' /** * Tracks passive analytics like app foreground/background time. @@ -27,19 +25,19 @@ export function PassiveAnalytics() { }) } - if (IS_DEV || IS_TESTFLIGHT) { - const feats = Object.values(Features).reduce( - (acc, feat) => { - acc[feat] = features.evalFeature(feat) - return acc - }, - {} as Record, - ) - ax.logger.info('FEATURES', { - features: feats, - definitions: features.getFeatures(), - }) - } + // if (IS_DEV || IS_TESTFLIGHT) { + // const feats = Object.values(Features).reduce( + // (acc, feat) => { + // acc[feat] = features.evalFeature(feat) + // return acc + // }, + // {} as Record, + // ) + // ax.logger.info('FEATURES', { + // features: feats, + // definitions: features.getFeatures(), + // }) + // } }) return () => sub.remove() }, [ax]) diff --git a/src/analytics/metrics/client.ts b/src/analytics/metrics/client.ts index 2aab90f265..1ea0573dcd 100644 --- a/src/analytics/metrics/client.ts +++ b/src/analytics/metrics/client.ts @@ -67,10 +67,6 @@ export class MetricsClient> { } private async sendBatch(events: Event[], isRetry: boolean = false) { - logger.debug(`sendBatch: ${events.length}`, { - isRetry, - }) - try { const body = JSON.stringify({events}) if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) { diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 5f7fc1f5e7..c7b8abf88c 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -356,6 +356,7 @@ export function PublicationCard({ /> {view.description && ( - + {view.description} @@ -617,6 +618,7 @@ export function PublicationFooter({ /> {sanitizeDisplayName(