From c6e6b83d503fdb7023e0b091d0dda079d4aab9dc Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:18:45 +0000 Subject: [PATCH 01/23] Nightly source-language update --- src/locale/locales/en/messages.po | 561 +++++++++++++++--------------- 1 file changed, 290 insertions(+), 271 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 03773ccd21..6dcbc25d3a 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -365,7 +365,7 @@ msgstr "" msgid "{0}s" msgstr "" -#: src/screens/Messages/JoinRequests.tsx:428 +#: src/screens/Messages/JoinRequests.tsx:427 msgid "{count, plural, =0 {No requests to join} one {# request to join} other {# requests to join}}" msgstr "{count, plural, =0 {No requests to join} one {# request to join} other {# requests to join}}" @@ -398,7 +398,7 @@ msgid "{count, plural, one {Update your app to see it.} other {Update your app t msgstr "{count, plural, one {Update your app to see it.} other {Update your app to see them all.}}" #. Displayed when there are more requests to join a group chat than have been loaded -#: src/screens/Messages/JoinRequests.tsx:422 +#: src/screens/Messages/JoinRequests.tsx:421 msgid "{count, plural, other {#+ requests to join}}" msgstr "{count, plural, other {#+ requests to join}}" @@ -586,7 +586,7 @@ msgid "{following} following" msgstr "" #: src/components/dms/components/GroupChatProfileCard.tsx:62 -#: src/components/dms/InitiateChatFlow.tsx:919 +#: src/components/dms/InitiateChatFlow.tsx:922 msgid "{handle} can’t be added" msgstr "{handle} can’t be added" @@ -594,7 +594,7 @@ msgstr "{handle} can’t be added" msgid "{handle} can't be messaged" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:880 +#: src/components/dms/InitiateChatFlow.tsx:883 msgid "{handle} can’t be messaged" msgstr "{handle} can’t be messaged" @@ -749,28 +749,28 @@ msgstr "" #. Like count display, the <0> tags enclose the number of likes in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.likeCount) #. placeholder {1}: post.likeCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:490 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:492 msgid "<0>{0} {1, plural, one {like} other {likes}}" msgstr "" #. Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.quoteCount) #. placeholder {1}: post.quoteCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:471 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:473 msgid "<0>{0} {1, plural, one {quote} other {quotes}}" msgstr "" #. Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.repostCount) #. placeholder {1}: post.repostCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:450 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:452 msgid "<0>{0} {1, plural, one {repost} other {reposts}}" msgstr "" #. Save count display, the <0> tags enclose the number of saves in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.bookmarkCount) #. placeholder {1}: post.bookmarkCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:508 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:510 msgid "<0>{0} {1, plural, one {save} other {saves}}" msgstr "" @@ -793,7 +793,7 @@ msgid "<0>{0} members" msgstr "" #. Text identifying the person who added you to a group chat -#: src/screens/Messages/components/ChatStatusInfo.tsx:141 +#: src/screens/Messages/components/ChatStatusInfo.tsx:135 msgid "<0>{displayName}<1/><2> added you" msgstr "<0>{displayName}<1/><2> added you" @@ -855,8 +855,8 @@ msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:52 #: src/components/dms/dialogs/NewChatDialog.tsx:90 #: src/components/dms/LeaveConvoPrompt.tsx:38 -#: src/screens/Messages/JoinRequests.tsx:188 -#: src/screens/Messages/JoinRequests.tsx:220 +#: src/screens/Messages/JoinRequests.tsx:187 +#: src/screens/Messages/JoinRequests.tsx:219 msgid "A network error occurred. Please check your internet connection." msgstr "" @@ -903,26 +903,26 @@ msgid "Abusive or discriminatory behavior" msgstr "" #. Accept a chat request -#: src/screens/Messages/components/RequestButtons.tsx:286 +#: src/screens/Messages/components/RequestButtons.tsx:287 msgid "Accept" msgstr "" #. Accept a request to join a chat -#: src/screens/Messages/JoinRequests.tsx:459 +#: src/screens/Messages/JoinRequests.tsx:458 msgctxt "button" msgid "Accept" msgstr "Accept" -#: src/screens/Messages/components/RequestButtons.tsx:280 +#: src/screens/Messages/components/RequestButtons.tsx:281 msgid "Accept chat request" msgstr "" -#: src/screens/Messages/JoinRequests.tsx:453 +#: src/screens/Messages/JoinRequests.tsx:452 msgid "Accept join request" msgstr "Accept join request" #. Accept a chat request -#: src/screens/Messages/components/RequestListItem.tsx:51 +#: src/screens/Messages/components/IncomingRequestListItem.tsx:51 msgid "Accept Request" msgstr "" @@ -958,7 +958,7 @@ msgid "Account" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:426 -#: src/screens/Messages/components/RequestButtons.tsx:97 +#: src/screens/Messages/components/RequestButtons.tsx:104 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:117 #: src/view/com/profile/ProfileMenu.tsx:188 msgctxt "toast" @@ -1092,11 +1092,11 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:1454 +#: src/view/com/composer/Composer.tsx:1472 msgid "Add another post" msgstr "" -#: src/view/com/composer/Composer.tsx:2117 +#: src/view/com/composer/Composer.tsx:2135 msgid "Add another post to thread" msgstr "" @@ -1245,7 +1245,7 @@ msgid "Additional details (limit 300 characters)" msgstr "" #: src/screens/Messages/ConversationSettings/Member.tsx:82 -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:152 +#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:150 msgid "Admin" msgstr "Admin" @@ -1414,7 +1414,7 @@ msgid "Already signed in as @{0}" msgstr "" #: src/components/images/AutoSizedImage.tsx:204 -#: src/components/images/Gallery/index.tsx:576 +#: src/components/images/Gallery/index.tsx:584 #: src/components/images/ImageLayoutGridItem.tsx:142 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94 #: src/view/com/composer/GifAltText.tsx:100 @@ -1736,12 +1736,12 @@ msgid "Apply Pull Request" msgstr "" #. placeholder {0}: niceDate(i18n, createdAt, 'medium') -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:630 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:632 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:601 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:639 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:603 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 msgid "Archived post" msgstr "" @@ -1767,7 +1767,8 @@ msgstr "" msgid "Are you sure you want to discard your changes?" msgstr "" -#: src/screens/Messages/ConversationSettings/prompts.tsx:117 +#: src/screens/Messages/ConversationSettings/prompts.tsx:120 +#: src/screens/Messages/ConversationSettings/prompts.tsx:144 msgid "Are you sure you want to leave {groupName}?" msgstr "Are you sure you want to leave {groupName}?" @@ -1783,7 +1784,12 @@ msgstr "Are you sure you want to leave this conversation? Your messages will be msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:1590 +#. placeholder {0}: convoView.name +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:117 +msgid "Are you sure you want to rescind your request to join {0}?" +msgstr "Are you sure you want to rescind your request to join {0}?" + +#: src/view/com/composer/Composer.tsx:1608 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1850,8 +1856,8 @@ msgstr "" #: src/components/dms/AddMembersFlow.tsx:520 #: src/components/dms/AddMembersFlow.tsx:526 #: src/components/dms/InitiateChatFlow.tsx:517 -#: src/components/dms/InitiateChatFlow.tsx:732 -#: src/components/dms/InitiateChatFlow.tsx:739 +#: src/components/dms/InitiateChatFlow.tsx:735 +#: src/components/dms/InitiateChatFlow.tsx:742 #: src/components/moderation/LabelsOnMeDialog.tsx:334 #: src/components/moderation/LabelsOnMeDialog.tsx:335 #: src/screens/Login/ChooseAccountForm.tsx:98 @@ -1866,7 +1872,7 @@ msgstr "" #: src/screens/Messages/components/ChatDisabled.tsx:166 #: src/screens/Messages/components/InviteLinkDialog.tsx:251 #: src/screens/Messages/components/InviteLinkDialog.tsx:279 -#: src/screens/Messages/Inbox.tsx:232 +#: src/screens/Messages/Inbox.tsx:245 #: src/screens/Profile/Header/Shell.tsx:174 #: src/screens/Settings/components/ChangePasswordDialog.tsx:273 #: src/screens/Settings/components/ChangePasswordDialog.tsx:282 @@ -1877,7 +1883,7 @@ msgstr "" msgid "Back" msgstr "" -#: src/screens/Messages/Inbox.tsx:231 +#: src/screens/Messages/Inbox.tsx:244 msgid "Back to Chats" msgstr "" @@ -1913,7 +1919,7 @@ msgstr "" msgid "Before creating a starter pack, you must first verify your email." msgstr "" -#: src/screens/Messages/components/RequestButtons.tsx:259 +#: src/screens/Messages/components/RequestButtons.tsx:260 msgid "Before you can accept this chat request, you must first verify your email." msgstr "" @@ -1926,7 +1932,7 @@ msgstr "" #: src/screens/Messages/ChatList.tsx:147 #: src/screens/Messages/ChatList.tsx:211 #: src/screens/Messages/ChatList.tsx:482 -#: src/screens/Messages/Conversation.tsx:208 +#: src/screens/Messages/Conversation.tsx:207 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:94 msgid "Before you can message another user, you must first verify your email." msgstr "" @@ -1956,9 +1962,8 @@ msgid "Birthday" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:853 -#: src/screens/Messages/components/ChatStatusInfo.tsx:67 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:222 -#: src/screens/Messages/ConversationSettings/prompts.tsx:142 +#: src/screens/Messages/ConversationSettings/prompts.tsx:169 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:207 #: src/view/com/profile/ProfileMenu.tsx:563 msgid "Block" @@ -1968,18 +1973,18 @@ msgstr "" msgid "Block {displayName}" msgstr "Block {displayName}" -#: src/components/dms/ConvoMenu.tsx:294 -#: src/components/dms/ConvoMenu.tsx:298 +#: src/components/dms/ConvoMenu.tsx:284 +#: src/components/dms/ConvoMenu.tsx:288 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:747 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:749 -#: src/screens/Messages/components/RequestButtons.tsx:149 -#: src/screens/Messages/components/RequestButtons.tsx:151 +#: src/screens/Messages/components/RequestButtons.tsx:154 +#: src/screens/Messages/components/RequestButtons.tsx:156 #: src/view/com/profile/ProfileMenu.tsx:469 #: src/view/com/profile/ProfileMenu.tsx:476 msgid "Block account" msgstr "" -#: src/screens/Messages/ConversationSettings/prompts.tsx:139 +#: src/screens/Messages/ConversationSettings/prompts.tsx:166 msgid "Block account?" msgstr "Block account?" @@ -2007,7 +2012,7 @@ msgstr "Block and leave" msgid "Block list" msgstr "" -#: src/screens/Messages/components/ChatStatusInfo.tsx:67 +#: src/screens/Messages/components/ChatStatusInfo.tsx:61 msgid "Block or report" msgstr "" @@ -2051,7 +2056,7 @@ msgid "Blocked Accounts" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:851 -#: src/screens/Messages/ConversationSettings/prompts.tsx:140 +#: src/screens/Messages/ConversationSettings/prompts.tsx:167 #: src/view/com/profile/ProfileMenu.tsx:558 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" @@ -2081,7 +2086,7 @@ msgstr "" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:655 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:657 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -2302,8 +2307,9 @@ msgstr "" #: src/screens/Deactivated.tsx:150 #: src/screens/Messages/components/InviteLinkDialog.tsx:463 #: src/screens/Messages/components/InviteLinkDialog.tsx:467 -#: src/screens/Messages/ConversationSettings/prompts.tsx:97 -#: src/screens/Messages/ConversationSettings/prompts.tsx:121 +#: src/screens/Messages/ConversationSettings/prompts.tsx:100 +#: src/screens/Messages/ConversationSettings/prompts.tsx:124 +#: src/screens/Messages/ConversationSettings/prompts.tsx:148 #: src/screens/Profile/Header/EditProfileDialog.tsx:215 #: src/screens/Profile/Header/EditProfileDialog.tsx:223 #: src/screens/Search/Shell.tsx:405 @@ -2316,8 +2322,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:300 #: src/screens/Takendown.tsx:102 #: src/screens/Takendown.tsx:105 -#: src/view/com/composer/Composer.tsx:1668 -#: src/view/com/composer/Composer.tsx:1678 +#: src/view/com/composer/Composer.tsx:1686 +#: src/view/com/composer/Composer.tsx:1696 #: src/view/com/composer/photos/EditImageDialog.web.tsx:44 #: src/view/com/composer/photos/EditImageDialog.web.tsx:53 #: src/view/shell/desktop/LeftNav.tsx:228 @@ -2352,7 +2358,7 @@ msgstr "" msgid "Captions & alt text" msgstr "" -#: src/components/images/Gallery/index.tsx:263 +#: src/components/images/Gallery/index.tsx:275 msgid "carousel" msgstr "carousel" @@ -2439,8 +2445,8 @@ msgstr "" msgid "Chat" msgstr "" -#: src/screens/Messages/components/RequestButtons.tsx:84 -#: src/screens/Messages/components/RequestButtons.tsx:330 +#: src/screens/Messages/components/RequestButtons.tsx:91 +#: src/screens/Messages/components/RequestButtons.tsx:331 msgctxt "toast" msgid "Chat deleted" msgstr "" @@ -2468,7 +2474,7 @@ msgstr "" msgid "Chat messages - sound" msgstr "" -#: src/components/dms/ConvoMenu.tsx:216 +#: src/components/dms/ConvoMenu.tsx:206 msgctxt "toast" msgid "Chat muted" msgstr "" @@ -2481,12 +2487,12 @@ msgstr "Chat recipient is not followed by the sender." msgid "Chat request inbox" msgstr "" -#: src/screens/Messages/Inbox.tsx:56 -#: src/screens/Messages/Inbox.tsx:96 +#: src/screens/Messages/Inbox.tsx:62 +#: src/screens/Messages/Inbox.tsx:109 msgid "Chat requests" msgstr "" -#: src/components/dms/ConvoMenu.tsx:84 +#: src/components/dms/ConvoMenu.tsx:88 #: src/Navigation.tsx:500 #: src/screens/Messages/ChatList.tsx:83 #: src/screens/Messages/ChatList.tsx:87 @@ -2513,7 +2519,7 @@ msgstr "Chat title changed to {0}" msgid "Chat unlocked" msgstr "Chat unlocked" -#: src/components/dms/ConvoMenu.tsx:218 +#: src/components/dms/ConvoMenu.tsx:208 msgctxt "toast" msgid "Chat unmuted" msgstr "" @@ -2805,7 +2811,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:1676 +#: src/view/com/composer/Composer.tsx:1694 msgid "Closes post composer and discards post draft" msgstr "" @@ -2855,7 +2861,7 @@ msgid "Compose new post" msgstr "" #. placeholder {0}: MAX_GRAPHEME_LENGTH || 0 -#: src/view/com/composer/Composer.tsx:1552 +#: src/view/com/composer/Composer.tsx:1570 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -2863,11 +2869,11 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:2514 +#: src/view/com/composer/Composer.tsx:2532 msgid "Compressing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2516 +#: src/view/com/composer/Composer.tsx:2534 msgid "Compressing video..." msgstr "" @@ -3035,7 +3041,7 @@ msgstr "" msgid "Conversation" msgstr "" -#: src/screens/Messages/components/ChatListItem.tsx:329 +#: src/screens/Messages/components/ChatListItem.tsx:322 msgid "Conversation deleted" msgstr "" @@ -3052,8 +3058,8 @@ msgid "Conversation left" msgstr "Conversation left" #: src/components/dms/LeaveConvoPrompt.tsx:40 -#: src/screens/Messages/JoinRequests.tsx:192 -#: src/screens/Messages/JoinRequests.tsx:224 +#: src/screens/Messages/JoinRequests.tsx:191 +#: src/screens/Messages/JoinRequests.tsx:223 msgid "Conversation not found." msgstr "Conversation not found." @@ -3222,7 +3228,7 @@ msgstr "Could not load invite" msgid "Could not load list" msgstr "" -#: src/components/dms/ConvoMenu.tsx:222 +#: src/components/dms/ConvoMenu.tsx:212 msgid "Could not mute chat" msgstr "" @@ -3357,7 +3363,7 @@ msgstr "" msgid "Create new account" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:487 +#: src/screens/Messages/ConversationSettings/index.tsx:520 msgid "Create or modify an invite link for this group chat" msgstr "Create or modify an invite link for this group chat" @@ -3381,7 +3387,7 @@ msgstr "" #. placeholder {0}: i18n.date(createdAt, { dateStyle: 'long', timeStyle: 'short', }) #. placeholder {0}: i18n.date(createdAt, { month: 'long', day: 'numeric', year: 'numeric', }) #: src/screens/Messages/components/InviteLinkDialog.tsx:324 -#: src/screens/Messages/ConversationSettings/index.tsx:440 +#: src/screens/Messages/ConversationSettings/index.tsx:472 #: src/screens/Settings/AppPasswords.tsx:174 msgid "Created {0}" msgstr "" @@ -3493,8 +3499,8 @@ msgstr "" msgid "Delete app password?" msgstr "" -#: src/screens/Messages/components/RequestButtons.tsx:343 -#: src/screens/Messages/components/RequestButtons.tsx:349 +#: src/screens/Messages/components/RequestButtons.tsx:344 +#: src/screens/Messages/components/RequestButtons.tsx:350 msgid "Delete chat" msgstr "" @@ -3509,8 +3515,8 @@ msgstr "" #: 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 +#: src/screens/Messages/components/RequestButtons.tsx:147 +#: src/screens/Messages/components/RequestButtons.tsx:149 msgid "Delete conversation" msgstr "" @@ -3537,7 +3543,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:787 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:789 -#: src/view/com/composer/Composer.tsx:1564 +#: src/view/com/composer/Composer.tsx:1582 msgid "Delete post" msgstr "" @@ -3562,7 +3568,7 @@ msgstr "" msgid "Deleted" msgstr "" -#: src/components/dms/MessagesListHeader.tsx:109 +#: src/components/dms/MessagesListHeader.tsx:103 #: src/screens/Messages/components/ChatListItem.tsx:134 #: src/screens/Messages/ConversationSettings/Member.tsx:75 msgid "Deleted Account" @@ -3684,9 +3690,9 @@ msgstr "" #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:101 #: src/screens/Profile/Header/EditProfileDialog.tsx:79 -#: src/view/com/composer/Composer.tsx:1347 -#: src/view/com/composer/Composer.tsx:1391 -#: src/view/com/composer/Composer.tsx:1597 +#: src/view/com/composer/Composer.tsx:1365 +#: src/view/com/composer/Composer.tsx:1409 +#: src/view/com/composer/Composer.tsx:1615 #: src/view/com/composer/drafts/DraftItem.tsx:242 #: src/view/com/composer/drafts/DraftsButton.tsx:131 msgid "Discard" @@ -3697,14 +3703,14 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1345 +#: src/view/com/composer/Composer.tsx:1363 #: src/view/com/composer/drafts/DraftItem.tsx:239 #: src/view/com/composer/drafts/DraftsButton.tsx:98 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:1362 -#: src/view/com/composer/Composer.tsx:1589 +#: src/view/com/composer/Composer.tsx:1380 +#: src/view/com/composer/Composer.tsx:1607 msgid "Discard post?" msgstr "" @@ -3741,7 +3747,7 @@ msgstr "" msgid "Dismiss banner" msgstr "" -#: src/view/com/composer/Composer.tsx:2435 +#: src/view/com/composer/Composer.tsx:2453 msgid "Dismiss error" msgstr "" @@ -3994,11 +4000,11 @@ msgstr "" msgid "Edit interests" msgstr "" -#: src/screens/Messages/JoinRequests.tsx:294 +#: src/screens/Messages/JoinRequests.tsx:293 msgid "Edit invite link" msgstr "Edit invite link" -#: src/screens/Messages/JoinRequests.tsx:300 +#: src/screens/Messages/JoinRequests.tsx:299 msgctxt "button" msgid "Edit invite link" msgstr "Edit invite link" @@ -4026,7 +4032,7 @@ msgstr "" msgid "Edit My Feeds" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:474 +#: src/screens/Messages/ConversationSettings/index.tsx:507 msgid "Edit name" msgstr "Edit name" @@ -4055,7 +4061,8 @@ msgstr "" msgid "Edit starter pack" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:473 +#: src/screens/Messages/ConversationSettings/index.tsx:453 +#: src/screens/Messages/ConversationSettings/index.tsx:506 msgid "Edit this group chat’s name" msgstr "Edit this group chat’s name" @@ -4272,7 +4279,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:2534 +#: src/view/com/composer/Composer.tsx:2552 #: src/view/com/util/error/ErrorScreen.tsx:40 msgid "Error" msgstr "" @@ -4457,12 +4464,12 @@ msgstr "" msgid "Extremist content" msgstr "" -#: src/screens/Messages/components/RequestButtons.tsx:243 +#: src/screens/Messages/components/RequestButtons.tsx:244 msgctxt "toast" msgid "Failed to accept chat" msgstr "" -#: src/screens/Messages/JoinRequests.tsx:186 +#: src/screens/Messages/JoinRequests.tsx:185 msgid "Failed to accept join request" msgstr "Failed to accept join request" @@ -4506,8 +4513,8 @@ msgstr "Failed to create invite link" msgid "Failed to create starter pack" msgstr "" -#: src/screens/Messages/components/RequestButtons.tsx:70 -#: src/screens/Messages/components/RequestButtons.tsx:317 +#: src/screens/Messages/components/RequestButtons.tsx:77 +#: src/screens/Messages/components/RequestButtons.tsx:318 msgctxt "toast" msgid "Failed to delete chat" msgstr "" @@ -4533,7 +4540,7 @@ msgstr "Failed to disable invite link" msgid "Failed to disconnect Germ DM. Error: {0}" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:344 +#: src/screens/Messages/ConversationSettings/index.tsx:349 msgid "Failed to edit group chat name" msgstr "Failed to edit group chat name" @@ -4557,7 +4564,7 @@ msgstr "" msgid "Failed to hide suggestion, please check your internet connection" msgstr "" -#: src/screens/Messages/JoinRequests.tsx:218 +#: src/screens/Messages/JoinRequests.tsx:217 msgid "Failed to ignore join request" msgstr "Failed to ignore join request" @@ -4571,13 +4578,13 @@ msgstr "" #: src/screens/Messages/components/ChatEnded.tsx:42 #: src/screens/Messages/components/ChatLocked.tsx:53 -#: src/screens/Messages/ConversationSettings/index.tsx:371 +#: src/screens/Messages/ConversationSettings/index.tsx:376 msgctxt "toast" msgid "Failed to leave group chat" msgstr "Failed to leave group chat" #: src/screens/Messages/ChatList.tsx:345 -#: src/screens/Messages/Inbox.tsx:201 +#: src/screens/Messages/Inbox.tsx:214 msgid "Failed to load conversations" msgstr "" @@ -4622,15 +4629,15 @@ msgstr "" msgid "Failed to load suggested follows" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:392 +#: src/screens/Messages/ConversationSettings/index.tsx:400 msgid "Failed to lock group chat" msgstr "Failed to lock group chat" -#: src/screens/Messages/Inbox.tsx:299 +#: src/screens/Messages/Inbox.tsx:316 msgid "Failed to mark all requests as read" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:358 +#: src/screens/Messages/ConversationSettings/index.tsx:363 msgid "Failed to mute group chat" msgstr "Failed to mute group chat" @@ -4671,6 +4678,7 @@ msgid "Failed to remove verification" msgstr "" #: src/components/intents/GroupChatJoinDialog.tsx:151 +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:37 msgid "Failed to rescind your request. Please try again." msgstr "Failed to rescind your request. Please try again." @@ -4678,7 +4686,7 @@ msgstr "Failed to rescind your request. Please try again." msgid "Failed to resolve location. Please try again." msgstr "" -#: src/view/com/composer/Composer.tsx:639 +#: src/view/com/composer/Composer.tsx:640 msgid "Failed to save draft" msgstr "" @@ -4721,7 +4729,7 @@ msgid "Failed to toggle thread mute, please try again" msgstr "" #: src/screens/Messages/components/ChatLocked.tsx:42 -#: src/screens/Messages/ConversationSettings/index.tsx:395 +#: src/screens/Messages/ConversationSettings/index.tsx:403 msgid "Failed to unlock group chat" msgstr "Failed to unlock group chat" @@ -5327,7 +5335,7 @@ msgstr "" msgid "GIF" msgstr "" -#: src/view/com/composer/Composer.tsx:2539 +#: src/view/com/composer/Composer.tsx:2557 msgid "GIF uploaded" msgstr "" @@ -5359,9 +5367,9 @@ msgstr "" msgid "Go back" msgstr "" -#: src/components/Error.tsx:77 +#: src/components/Error.tsx:72 #: src/screens/List/ListHiddenScreen.tsx:228 -#: src/screens/Messages/Conversation.tsx:371 +#: src/screens/Messages/Conversation.tsx:370 #: src/screens/Profile/ErrorState.tsx:63 #: src/screens/Profile/ErrorState.tsx:67 #: src/screens/StarterPack/StarterPackScreen.tsx:807 @@ -5432,9 +5440,9 @@ msgstr "" msgid "Go to next" msgstr "" -#: src/components/dms/ConvoMenu.tsx:272 -#: src/screens/Messages/components/MessagesListInfoPanel.tsx:93 -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:191 +#: src/components/dms/ConvoMenu.tsx:262 +#: src/screens/Messages/components/MessagesListInfoPanel.tsx:97 +#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:189 #: src/view/shell/desktop/LeftNav.tsx:336 #: src/view/shell/desktop/LeftNav.tsx:342 msgid "Go to profile" @@ -5444,11 +5452,11 @@ msgstr "" msgid "Go to the group chat named \"{chatName}\"" msgstr "Go to the group chat named \"{chatName}\"" -#: src/components/dms/ConvoMenu.tsx:268 +#: src/components/dms/ConvoMenu.tsx:258 msgid "Go to user's profile" msgstr "" -#: src/screens/Messages/components/MessagesListInfoPanel.tsx:87 +#: src/screens/Messages/components/MessagesListInfoPanel.tsx:91 msgid "Go to user’s profile" msgstr "Go to user’s profile" @@ -5482,46 +5490,46 @@ msgstr "" msgid "Group chat" msgstr "Group chat" -#: src/screens/Messages/components/InviteLinkDialog.tsx:517 +#: src/screens/Messages/components/InviteLinkDialog.tsx:518 msgid "Group chat invite link dialog" msgstr "Group chat invite link dialog" -#: src/screens/Messages/ConversationSettings/index.tsx:384 +#: src/screens/Messages/ConversationSettings/index.tsx:392 msgctxt "toast" msgid "Group chat locked" msgstr "Group chat locked" -#: src/screens/Messages/ConversationSettings/index.tsx:351 +#: src/screens/Messages/ConversationSettings/index.tsx:356 msgctxt "toast" msgid "Group chat muted" msgstr "Group chat muted" -#: src/screens/Messages/ConversationSettings/index.tsx:339 +#: src/screens/Messages/ConversationSettings/index.tsx:344 msgctxt "toast" msgid "Group chat name updated" msgstr "Group chat name updated" #: src/Navigation.tsx:490 -#: src/screens/Messages/ConversationSettings/index.tsx:91 +#: src/screens/Messages/ConversationSettings/index.tsx:97 msgid "Group chat settings" msgstr "Group chat settings" #: src/screens/Messages/components/ChatLocked.tsx:38 -#: src/screens/Messages/ConversationSettings/index.tsx:386 +#: src/screens/Messages/ConversationSettings/index.tsx:394 msgctxt "toast" msgid "Group chat unlocked" msgstr "Group chat unlocked" -#: src/screens/Messages/ConversationSettings/index.tsx:353 +#: src/screens/Messages/ConversationSettings/index.tsx:358 msgctxt "toast" msgid "Group chat unmuted" msgstr "Group chat unmuted" -#: src/screens/Messages/Conversation.tsx:355 +#: src/screens/Messages/Conversation.tsx:354 msgid "Group chats are not yet available" msgstr "Group chats are not yet available" -#: src/screens/Messages/Conversation.tsx:353 +#: src/screens/Messages/Conversation.tsx:352 msgid "Group chats are now available" msgstr "Group chats are now available" @@ -5542,8 +5550,8 @@ msgstr "Group name" #: src/components/dms/InitiateChatFlow.tsx:602 #: src/screens/Messages/ConversationSettings/prompts.tsx:61 -msgid "Group name is too long. The maximum number of characters is {MAX_GROUP_NAME_GRAPHEME_LENGTH}." -msgstr "Group name is too long. The maximum number of characters is {MAX_GROUP_NAME_GRAPHEME_LENGTH}." +msgid "Group name is too long. {MAX_GROUP_NAME_GRAPHEME_LENGTH, plural, other {The maximum number of characters is #.}}" +msgstr "Group name is too long. {MAX_GROUP_NAME_GRAPHEME_LENGTH, plural, other {The maximum number of characters is #.}}" #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:219 msgid "Hacking or system attacks" @@ -5808,7 +5816,7 @@ msgstr "" msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!" msgstr "" -#: src/screens/Messages/Conversation.tsx:362 +#: src/screens/Messages/Conversation.tsx:361 msgid "Hold your horses! This feature isn't available to you yet. Please check back later." msgstr "Hold your horses! This feature isn't available to you yet. Please check back later." @@ -5938,12 +5946,12 @@ msgid "If you're trying to change your handle or email, do so before you deactiv msgstr "" #. Ignore a request to join a chat -#: src/screens/Messages/JoinRequests.tsx:484 +#: src/screens/Messages/JoinRequests.tsx:483 msgctxt "button" msgid "Ignore" msgstr "Ignore" -#: src/screens/Messages/JoinRequests.tsx:478 +#: src/screens/Messages/JoinRequests.tsx:477 msgid "Ignore join request" msgstr "Ignore join request" @@ -5952,7 +5960,7 @@ msgid "Image" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:451 +#: src/components/images/Gallery/index.tsx:459 msgid "Image {0}" msgstr "Image {0}" @@ -5963,7 +5971,7 @@ msgid "Image {0} of {1}" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:436 +#: src/components/images/Gallery/index.tsx:444 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5978,7 +5986,7 @@ msgid "Image cache cleared, freed {0}" msgstr "" #. placeholder {0}: images.length -#: src/components/images/Gallery/index.tsx:264 +#: src/components/images/Gallery/index.tsx:276 msgid "Image gallery, {0} images" msgstr "Image gallery, {0} images" @@ -6075,7 +6083,7 @@ msgid "Inbox empty" msgstr "Inbox empty" #. Title message shown in chat requests inbox when it's empty -#: src/screens/Messages/Inbox.tsx:218 +#: src/screens/Messages/Inbox.tsx:231 msgid "Inbox zero!" msgstr "" @@ -6147,6 +6155,7 @@ msgid "Invalid report subject" msgstr "" #: src/components/intents/GroupChatJoinDialog.tsx:158 +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:44 msgid "Invalid rescind request." msgstr "Invalid rescind request." @@ -6184,7 +6193,7 @@ msgstr "" #: src/screens/Messages/components/InviteLinkDialog.tsx:310 #: src/screens/Messages/components/InviteLinkDialog.tsx:477 #: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:148 -#: src/screens/Messages/ConversationSettings/index.tsx:490 +#: src/screens/Messages/ConversationSettings/index.tsx:523 msgid "Invite link" msgstr "Invite link" @@ -6213,7 +6222,7 @@ msgstr "" msgid "Invite your friends to follow your favorite feeds and people" msgstr "" -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:152 +#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:150 msgid "Invited" msgstr "Invited" @@ -6243,7 +6252,7 @@ msgid "It's just you right now! Add more people to your starter pack by searchin msgstr "" #. placeholder {0}: videoState.jobId -#: src/view/com/composer/Composer.tsx:2454 +#: src/view/com/composer/Composer.tsx:2472 msgid "Job ID: {0}" msgstr "" @@ -6270,6 +6279,7 @@ msgid "Join group chat" msgstr "Join group chat" #: src/components/intents/GroupChatJoinDialog.tsx:147 +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:34 msgid "Join request rescinded." msgstr "Join request rescinded." @@ -6282,8 +6292,8 @@ msgstr "" msgid "Journalism" msgstr "" -#: src/view/com/composer/Composer.tsx:1395 -#: src/view/com/composer/Composer.tsx:1405 +#: src/view/com/composer/Composer.tsx:1413 +#: src/view/com/composer/Composer.tsx:1423 #: src/view/com/composer/drafts/DraftsButton.tsx:135 msgid "Keep editing" msgstr "" @@ -6436,12 +6446,12 @@ msgid "Learn more." msgstr "" #: src/components/dms/LeaveConvoPrompt.tsx:59 -#: src/screens/Messages/ConversationSettings/index.tsx:526 +#: src/screens/Messages/ConversationSettings/index.tsx:558 msgid "Leave" msgstr "" #. Leave a conversation (reject a chat invitation) -#: src/screens/Messages/components/ChatStatusInfo.tsx:78 +#: src/screens/Messages/components/ChatStatusInfo.tsx:72 msgctxt "Button" msgid "Leave" msgstr "Leave" @@ -6455,10 +6465,10 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:229 #: src/components/dms/AfterReportConversationDialog.tsx:233 -#: src/components/dms/ConvoMenu.tsx:246 -#: src/components/dms/ConvoMenu.tsx:250 -#: src/components/dms/ConvoMenu.tsx:316 -#: src/components/dms/ConvoMenu.tsx:320 +#: src/components/dms/ConvoMenu.tsx:236 +#: src/components/dms/ConvoMenu.tsx:240 +#: src/components/dms/ConvoMenu.tsx:308 +#: src/components/dms/ConvoMenu.tsx:312 #: src/components/dms/LeaveConvoPrompt.tsx:53 msgid "Leave conversation" msgstr "" @@ -6469,11 +6479,12 @@ msgctxt "button" msgid "Leave conversation" msgstr "Leave conversation" -#: src/screens/Messages/ConversationSettings/prompts.tsx:119 +#: src/screens/Messages/ConversationSettings/prompts.tsx:122 +#: src/screens/Messages/ConversationSettings/prompts.tsx:146 msgid "Leave group chat" msgstr "Leave group chat" -#: src/screens/Messages/ConversationSettings/index.tsx:525 +#: src/screens/Messages/ConversationSettings/index.tsx:557 msgid "Leave this group chat" msgstr "Leave this group chat" @@ -6482,6 +6493,10 @@ msgstr "Leave this group chat" msgid "Leaving Bluesky" msgstr "" +#: src/screens/Messages/ConversationSettings/prompts.tsx:145 +msgid "Leaving this chat will lock it permanently and you won’t be able to rejoin." +msgstr "Leaving this chat will lock it permanently and you won’t be able to rejoin." + #: src/screens/SignupQueued.tsx:158 msgid "left to go." msgstr "" @@ -6575,7 +6590,7 @@ msgstr "" msgid "Likes of your reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:486 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:488 msgid "Likes on this post" msgstr "" @@ -6770,23 +6785,23 @@ msgstr "" msgid "Loading..." msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:504 +#: src/screens/Messages/ConversationSettings/index.tsx:537 msgid "Lock" msgstr "Lock" -#: src/screens/Messages/ConversationSettings/prompts.tsx:96 +#: src/screens/Messages/ConversationSettings/prompts.tsx:99 msgid "Lock group chat" msgstr "Lock group chat" -#: src/screens/Messages/ConversationSettings/prompts.tsx:94 +#: src/screens/Messages/ConversationSettings/prompts.tsx:97 msgid "Lock group chat?" msgstr "Lock group chat?" -#: src/screens/Messages/ConversationSettings/index.tsx:502 +#: src/screens/Messages/ConversationSettings/index.tsx:535 msgid "Lock this group chat" msgstr "Lock this group chat" -#: src/screens/Messages/ConversationSettings/index.tsx:504 +#: src/screens/Messages/ConversationSettings/index.tsx:537 msgid "Locked" msgstr "Locked" @@ -6862,17 +6877,17 @@ msgstr "" msgid "Manage your muted words and tags" msgstr "" -#: src/screens/Messages/Inbox.tsx:307 -#: src/screens/Messages/Inbox.tsx:313 +#: src/screens/Messages/Inbox.tsx:324 +#: src/screens/Messages/Inbox.tsx:330 msgid "Mark all as read" msgstr "" -#: src/components/dms/ConvoMenu.tsx:258 -#: src/components/dms/ConvoMenu.tsx:262 +#: src/components/dms/ConvoMenu.tsx:248 +#: src/components/dms/ConvoMenu.tsx:252 msgid "Mark as read" msgstr "" -#: src/screens/Messages/Inbox.tsx:294 +#: src/screens/Messages/Inbox.tsx:311 msgid "Marked all as read" msgstr "" @@ -6904,7 +6919,7 @@ msgstr "" msgid "Members" msgstr "Members" -#: src/screens/Messages/ConversationSettings/prompts.tsx:95 +#: src/screens/Messages/ConversationSettings/prompts.tsx:98 msgid "Members can still read chat history but can’t send new messages." msgstr "Members can still read chat history but can’t send new messages." @@ -6930,7 +6945,7 @@ msgstr "Message" #: src/screens/Messages/components/MessageComposer.tsx:218 #: src/screens/Messages/components/MessageInput.tsx:235 #: src/screens/Messages/components/MessageInput.web.tsx:203 -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:199 +#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:198 msgctxt "action" msgid "Message" msgstr "Message" @@ -6940,11 +6955,11 @@ msgstr "Message" msgid "Message {0}" msgstr "" -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:195 +#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:194 msgid "Message {displayName}" msgstr "Message {displayName}" -#: src/screens/Messages/components/ChatListItem.tsx:330 +#: src/screens/Messages/components/ChatListItem.tsx:323 msgid "Message deleted" msgstr "" @@ -7114,7 +7129,7 @@ msgstr "" msgid "Music" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:466 +#: src/screens/Messages/ConversationSettings/index.tsx:499 msgid "Mute" msgstr "Mute" @@ -7142,8 +7157,8 @@ msgstr "" msgid "Mute accounts" msgstr "" -#: src/components/dms/ConvoMenu.tsx:277 -#: src/components/dms/ConvoMenu.tsx:284 +#: src/components/dms/ConvoMenu.tsx:267 +#: src/components/dms/ConvoMenu.tsx:274 msgid "Mute conversation" msgstr "" @@ -7159,7 +7174,7 @@ msgstr "" msgid "Mute these accounts?" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:464 +#: src/screens/Messages/ConversationSettings/index.tsx:497 msgid "Mute this group chat" msgstr "Mute this group chat" @@ -7197,7 +7212,7 @@ msgstr "" msgid "Mute words & tags" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:466 +#: src/screens/Messages/ConversationSettings/index.tsx:499 msgid "Muted" msgstr "Muted" @@ -7351,8 +7366,8 @@ msgstr "New group chat" #. Button used to create a new group chat. #. Button used to create a new group chat. -#: src/components/dms/InitiateChatFlow.tsx:774 -#: src/components/dms/InitiateChatFlow.tsx:808 +#: src/components/dms/InitiateChatFlow.tsx:777 +#: src/components/dms/InitiateChatFlow.tsx:811 msgctxt "action" msgid "New group chat" msgstr "New group chat" @@ -7523,7 +7538,7 @@ msgstr "" msgid "No media yet" msgstr "" -#: src/screens/Messages/components/ChatListItem.tsx:316 +#: src/screens/Messages/components/ChatListItem.tsx:311 msgid "No messages yet" msgstr "" @@ -7607,8 +7622,8 @@ msgstr "" msgid "No results for \"{0}\"." msgstr "" -#: src/components/Lists.tsx:204 -#: src/components/Lists.tsx:219 +#: src/components/Lists.tsx:203 +#: src/components/Lists.tsx:218 msgid "No results found" msgstr "" @@ -7767,10 +7782,10 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 -#: src/components/dms/InitiateChatFlow.tsx:707 +#: src/components/dms/InitiateChatFlow.tsx:710 #: src/components/dms/MessageItem.tsx:641 #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:661 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:663 msgid "Okay" msgstr "" @@ -7801,11 +7816,11 @@ msgstr "One of the selected recipients does not allow group chats." msgid "One of the selected recipients has blocked you and cannot be messaged." msgstr "One of the selected recipients has blocked you and cannot be messaged." -#: src/view/com/composer/Composer.tsx:855 +#: src/view/com/composer/Composer.tsx:856 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:852 +#: src/view/com/composer/Composer.tsx:853 msgid "One or more images is missing alt text." msgstr "" @@ -7817,11 +7832,11 @@ msgstr "" msgid "One or more of your selected files are too large. Maximum size is {VIDEO_MAX_SIZE_MB} MB." msgstr "One or more of your selected files are too large. Maximum size is {VIDEO_MAX_SIZE_MB} MB." -#: src/view/com/composer/Composer.tsx:650 +#: src/view/com/composer/Composer.tsx:651 msgid "One or more posts are too long to save as a draft. {MAX_DRAFT_GRAPHEME_LENGTH, plural, one {The maximum number of characters is # character.} other {The maximum number of characters is # characters.}}" msgstr "" -#: src/view/com/composer/Composer.tsx:862 +#: src/view/com/composer/Composer.tsx:863 msgid "One or more videos is missing alt text." msgstr "" @@ -7834,15 +7849,15 @@ msgstr "" #. placeholder {0}: result.accepted.length #. placeholder {1}: next.length #. placeholder {2}: next.length -#: src/view/com/composer/Composer.tsx:225 +#: src/view/com/composer/Composer.tsx:226 msgid "Only {0} of {1} {2, plural, one {image} other {images}} added; limit is {MAX_GALLERY_IMAGES}" msgstr "Only {0} of {1} {2, plural, one {image} other {images}} added; limit is {MAX_GALLERY_IMAGES}" -#: src/screens/Messages/JoinRequests.tsx:196 +#: src/screens/Messages/JoinRequests.tsx:195 msgid "Only admins can accept join requests." msgstr "Only admins can accept join requests." -#: src/screens/Messages/JoinRequests.tsx:228 +#: src/screens/Messages/JoinRequests.tsx:227 msgid "Only admins can ignore join requests." msgstr "Only admins can ignore join requests." @@ -7900,12 +7915,12 @@ msgstr "" msgid "Open chat" msgstr "Open chat" -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:139 +#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:137 msgid "Open chat member options for {displayName}" msgstr "Open chat member options for {displayName}" -#: src/screens/Messages/components/ChatListItem.tsx:495 -#: src/screens/Messages/components/ChatListItem.tsx:499 +#: src/screens/Messages/components/ChatListItem.tsx:486 +#: src/screens/Messages/components/ChatListItem.tsx:490 msgid "Open conversation options" msgstr "" @@ -7919,7 +7934,7 @@ msgstr "" #: src/screens/Messages/components/MessageComposer.tsx:176 #: src/screens/Messages/components/MessageInput.web.tsx:148 -#: src/view/com/composer/Composer.tsx:2094 +#: src/view/com/composer/Composer.tsx:2112 msgid "Open emoji picker" msgstr "" @@ -7944,8 +7959,8 @@ msgstr "" msgid "Open group chat" msgstr "Open group chat" -#: src/components/dms/MessagesListHeader.tsx:181 -#: src/components/dms/MessagesListHeader.tsx:220 +#: src/components/dms/MessagesListHeader.tsx:168 +#: src/components/dms/MessagesListHeader.tsx:207 msgid "Open group chat settings" msgstr "Open group chat settings" @@ -8063,7 +8078,7 @@ msgstr "" msgid "Opens flow to sign in to your existing Bluesky account" msgstr "" -#: src/components/images/Gallery/index.tsx:452 +#: src/components/images/Gallery/index.tsx:460 msgid "Opens full image" msgstr "Opens full image" @@ -8204,8 +8219,8 @@ msgstr "Owner" msgid "Owner must lock the group before leaving." msgstr "Owner must lock the group before leaving." -#: src/components/Lists.tsx:205 -#: src/components/Lists.tsx:220 +#: src/components/Lists.tsx:204 +#: src/components/Lists.tsx:219 #: src/view/screens/NotFound.tsx:34 #: src/view/screens/NotFound.tsx:41 msgid "Page not found" @@ -8574,7 +8589,7 @@ msgstr "" msgid "Porn" msgstr "" -#: src/view/com/composer/Composer.tsx:1742 +#: src/view/com/composer/Composer.tsx:1760 msgctxt "action" msgid "Post" msgstr "" @@ -8594,12 +8609,12 @@ msgstr "" msgid "Post a video" msgstr "" -#: src/view/com/composer/Composer.tsx:1740 +#: src/view/com/composer/Composer.tsx:1758 msgctxt "action" msgid "Post All" msgstr "" -#: src/view/com/composer/Composer.tsx:1404 +#: src/view/com/composer/Composer.tsx:1422 msgid "Post anyway" msgstr "Post anyway" @@ -8711,10 +8726,10 @@ msgstr "" msgid "Press to attempt reconnection" msgstr "" -#: src/components/Error.tsx:61 +#: src/components/Error.tsx:56 #: src/components/Lists.tsx:104 #: src/screens/Messages/components/MessageListError.tsx:23 -#: src/screens/Messages/JoinRequests.tsx:350 +#: src/screens/Messages/JoinRequests.tsx:349 #: src/screens/Signup/BackNextButtons.tsx:48 msgid "Press to retry" msgstr "" @@ -8772,11 +8787,11 @@ msgstr "" msgid "Privacy violation of a minor" msgstr "" -#: src/view/com/composer/Composer.tsx:2528 +#: src/view/com/composer/Composer.tsx:2546 msgid "Processing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2530 +#: src/view/com/composer/Composer.tsx:2548 msgid "Processing video..." msgstr "" @@ -8821,22 +8836,22 @@ msgid "Public, sharable lists of users to mute or block in bulk." msgstr "" #. Accessibility label for button to publish a single post -#: src/view/com/composer/Composer.tsx:1726 +#: src/view/com/composer/Composer.tsx:1744 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1721 +#: src/view/com/composer/Composer.tsx:1739 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:1710 +#: src/view/com/composer/Composer.tsx:1728 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:1715 +#: src/view/com/composer/Composer.tsx:1733 msgid "Publish reply" msgstr "" @@ -8898,7 +8913,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:467 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:469 msgid "Quotes of this post" msgstr "" @@ -9028,16 +9043,16 @@ msgid "Refresh the page to see it." msgstr "Refresh the page to see it." #. Reject a chat request, this opens a menu with options -#: src/screens/Messages/components/RequestButtons.tsx:131 +#: src/screens/Messages/components/RequestButtons.tsx:136 msgid "Reject" msgstr "" -#: src/screens/Messages/components/RequestButtons.tsx:120 +#: src/screens/Messages/components/RequestButtons.tsx:125 msgid "Reject chat request" msgstr "" #: src/screens/Messages/ChatList.tsx:349 -#: src/screens/Messages/Inbox.tsx:205 +#: src/screens/Messages/Inbox.tsx:218 msgid "Reload conversations" msgstr "" @@ -9290,7 +9305,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1738 +#: src/view/com/composer/Composer.tsx:1756 msgctxt "action" msgid "Reply" msgstr "" @@ -9330,7 +9345,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:176 #: src/features/liveNow/components/LiveStatusDialog.tsx:267 -#: src/screens/Messages/ConversationSettings/index.tsx:517 +#: src/screens/Messages/ConversationSettings/index.tsx:550 msgid "Report" msgstr "" @@ -9339,10 +9354,10 @@ msgstr "" msgid "Report account" msgstr "" -#: src/components/dms/ConvoMenu.tsx:304 -#: src/components/dms/ConvoMenu.tsx:308 -#: src/screens/Messages/components/RequestButtons.tsx:160 -#: src/screens/Messages/components/RequestButtons.tsx:163 +#: src/components/dms/ConvoMenu.tsx:295 +#: src/components/dms/ConvoMenu.tsx:299 +#: src/screens/Messages/components/RequestButtons.tsx:161 +#: src/screens/Messages/components/RequestButtons.tsx:164 msgid "Report conversation" msgstr "" @@ -9395,7 +9410,7 @@ msgstr "" msgid "Report this feed" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:516 +#: src/screens/Messages/ConversationSettings/index.tsx:549 msgid "Report this group chat" msgstr "Report this group chat" @@ -9465,7 +9480,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:446 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:448 msgid "Reposts of this post" msgstr "" @@ -9479,7 +9494,7 @@ msgstr "" msgid "Request access to group chat" msgstr "Request access to group chat" -#: src/screens/Messages/JoinRequests.tsx:178 +#: src/screens/Messages/JoinRequests.tsx:177 msgid "Request approved." msgstr "Request approved." @@ -9488,7 +9503,7 @@ msgstr "Request approved." msgid "Request code" msgstr "" -#: src/screens/Messages/JoinRequests.tsx:210 +#: src/screens/Messages/JoinRequests.tsx:209 msgid "Request ignored." msgstr "Request ignored." @@ -9508,7 +9523,7 @@ msgstr "Requests" #: src/Navigation.tsx:495 #: src/screens/Messages/JoinRequests.tsx:58 -#: src/screens/Messages/JoinRequests.tsx:420 +#: src/screens/Messages/JoinRequests.tsx:419 msgid "Requests to join" msgstr "Requests to join" @@ -9530,9 +9545,15 @@ msgid "Required in your region" msgstr "" #: src/components/intents/GroupChatJoinDialog.tsx:273 +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:116 +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:118 msgid "Rescind request" msgstr "Rescind request" +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:53 +msgid "Rescind request to join group chat" +msgstr "Rescind request to join group chat" + #: src/components/dialogs/EmailDialog/components/ResendEmailText.tsx:39 msgid "Resend" msgstr "" @@ -9596,7 +9617,7 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceErrors.tsx:31 #: src/components/contacts/screens/VerifyNumber.tsx:350 #: src/components/contacts/screens/VerifyNumber.tsx:355 -#: src/components/Error.tsx:65 +#: src/components/Error.tsx:60 #: src/components/Lists.tsx:115 #: src/components/moderation/ReportDialog/index.tsx:297 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56 @@ -9606,8 +9627,8 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:335 #: src/screens/Messages/ChatList.tsx:354 #: src/screens/Messages/components/MessageListError.tsx:24 -#: src/screens/Messages/Inbox.tsx:210 -#: src/screens/Messages/JoinRequests.tsx:356 +#: src/screens/Messages/Inbox.tsx:223 +#: src/screens/Messages/JoinRequests.tsx:355 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:268 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:271 #: src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx:92 @@ -9626,7 +9647,7 @@ msgstr "" msgid "Retry loading report options" msgstr "" -#: src/components/Error.tsx:73 +#: src/components/Error.tsx:68 #: src/screens/List/ListHiddenScreen.tsx:223 #: src/screens/StarterPack/StarterPackScreen.tsx:801 msgid "Return to previous page" @@ -9660,7 +9681,7 @@ msgstr "Sad GIFs" #: src/components/StarterPack/QrCodeDialog.tsx:207 #: src/features/liveNow/components/EditLiveDialog.tsx:204 #: src/features/liveNow/components/EditLiveDialog.tsx:211 -#: src/screens/Messages/ConversationSettings/prompts.tsx:71 +#: src/screens/Messages/ConversationSettings/prompts.tsx:74 #: src/screens/Profile/Header/EditProfileDialog.tsx:233 #: src/screens/Profile/Header/EditProfileDialog.tsx:247 #: src/screens/SavedFeeds.tsx:124 @@ -9685,22 +9706,22 @@ msgstr "" #: src/screens/SavedFeeds.tsx:124 #: src/screens/SavedFeeds.tsx:311 #: src/screens/SavedFeeds.tsx:315 -#: src/view/com/composer/Composer.tsx:1385 +#: src/view/com/composer/Composer.tsx:1403 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save changes" msgstr "" -#: src/view/com/composer/Composer.tsx:1357 +#: src/view/com/composer/Composer.tsx:1375 #: src/view/com/composer/drafts/DraftsButton.tsx:93 msgid "Save changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1385 +#: src/view/com/composer/Composer.tsx:1403 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save draft" msgstr "" -#: src/view/com/composer/Composer.tsx:1359 +#: src/view/com/composer/Composer.tsx:1377 #: src/view/com/composer/drafts/DraftsButton.tsx:95 msgid "Save draft?" msgstr "" @@ -10036,7 +10057,7 @@ msgstr "" msgid "Select GIF \"{0}\"" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:699 +#: src/components/dms/InitiateChatFlow.tsx:702 msgid "Select group chat members" msgstr "Select group chat members" @@ -10484,7 +10505,7 @@ msgstr "" msgid "Show when you’re live" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:602 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:604 msgid "Shows information about when this post was created" msgstr "" @@ -10611,7 +10632,7 @@ msgstr "" msgid "Skip contact sharing and continue to the app" msgstr "" -#: src/view/com/composer/Composer.tsx:1402 +#: src/view/com/composer/Composer.tsx:1420 msgid "Skip empty posts?" msgstr "Skip empty posts?" @@ -10625,7 +10646,7 @@ msgstr "" msgid "Skip to next step" msgstr "" -#: src/components/images/Gallery/index.tsx:435 +#: src/components/images/Gallery/index.tsx:443 msgid "slide" msgstr "slide" @@ -10716,7 +10737,7 @@ msgid "Something wasn't quite right with the data you're trying to report. Pleas msgstr "" #: src/screens/Messages/Conversation.tsx:137 -#: src/screens/Messages/ConversationSettings/index.tsx:112 +#: src/screens/Messages/ConversationSettings/index.tsx:118 #: src/screens/Messages/JoinRequests.tsx:87 msgid "Something went wrong" msgstr "" @@ -10807,12 +10828,12 @@ msgstr "" msgid "Start adding people!" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:700 +#: src/components/dms/InitiateChatFlow.tsx:703 msgid "Start chat" msgstr "Start chat" #: src/components/dialogs/SearchablePeopleList.tsx:428 -#: src/components/dms/InitiateChatFlow.tsx:848 +#: src/components/dms/InitiateChatFlow.tsx:851 msgid "Start chat with {displayName}" msgstr "" @@ -11297,7 +11318,7 @@ msgid "The laws in your region require you to verify you're an adult to access c msgstr "" #: src/components/intents/GroupChatJoinDialog.tsx:135 -#: src/screens/Messages/JoinRequests.tsx:201 +#: src/screens/Messages/JoinRequests.tsx:200 msgid "The member limit has been reached." msgstr "The member limit has been reached." @@ -11369,6 +11390,7 @@ msgstr "There was a problem loading GIFs. Check your connection and try again." #: src/components/contacts/screens/GetContacts.tsx:147 #: src/components/intents/GroupChatJoinDialog.tsx:119 #: src/components/intents/GroupChatJoinDialog.tsx:153 +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:39 msgid "There was a problem with your internet connection, please try again" msgstr "" @@ -11390,7 +11412,7 @@ msgid "There was an issue fetching notifications. Tap here to try again." msgstr "" #: src/screens/Search/Explore.tsx:1027 -#: src/view/com/posts/PostFeed.tsx:775 +#: src/view/com/posts/PostFeed.tsx:777 msgid "There was an issue fetching posts. Tap here to try again." msgstr "" @@ -11530,7 +11552,7 @@ msgstr "This chat has ended" msgid "This chat is full" msgstr "This chat is full" -#: src/screens/Messages/components/ChatListItem.tsx:380 +#: src/screens/Messages/components/ChatListItem.tsx:372 #: src/screens/Messages/components/ChatLocked.tsx:61 msgid "This chat is locked" msgstr "This chat is locked" @@ -11709,7 +11731,7 @@ msgstr "This person is blocking you" #. placeholder {0}: niceDate(i18n, createdAt) #. placeholder {1}: niceDate(i18n, indexedAt) -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:642 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:644 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -11729,7 +11751,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:1015 +#: src/view/com/composer/Composer.tsx:1033 msgid "This post's author has disabled quote posts." msgstr "" @@ -11741,8 +11763,8 @@ msgstr "" msgid "This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others." msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:132 -#: src/screens/Messages/JoinRequests.tsx:111 +#: src/screens/Messages/ConversationSettings/index.tsx:137 +#: src/screens/Messages/JoinRequests.tsx:110 msgid "This screen is only available for group conversations." msgstr "This screen is only available for group conversations." @@ -12039,7 +12061,7 @@ msgstr "" msgid "Unable to delete" msgstr "" -#: src/screens/Messages/JoinRequests.tsx:346 +#: src/screens/Messages/JoinRequests.tsx:345 msgid "Unable to fetch join requests." msgstr "Unable to fetch join requests." @@ -12093,8 +12115,8 @@ msgstr "" msgid "Unblock {displayName}" msgstr "Unblock {displayName}" -#: src/components/dms/ConvoMenu.tsx:294 -#: src/components/dms/ConvoMenu.tsx:298 +#: src/components/dms/ConvoMenu.tsx:284 +#: src/components/dms/ConvoMenu.tsx:288 #: src/view/com/profile/ProfileMenu.tsx:468 #: src/view/com/profile/ProfileMenu.tsx:474 msgid "Unblock account" @@ -12164,14 +12186,6 @@ msgstr "" msgid "Unfortunately, your declared age indicates that you are not old enough to access Bluesky in your region." msgstr "" -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:245 -msgid "Uninvite" -msgstr "Uninvite" - -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:240 -msgid "Uninvite {displayName} from this group chat" -msgstr "Uninvite {displayName} from this group chat" - #: src/components/verification/VerificationsDialog.tsx:209 msgid "Unknown verifier" msgstr "" @@ -12198,7 +12212,7 @@ msgstr "" msgid "Unlock chat" msgstr "Unlock chat" -#: src/screens/Messages/ConversationSettings/index.tsx:501 +#: src/screens/Messages/ConversationSettings/index.tsx:534 msgid "Unlock this group chat" msgstr "Unlock this group chat" @@ -12226,7 +12240,7 @@ msgstr "Unmute {0}" msgid "Unmute account" msgstr "" -#: src/components/dms/ConvoMenu.tsx:282 +#: src/components/dms/ConvoMenu.tsx:272 msgid "Unmute conversation" msgstr "" @@ -12235,7 +12249,7 @@ msgstr "" msgid "Unmute list" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:463 +#: src/screens/Messages/ConversationSettings/index.tsx:496 msgid "Unmute this group chat" msgstr "Unmute this group chat" @@ -12314,7 +12328,7 @@ msgstr "" msgid "Unsupported clipboard content" msgstr "Unsupported clipboard content" -#: src/view/com/composer/Composer.tsx:1491 +#: src/view/com/composer/Composer.tsx:1509 msgid "Unsupported video type: {mimeType}" msgstr "" @@ -12357,7 +12371,7 @@ msgstr "Update your app" msgid "Update your app to see it." msgstr "Update your app to see it." -#: src/screens/Messages/Conversation.tsx:360 +#: src/screens/Messages/Conversation.tsx:359 msgid "Update your app to the latest version to join in!" msgstr "Update your app to the latest version to join in!" @@ -12409,7 +12423,7 @@ msgstr "" msgid "Upload from Library" msgstr "" -#: src/view/com/composer/Composer.tsx:2521 +#: src/view/com/composer/Composer.tsx:2539 msgid "Uploading GIF..." msgstr "" @@ -12423,7 +12437,7 @@ msgstr "" msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:2523 +#: src/view/com/composer/Composer.tsx:2541 msgid "Uploading video..." msgstr "" @@ -12712,7 +12726,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:2541 +#: src/view/com/composer/Composer.tsx:2559 msgid "Video uploaded" msgstr "" @@ -12729,7 +12743,7 @@ msgstr "" msgid "Videos must be less than 3 minutes long." msgstr "" -#: src/view/com/composer/Composer.tsx:1108 +#: src/view/com/composer/Composer.tsx:1126 msgctxt "Action to view the post the user just created" msgid "View" msgstr "" @@ -12760,8 +12774,8 @@ msgstr "" msgid "View {0}’s profile" msgstr "View {0}’s profile" -#: src/components/dms/MessagesListHeader.tsx:123 -#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:185 +#: src/components/dms/MessagesListHeader.tsx:111 +#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:183 msgid "View {displayName}’s profile" msgstr "View {displayName}’s profile" @@ -12823,7 +12837,7 @@ msgstr "" msgid "View more trending videos" msgstr "" -#: src/view/com/composer/Composer.tsx:1103 +#: src/view/com/composer/Composer.tsx:1121 msgid "View post" msgstr "" @@ -12851,7 +12865,7 @@ msgstr "View publication" msgid "View the avatar" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:488 +#: src/screens/Messages/ConversationSettings/index.tsx:521 msgid "View the invite link for this group chat" msgstr "View the invite link for this group chat" @@ -12974,7 +12988,7 @@ msgstr "" msgid "We couldn’t load this conversation’s join requests" msgstr "We couldn’t load this conversation’s join requests" -#: src/screens/Messages/ConversationSettings/index.tsx:113 +#: src/screens/Messages/ConversationSettings/index.tsx:119 msgid "We couldn’t load this conversation’s settings" msgstr "We couldn’t load this conversation’s settings" @@ -13119,11 +13133,11 @@ msgstr "We’re sorry, but your search could not be completed. Please try again msgid "We're sorry, you cannot access this screen at this time." msgstr "" -#: src/view/com/composer/Composer.tsx:1013 +#: src/view/com/composer/Composer.tsx:1031 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" -#: src/components/Lists.tsx:224 +#: src/components/Lists.tsx:223 #: src/view/screens/NotFound.tsx:44 msgid "We're sorry! We can't find the page you were looking for." msgstr "" @@ -13170,7 +13184,7 @@ msgid "What do you want to call your starter pack?" msgstr "" #: src/view/com/auth/SplashScreen.web.tsx:98 -#: src/view/com/composer/Composer.tsx:1455 +#: src/view/com/composer/Composer.tsx:1473 #: src/view/com/feeds/ComposerPrompt.tsx:193 msgid "What's up?" msgstr "" @@ -13198,7 +13212,7 @@ msgstr "" #: src/screens/Home/NoFeedsPinned.tsx:80 #: src/screens/Messages/ChatList.tsx:334 -#: src/screens/Messages/Inbox.tsx:190 +#: src/screens/Messages/Inbox.tsx:203 msgid "Whoops!" msgstr "" @@ -13256,7 +13270,7 @@ msgstr "Would you like to block this user and/or leave this conversation?" msgid "Would you like to save this as a draft before viewing your drafts?" msgstr "" -#: src/view/com/composer/Composer.tsx:1373 +#: src/view/com/composer/Composer.tsx:1391 msgid "Would you like to save this as a draft to edit later?" msgstr "" @@ -13265,12 +13279,12 @@ msgstr "" msgid "Write a post" msgstr "" -#: src/view/com/composer/Composer.tsx:1551 +#: src/view/com/composer/Composer.tsx:1569 msgid "Write post" msgstr "" #: src/screens/PostThread/components/ThreadComposePrompt.tsx:91 -#: src/view/com/composer/Composer.tsx:1453 +#: src/view/com/composer/Composer.tsx:1471 msgid "Write your reply" msgstr "" @@ -13283,8 +13297,8 @@ msgstr "" msgid "Wrong DID returned from server. Received: {0}" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:131 -#: src/screens/Messages/JoinRequests.tsx:110 +#: src/screens/Messages/ConversationSettings/index.tsx:136 +#: src/screens/Messages/JoinRequests.tsx:109 msgid "Wrong kind of conversation" msgstr "Wrong kind of conversation" @@ -13438,11 +13452,11 @@ msgid "You can now sign in with your new password." msgstr "" #. Toast shown when the user tries to add more images but the post gallery is already at the cap -#: src/view/com/composer/Composer.tsx:212 +#: src/view/com/composer/Composer.tsx:213 msgid "You can only add up to {MAX_GALLERY_IMAGES, plural, other {# images}} per post" msgstr "You can only add up to {MAX_GALLERY_IMAGES, plural, other {# images}} per post" -#: src/view/com/composer/Composer.tsx:1378 +#: src/view/com/composer/Composer.tsx:1396 msgid "You can only save drafts up to 1000 characters." msgstr "" @@ -13581,7 +13595,7 @@ msgstr "" msgid "You have temporarily reached the limit for video uploads. Please try again later." msgstr "" -#: src/view/com/composer/Composer.tsx:1368 +#: src/view/com/composer/Composer.tsx:1386 msgid "You have unsaved changes to this draft, would you like to save them?" msgstr "" @@ -13683,6 +13697,11 @@ msgstr "You reacted {0} to {target}" msgid "You recently changed your birthdate" msgstr "" +#. Displayed when the user has requested to join a group chat. +#: src/screens/Messages/components/OutgoingRequestListItem.tsx:106 +msgid "You requested to join" +msgstr "You requested to join" + #: src/screens/Settings/Settings.tsx:297 #: src/view/shell/desktop/LeftNav.tsx:225 msgid "You will be signed out of all your accounts." @@ -13705,7 +13724,7 @@ msgstr "" msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." msgstr "" -#: src/screens/Messages/ConversationSettings/prompts.tsx:118 +#: src/screens/Messages/ConversationSettings/prompts.tsx:121 msgid "You won’t be able to rejoin unless you’re invited." msgstr "You won’t be able to rejoin unless you’re invited." @@ -13778,7 +13797,7 @@ msgstr "" msgid "You've reached the end of your feed! Find some more accounts to follow." msgstr "" -#: src/view/com/composer/Composer.tsx:637 +#: src/view/com/composer/Composer.tsx:638 msgid "You've reached the maximum number of drafts" msgstr "" @@ -13818,11 +13837,11 @@ msgstr "" msgid "Your account is not yet old enough to upload videos. Please try again later." msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:705 +#: src/components/dms/InitiateChatFlow.tsx:708 msgid "Your account is too new" msgstr "Your account is too new" -#: src/components/dms/InitiateChatFlow.tsx:706 +#: src/components/dms/InitiateChatFlow.tsx:709 msgid "Your account must be at least 7 days old to create a new group chat." msgstr "Your account must be at least 7 days old to create a new group chat." @@ -13946,11 +13965,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:1099 +#: src/view/com/composer/Composer.tsx:1117 msgid "Your post was sent" msgstr "" -#: src/view/com/composer/Composer.tsx:1096 +#: src/view/com/composer/Composer.tsx:1114 msgid "Your posts were sent" msgstr "" @@ -13971,7 +13990,7 @@ msgstr "" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "" -#: src/view/com/composer/Composer.tsx:1098 +#: src/view/com/composer/Composer.tsx:1116 msgid "Your reply was sent" msgstr "" @@ -13984,7 +14003,7 @@ msgstr "" msgid "Your selected interests help us serve you content you care about." msgstr "" -#: src/view/com/composer/Composer.tsx:1403 +#: src/view/com/composer/Composer.tsx:1421 msgid "Your thread has empty posts that will be skipped. The remaining posts will be published as a thread." msgstr "Your thread has empty posts that will be skipped. The remaining posts will be published as a thread." From 8015c26955d7682e1a3dda06929e621b0a94dce9 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:13:15 -0700 Subject: [PATCH 02/23] Hide last message in chat list if it's blocked (#10800) --- src/components/dms/getMessageInfo.ts | 4 ++++ src/lib/moderation/blocked-and-muted.ts | 2 +- src/screens/Messages/components/ChatListItem.tsx | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/dms/getMessageInfo.ts b/src/components/dms/getMessageInfo.ts index 6860c51f92..4eade5cb62 100644 --- a/src/components/dms/getMessageInfo.ts +++ b/src/components/dms/getMessageInfo.ts @@ -2,6 +2,7 @@ import {AppBskyEmbedRecord, ChatBskyConvoDefs} from '@atproto/api' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' +import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import { postUriToRelativePath, @@ -13,6 +14,7 @@ export type UserMessageInfo = { message: string | null sentAt: string reportableMessage?: ChatBskyConvoDefs.MessageView + isBlockedMessage: boolean } export function getMessageInfo({ @@ -36,6 +38,7 @@ export function getMessageInfo({ const isGroup = ChatBskyConvoDefs.isGroupConvo(convo.kind) const reportableMessage = isFromMe ? undefined : lastMessage + const isBlockedMessage = sender ? isBlockedOrBlocking(sender) : false const prefix = (message: string) => { if (isFromMe) { @@ -89,5 +92,6 @@ export function getMessageInfo({ message, sentAt: lastMessage.sentAt, reportableMessage, + isBlockedMessage, } } diff --git a/src/lib/moderation/blocked-and-muted.ts b/src/lib/moderation/blocked-and-muted.ts index 9806ca83ad..c8cc8611ce 100644 --- a/src/lib/moderation/blocked-and-muted.ts +++ b/src/lib/moderation/blocked-and-muted.ts @@ -1,7 +1,7 @@ import type * as bsky from '#/types/bsky' export function isBlockedOrBlocking(profile: bsky.profile.AnyProfileView) { - return profile.viewer?.blockedBy || profile.viewer?.blocking + return Boolean(profile.viewer?.blockedBy || profile.viewer?.blocking) } export function isMuted(profile: bsky.profile.AnyProfileView) { diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 1c83a6876b..d60b5f56c2 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -331,7 +331,9 @@ function BaseChatItem({ i18n, }) if (info) { - lastMessage = info.message ?? lastMessage + lastMessage = info.isBlockedMessage + ? l`This message is hidden` + : (info.message ?? lastMessage) lastMessageSentAt = info.sentAt } } From 2cb225dddde6fc0e493c8ef5a229aebc5f1fc457 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:03:52 +0100 Subject: [PATCH 03/23] Mark string for translation and add context (#10805) --- src/components/images/Gallery/index.tsx | 6 +++++- src/screens/Messages/JoinRequest.tsx | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 1b4e1d6239..5b0d97bc17 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -523,7 +523,11 @@ function GalleryImage({ a.font_bold, largeAltBadge ? a.text_xs : {fontSize: 8}, ]}> - {index + 1}/{imageCount} + + {index + 1}/{imageCount} + ) : null} diff --git a/src/screens/Messages/JoinRequest.tsx b/src/screens/Messages/JoinRequest.tsx index c6da1f91ec..1e23c3405b 100644 --- a/src/screens/Messages/JoinRequest.tsx +++ b/src/screens/Messages/JoinRequest.tsx @@ -127,7 +127,9 @@ export function JoinRequest({setScreenState}: Props) { a.leading_snug, t.atoms.text_contrast_medium, ]}> - + {data.joinLinkPreviews[0].memberCount}/ {data.joinLinkPreviews[0].memberLimit} From 84c4223ff937314b08514c9ea11d95825d1d2887 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jun 2026 16:31:22 +0300 Subject: [PATCH 04/23] [Chat] Gate group chats for under-18 users (#10548) Co-authored-by: Eric Bailey --- eslint-suppressions.json | 11 ---- src/ageAssurance/const.ts | 28 ++++++++ src/ageAssurance/index.tsx | 27 ++++++-- src/ageAssurance/types.ts | 2 + src/ageAssurance/util.ts | 49 +++----------- src/components/dms/InitiateChatFlow.tsx | 9 ++- src/screens/Messages/ChatList.tsx | 4 ++ src/screens/Messages/Settings.tsx | 24 +++++-- src/screens/Signup/StepInfo/index.tsx | 2 +- src/state/birthdate.ts | 9 ++- src/state/preferences/moderation-opts.tsx | 2 +- .../queries/messages/list-conversations.tsx | 3 + .../queries/messages/restrictChatSettings.ts | 64 ++++++++++++++++--- src/state/queries/preferences/const.ts | 15 ++++- src/state/queries/preferences/moderation.ts | 14 +--- src/state/session/agent.ts | 13 ++-- 16 files changed, 180 insertions(+), 96 deletions(-) create mode 100644 src/ageAssurance/const.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 304fd4a065..1eea217133 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -19,11 +19,6 @@ "count": 1 } }, - "src/ageAssurance/util.ts": { - "@typescript-eslint/no-floating-promises": { - "count": 1 - } - }, "src/alf/util/flatten.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -1903,12 +1898,6 @@ "src/state/session/agent.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 - }, - "@typescript-eslint/no-floating-promises": { - "count": 1 - }, - "@typescript-eslint/require-await": { - "count": 1 } }, "src/state/shell/color-mode.tsx": { diff --git a/src/ageAssurance/const.ts b/src/ageAssurance/const.ts new file mode 100644 index 0000000000..bd79353b51 --- /dev/null +++ b/src/ageAssurance/const.ts @@ -0,0 +1,28 @@ +import { + ageAssuranceRuleIDs as ids, + type AppBskyAgeassuranceDefs, +} from '@atproto/api' + +import {AgeAssuranceAccess} from '#/ageAssurance/types' + +/** + * Minimum age required to access the app at all. + */ +export const MIN_ACCESS_AGE = 13 + +export const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = { + countryCode: '*', + regionCode: undefined, + minAccessAge: MIN_ACCESS_AGE, + rules: [ + { + $type: ids.IfDeclaredOverAge, + age: MIN_ACCESS_AGE, + access: AgeAssuranceAccess.Full, + }, + { + $type: ids.Default, + access: AgeAssuranceAccess.None, + }, + ], +} diff --git a/src/ageAssurance/index.tsx b/src/ageAssurance/index.tsx index 2b10c93a5d..20bcc2bdb8 100644 --- a/src/ageAssurance/index.tsx +++ b/src/ageAssurance/index.tsx @@ -1,6 +1,7 @@ import {createContext, useCallback, useContext, useMemo} from 'react' import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications' +import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {useAgent} from '#/state/session' import {Provider as RedirectOverlayProvider} from '#/ageAssurance/components/RedirectOverlay' import { @@ -20,7 +21,6 @@ import { } from '#/ageAssurance/types' import { computeAgeAssuranceFlags, - maybeRestrictChatSettings, useAgeAssuranceRegionConfigWithFallback, } from '#/ageAssurance/util' @@ -32,7 +32,6 @@ export { usePatchServerState as usePatchAgeAssuranceServerState, } from '#/ageAssurance/data' export {logger} from '#/ageAssurance/logger' -export {MIN_ACCESS_AGE} from '#/ageAssurance/util' const AgeAssuranceStateContext = createContext<{ Access: typeof AgeAssuranceAccess @@ -48,8 +47,10 @@ const AgeAssuranceStateContext = createContext<{ access: AgeAssuranceAccess.Full, }, flags: { + isAgeRestricted: false, adultContentDisabled: false, chatDisabled: false, + groupChatDisabled: false, isDeclaredUnderAdultAge: false, isOverRegionMinAccessAge: false, isOverAppMinAccessAge: false, @@ -84,13 +85,25 @@ function InnerProvider({children}: {children: React.ReactNode}) { const handleAccessUpdate = useCallback( (s: AgeAssuranceState) => { - const isAgeRestricted = s.access !== AgeAssuranceAccess.Full - if (isAgeRestricted) { - void getAndRegisterPushToken({isAgeRestricted}) - maybeRestrictChatSettings({agent}) + const flags = computeAgeAssuranceFlags({ + state: s, + regionConfig, + metadata, + }) + if (flags.isAgeRestricted) { + void getAndRegisterPushToken({ + isAgeRestricted: true, + }) + } + if (flags.chatDisabled || flags.groupChatDisabled) { + void restrictChatSettings({ + agent, + restrictIncoming: flags.chatDisabled, + restrictGroupInvites: flags.groupChatDisabled, + }) } }, - [agent, getAndRegisterPushToken], + [agent, getAndRegisterPushToken, regionConfig, metadata], ) useOnAgeAssuranceAccessUpdate(handleAccessUpdate) diff --git a/src/ageAssurance/types.ts b/src/ageAssurance/types.ts index 12473bc2e6..21abff89f0 100644 --- a/src/ageAssurance/types.ts +++ b/src/ageAssurance/types.ts @@ -30,8 +30,10 @@ export type AgeAssuranceState = { } export type AgeAssuranceFlags = { + isAgeRestricted: boolean adultContentDisabled: boolean chatDisabled: boolean + groupChatDisabled: boolean isDeclaredUnderAdultAge: boolean isOverRegionMinAccessAge: boolean isOverAppMinAccessAge: boolean diff --git a/src/ageAssurance/util.ts b/src/ageAssurance/util.ts index b0e601d6c7..0d493f1130 100644 --- a/src/ageAssurance/util.ts +++ b/src/ageAssurance/util.ts @@ -1,20 +1,14 @@ import {useMemo} from 'react' import { - ageAssuranceRuleIDs as ids, type AppBskyAgeassuranceDefs, - type AtpAgent, getAgeAssuranceRegionConfig, type ModerationPrefs, } from '@atproto/api' import {getAge} from '#/lib/strings/time' -import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' -import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' -import { - getDidFromAgentSession, - getOtherRequiredDataFromCache, - useAgeAssuranceServerDataContext, -} from '#/ageAssurance/data' +import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const' +import {FALLBACK_REGION_CONFIG, MIN_ACCESS_AGE} from '#/ageAssurance/const' +import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data' import { AgeAssuranceAccess, type AgeAssuranceFlags, @@ -23,24 +17,6 @@ import { } from '#/ageAssurance/types' import {type Geolocation, useGeolocation} from '#/geolocation' -export const MIN_ACCESS_AGE = 13 -const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = { - countryCode: '*', - regionCode: undefined, - minAccessAge: MIN_ACCESS_AGE, - rules: [ - { - $type: ids.IfDeclaredOverAge, - age: MIN_ACCESS_AGE, - access: AgeAssuranceAccess.Full, - }, - { - $type: ids.Default, - access: AgeAssuranceAccess.None, - }, - ], -} - /** * Get age assurance region config based on geolocation, with fallback to * app defaults if no region config is found. @@ -121,19 +97,6 @@ export const makeAgeRestrictedModerationPrefs = ( labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, }) -/** - * Checks our cache of the actor's chat declaration record, and if it's not - * already restricted, restricts it. - */ -export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) { - const did = getDidFromAgentSession(agent) - if (!did) return - const data = getOtherRequiredDataFromCache({did}) - // ...update the chat setting record if allowIncoming is not already 'none'. - if (data?.actorDeclaration?.allowIncoming === 'none') return - restrictChatSettings({agent, did}) -} - export function computeAgeAssuranceFlags({ state, regionConfig, @@ -143,10 +106,12 @@ export function computeAgeAssuranceFlags({ regionConfig: AppBskyAgeassuranceDefs.ConfigRegion metadata?: AgeAssuranceMetadata }): AgeAssuranceFlags { - const chatDisabled = state.access !== AgeAssuranceAccess.Full + const isAgeRestricted = state.access !== AgeAssuranceAccess.Full + const chatDisabled = isAgeRestricted const isDeclaredUnderAdultAge = metadata?.declaredAge ? metadata.declaredAge < 18 : true + const groupChatDisabled = chatDisabled || isDeclaredUnderAdultAge const isOverRegionMinAccessAge = metadata?.declaredAge ? metadata.declaredAge >= regionConfig.minAccessAge : false @@ -157,8 +122,10 @@ export function computeAgeAssuranceFlags({ state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge return { + isAgeRestricted, adultContentDisabled, chatDisabled, + groupChatDisabled, isDeclaredUnderAdultAge, isOverRegionMinAccessAge, isOverAppMinAccessAge, diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx index 161b0cf722..ed519fed23 100644 --- a/src/components/dms/InitiateChatFlow.tsx +++ b/src/components/dms/InitiateChatFlow.tsx @@ -36,6 +36,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Ti import * as ProfileCard from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' +import {useAgeAssurance} from '#/ageAssurance' import {IS_NATIVE, IS_WEB} from '#/env' import type * as bsky from '#/types/bsky' import {ChatProfileTabs} from './ChatProfileTabs' @@ -209,6 +210,7 @@ export function InitiateChatFlow({ const [footerHeight, setFooterHeight] = useState(0) const listRef = useRef(null) const {currentAccount} = useSession() + const aa = useAgeAssurance() const inputRef = useRef(null) const accountTooNewPromptControl = Dialog.useDialogControl() @@ -330,7 +332,11 @@ export function InitiateChatFlow({ }) } - if (chatState === ChatState.NEW_CHAT && searchText === '') { + if ( + chatState === ChatState.NEW_CHAT && + searchText === '' && + !aa.flags.groupChatDisabled + ) { _items.unshift({type: 'newGroupChat', key: 'newGroupChat'}) } @@ -344,6 +350,7 @@ export function InitiateChatFlow({ results, currentAccount?.did, follows, + aa.flags.groupChatDisabled, ]) if (searchText && !isFetching && !items.length && !isError) { diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 387fcd2bd1..ab622f708a 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -198,6 +198,7 @@ export function ChatList({ }) { const t = useTheme() const {t: l} = useLingui() + const aa = useAgeAssurance() const scrollElRef: ListRef = useAnimatedRef() const {isWithinSplitView} = useIsWithinSplitView() @@ -230,6 +231,7 @@ export function ChatList({ const {refetch: refetchInbox} = useListConvosQuery({ status: 'request', + kind: aa.flags.groupChatDisabled ? 'direct' : 'all', }) useRefreshOnFocus(refetch) @@ -449,6 +451,7 @@ export function Header({ }) { const {t: l} = useLingui() const {gtMobile} = useBreakpoints() + const aa = useAgeAssurance() const requireEmailVerification = useRequireEmailVerification() const leftConvos = useLeftConvos() const {isWithinSplitView} = useIsWithinSplitView() @@ -462,6 +465,7 @@ export function Header({ useListConvosQuery({ status: 'request', readState: 'unread', + kind: aa.flags.groupChatDisabled ? 'direct' : 'all', }) const inboxAllConvos = diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index 91d9f879d5..195a87af62 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -21,6 +21,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/compon import * as Layout from '#/components/Layout' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {useAgeAssurance} from '#/ageAssurance' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -46,6 +47,7 @@ export function MessagesSettingsScreenInner({}: Props) { const t = useTheme() const {t: l} = useLingui() const ax = useAnalytics() + const aa = useAgeAssurance() const {currentAccount} = useSession() const {data: profile} = useProfileQuery({ did: currentAccount!.did, @@ -55,6 +57,7 @@ export function MessagesSettingsScreenInner({}: Props) { const exportCarControl = Dialog.useDialogControl() const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable) + const groupInvitesLocked = aa.flags.groupChatDisabled const allowMessagesFromOptions: {name: AllowIncoming; label: string}[] = [ { @@ -192,15 +195,26 @@ export function MessagesSettingsScreenInner({}: Props) { a.leading_snug, t.atoms.text_contrast_high, ]}> - - You can continue ongoing conversations regardless of which - setting you choose. - + {groupInvitesLocked ? ( + + Group chats are only available to users 18 and over. + + ) : ( + + You can continue ongoing conversations regardless of which + setting you choose. + + )} {allowGroupInvitesFromOptions.map(option => ( diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index 65b2791c29..5d1ecae014 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -24,9 +24,9 @@ import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate' import * as Toast from '#/components/Toast' +import {MIN_ACCESS_AGE} from '#/ageAssurance/const' import { isUnderAge, - MIN_ACCESS_AGE, useAgeAssuranceRegionConfigWithFallback, } from '#/ageAssurance/util' import {useAnalytics} from '#/analytics' diff --git a/src/state/birthdate.ts b/src/state/birthdate.ts index 71ca871df3..183e51aa45 100644 --- a/src/state/birthdate.ts +++ b/src/state/birthdate.ts @@ -1,10 +1,11 @@ import {useMemo} from 'react' import {useMutation, useQueryClient} from '@tanstack/react-query' +import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {preferencesQueryKey} from '#/state/queries/preferences' import {useAgent, useSession} from '#/state/session' import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance' -import {isUnderAge, maybeRestrictChatSettings} from '#/ageAssurance/util' +import {isUnderAge} from '#/ageAssurance/util' import {IS_DEV} from '#/env' import {account} from '#/storage' @@ -66,7 +67,11 @@ export function useBirthdateMutation() { }) if (isUnderAge(birthDate.toISOString(), 18)) { - maybeRestrictChatSettings({agent}) + await restrictChatSettings({ + agent, + restrictIncoming: true, + restrictGroupInvites: true, + }) } /** diff --git a/src/state/preferences/moderation-opts.tsx b/src/state/preferences/moderation-opts.tsx index b8592d77e0..119c9008db 100644 --- a/src/state/preferences/moderation-opts.tsx +++ b/src/state/preferences/moderation-opts.tsx @@ -2,7 +2,7 @@ import {createContext, useContext, useMemo} from 'react' import {AtpAgent, type ModerationOpts} from '@atproto/api' import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences' -import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' +import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const' import {useSession} from '#/state/session' import {usePreferencesQuery} from '../queries/preferences' diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx index 62f5772bef..0bc2f69997 100644 --- a/src/state/queries/messages/list-conversations.tsx +++ b/src/state/queries/messages/list-conversations.tsx @@ -20,6 +20,7 @@ import {useMessagesEventBus} from '#/state/messages/events' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAgent, useSession} from '#/state/session' import {parseConvoView} from '#/components/dms/util' +import {useAgeAssurance} from '#/ageAssurance' import * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import {useLeftConvos} from './leave-conversation' @@ -122,10 +123,12 @@ export function ListConvosProviderInner({ }: { children: React.ReactNode }) { + const aa = useAgeAssurance() const {refetch, data} = useListConvosQuery({ readState: 'unread', limit: UNREAD_LIMIT, lockStatus: 'unlocked', + kind: aa.flags.groupChatDisabled ? 'direct' : 'all', }) const messagesBus = useMessagesEventBus() const queryClient = useQueryClient() diff --git a/src/state/queries/messages/restrictChatSettings.ts b/src/state/queries/messages/restrictChatSettings.ts index 3a1e64eff1..ad33385dc8 100644 --- a/src/state/queries/messages/restrictChatSettings.ts +++ b/src/state/queries/messages/restrictChatSettings.ts @@ -3,23 +3,71 @@ import {type ChatBskyActorDeclaration} from '@atproto/api' import {networkRetry} from '#/lib/async/retry' import {logger} from '#/logger' -import {setOtherRequiredDataActorDeclarationCache} from '#/ageAssurance/data' +import { + getDidFromAgentSession, + getOtherRequiredDataFromCache, + setOtherRequiredDataActorDeclarationCache, +} from '#/ageAssurance/data' /** - * Helper to update the chat settings record. + * Updates the chat actor declaration record to restrict who can contact the + * user. Both restrictions write to the same record (`rkey: 'self'`), so this + * is a single helper to avoid two concurrent `putRecord` calls racing each + * other and clobbering one another's changes. + * + * - `restrictIncoming`: sets `allowIncoming: 'none'` (used when a user isn't + * age-assured). + * - `restrictGroupInvites`: sets `allowGroupInvites: 'none'` (used for under-18 + * users, who per spec cannot participate in group chats). + * + * Dimensions that aren't being restricted preserve their cached value, falling + * back to the lexicon defaults when the cache is empty. */ export async function restrictChatSettings({ agent, - did, + restrictIncoming = false, + restrictGroupInvites = false, }: { agent: AtpAgent - did: string + restrictIncoming?: boolean + restrictGroupInvites?: boolean }): Promise { + const did = getDidFromAgentSession(agent) + if (!did) return + + const cached = getOtherRequiredDataFromCache({did})?.actorDeclaration + + // When the cache is empty we fall back to defaults for any dimension we're + // not explicitly restricting, which could drop/downgrade a value the user + // has actually set server-side. The cache should be hydrated by + // prefetchOtherRequiredData before any of these paths fire, so log if that + // assumption ever breaks. (Restricting both dimensions is unaffected, so + // don't warn for the common signup/birthdate-change case.) + if (!cached && (!restrictIncoming || !restrictGroupInvites)) { + logger.warn( + `restrictChatSettings: cache miss, falling back to defaults for unrestricted dimensions`, + ) + } + + const record: ChatBskyActorDeclaration.Main = { + $type: 'chat.bsky.actor.declaration', + allowIncoming: restrictIncoming + ? 'none' + : (cached?.allowIncoming ?? 'following'), + allowGroupInvites: restrictGroupInvites + ? 'none' + : cached?.allowGroupInvites, + } + + // Nothing to do if the record already reflects the desired restrictions. + if ( + cached?.allowIncoming === record.allowIncoming && + cached?.allowGroupInvites === record.allowGroupInvites + ) { + return + } + try { - const record: ChatBskyActorDeclaration.Main = { - $type: 'chat.bsky.actor.declaration', - allowIncoming: 'none', - } await networkRetry(3, () => agent.com.atproto.repo.putRecord({ repo: did, diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 7508fa4f6d..02f7d62a87 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -1,9 +1,22 @@ -import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' +import {DEFAULT_LABEL_SETTINGS} from '@atproto/api' + import { type ThreadViewPreferences, type UsePreferencesQueryResponse, } from '#/state/queries/preferences/types' +/** + * More strict than our default settings for logged in users. + * + * Defined here rather than in `./moderation` to avoid a module-init cycle: + * `moderation` imports `./index`, which re-exports this file, so reading the + * value from `moderation` at init time lands in its temporal dead zone. + */ +export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS = + Object.fromEntries( + Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']), + ) + export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] = { hideReplies: false, diff --git a/src/state/queries/preferences/moderation.ts b/src/state/queries/preferences/moderation.ts index 1c0f5a3fe5..c23e995e41 100644 --- a/src/state/queries/preferences/moderation.ts +++ b/src/state/queries/preferences/moderation.ts @@ -1,22 +1,10 @@ import {useMemo} from 'react' -import { - BskyAgent, - DEFAULT_LABEL_SETTINGS, - interpretLabelValueDefinitions, -} from '@atproto/api' +import {BskyAgent, interpretLabelValueDefinitions} from '@atproto/api' import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useLabelersDetailedInfoQuery} from '../labeler' import {usePreferencesQuery} from './index' -/** - * More strict than our default settings for logged in users. - */ -export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: typeof DEFAULT_LABEL_SETTINGS = - Object.fromEntries( - Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']), - ) - export function useMyLabelersQuery({ excludeNonConfigurableLabelers = false, }: { diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 0abc0ca6cf..fe669dfe8b 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -29,7 +29,6 @@ import { setCreatedAtForDid, } from '#/ageAssurance/data' import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' -import {AgeAssuranceAccess} from '#/ageAssurance/types' import {features} from '#/analytics' import {emitNetworkConfirmed, emitNetworkLost} from '../events' import {addSessionErrorLog} from './logging' @@ -218,10 +217,14 @@ export async function createAgentAndCreateAccount( throw e }), // wait for AA data to load first, then check state - aa.then(async () => { - const {state} = unsafeGetAndComputeAgeAssurance({did: account.did}) - if (state.access !== AgeAssuranceAccess.Full) { - restrictChatSettings({agent, did: account.did}) + aa.then(() => { + const {flags} = unsafeGetAndComputeAgeAssurance({did: account.did}) + if (flags?.chatDisabled || flags?.groupChatDisabled) { + void restrictChatSettings({ + agent, + restrictIncoming: flags.chatDisabled, + restrictGroupInvites: flags.groupChatDisabled, + }) } }), ]).then(promises => { From 22643c76d0b71211aae36e32d5bec8c07ac88cc5 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jun 2026 16:51:36 +0300 Subject: [PATCH 05/23] Fix fullscreen infinite render loop in video embeds (#10807) --- eslint-suppressions.json | 8 ----- .../VideoEmbed/ActiveVideoWebContext.tsx | 11 +++++-- src/components/hooks/useFullscreen.ts | 32 +++++++++++-------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 1eea217133..fffa450d37 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -645,14 +645,6 @@ "count": 2 } }, - "src/components/hooks/useFullscreen.ts": { - "@typescript-eslint/no-floating-promises": { - "count": 2 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/hooks/useLandingEntry.native.ts": { "react-hooks/set-state-in-effect": { "count": 1 diff --git a/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx b/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx index d0f2660ba4..90f399eb21 100644 --- a/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx +++ b/src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx @@ -108,10 +108,15 @@ export function useActiveVideoWeb() { return { active: activeViewId === id, - setActive: () => { + setActive: useCallback(() => { setActiveView(id) - }, + }, [setActiveView, id]), currentActiveView: activeViewId, - sendPosition: (y: number) => sendViewPosition(id, y), + sendPosition: useCallback( + (y: number) => { + sendViewPosition(id, y) + }, + [sendViewPosition, id], + ), } } diff --git a/src/components/hooks/useFullscreen.ts b/src/components/hooks/useFullscreen.ts index 107095ef84..3578dd9fc4 100644 --- a/src/components/hooks/useFullscreen.ts +++ b/src/components/hooks/useFullscreen.ts @@ -1,10 +1,4 @@ -import { - useCallback, - useEffect, - useRef, - useState, - useSyncExternalStore, -} from 'react' +import {useCallback, useEffect, useRef, useSyncExternalStore} from 'react' import {IS_WEB, IS_WEB_FIREFOX, IS_WEB_SAFARI} from '#/env' @@ -13,28 +7,38 @@ function fullscreenSubscribe(onChange: () => void) { return () => document.removeEventListener('fullscreenchange', onChange) } +function getFullscreenSnapshot() { + return Boolean(document.fullscreenElement) +} + export function useFullscreen(ref?: React.RefObject) { if (!IS_WEB) throw new Error("'useFullscreen' is a web-only hook") - const isFullscreen = useSyncExternalStore(fullscreenSubscribe, () => - Boolean(document.fullscreenElement), + const isFullscreen = useSyncExternalStore( + fullscreenSubscribe, + getFullscreenSnapshot, ) const scrollYRef = useRef(null) - const [prevIsFullscreen, setPrevIsFullscreen] = useState(isFullscreen) + // Tracked via a ref rather than state so that reacting to a fullscreen change + // never schedules its own render. Scheduling a render in response to the + // external store value (the old `setPrevIsFullscreen` pattern) was a seed for + // the commit-phase update loop reported in APP-315 / APP-5PP / APP-7ZB. + const prevIsFullscreenRef = useRef(isFullscreen) const toggleFullscreen = useCallback(() => { if (isFullscreen) { - document.exitFullscreen() + void document.exitFullscreen() } else { if (!ref) throw new Error('No ref provided') if (!ref.current) return scrollYRef.current = window.scrollY - ref.current.requestFullscreen() + void ref.current.requestFullscreen() } }, [isFullscreen, ref]) useEffect(() => { + const prevIsFullscreen = prevIsFullscreenRef.current if (prevIsFullscreen === isFullscreen) return - setPrevIsFullscreen(isFullscreen) + prevIsFullscreenRef.current = isFullscreen // Chrome has an issue where it doesn't scroll back to the top after exiting fullscreen // Let's play it safe and do it if not FF or Safari, since anything else will probably be chromium @@ -46,7 +50,7 @@ export function useFullscreen(ref?: React.RefObject) { } }, 100) } - }, [isFullscreen, prevIsFullscreen]) + }, [isFullscreen]) return [isFullscreen, toggleFullscreen] as const } From bc94c8c07be1415760342e72e0f7ff0f1971db1b Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Tue, 9 Jun 2026 10:11:21 -0400 Subject: [PATCH 06/23] Fix composer horizontal swipe triggering browser back/forward (#10812) --- src/view/com/composer/Composer.tsx | 32 +++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index ab8f4d9cc5..25ed831c3c 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -134,7 +134,14 @@ import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' -import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' +import { + IS_ANDROID, + IS_IOS, + IS_LIQUID_GLASS, + IS_NATIVE, + IS_WEB, + IS_WEB_SAFARI, +} from '#/env' import {type Gif} from '#/features/gifPicker/types' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import { @@ -1219,6 +1226,24 @@ export const ComposePost = ({ } }, [composerState]) + useEffect(() => { + // Safari ignores `overscroll-behavior`, so horizontal trackpad swipes over + // the composer (e.g. on a quote post) can still trigger the browser's + // back/forward navigation gesture. Suppress predominantly-horizontal wheel + // events so the history-nav gesture never fires. Chrome and Firefox are + // covered by the `overscrollBehaviorX: 'contain'` style on the ScrollView. + if (!IS_WEB_SAFARI) return + const el = + scrollViewRef.current?.getScrollableNode() as unknown as HTMLElement | null + if (!el) return + const onWheel = (e: WheelEvent) => { + if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return + e.preventDefault() + } + el.addEventListener('wheel', onWheel, {passive: false}) + return () => el.removeEventListener('wheel', onWheel) + }, [scrollViewRef]) + const isLastThreadedPost = thread.posts.length > 1 && nextPost === undefined const { scrollHandler, @@ -1324,6 +1349,11 @@ export const ComposePost = ({ web({ scrollbarGutter: 'stable', scrollbarColor: `${t.palette.contrast_200} transparent`, + // Prevent horizontal trackpad swipes from triggering the + // browser's back/forward overscroll-navigation gesture. + // Handles Chrome and Firefox; Safari is handled separately + // via a wheel listener since it ignores overscroll-behavior. + overscrollBehaviorX: 'contain', }), ]} keyboardShouldPersistTaps="always" From 4c4ebd0510df48b136e9bf6aee5cdf348d428e1c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jun 2026 17:20:34 +0300 Subject: [PATCH 07/23] Catch NotAllowedError in web GIF player + lint modules/ (#10806) --- eslint-suppressions.json | 109 ++++++++++++++++++ .../expo-bluesky-gif-view/src/GifView.web.tsx | 13 ++- .../src/VisibilityView/index.native.tsx | 6 +- package.json | 2 +- 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index fffa450d37..59b062cc7b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1,4 +1,113 @@ { + "modules/bottom-sheet/src/BottomSheetNativeComponent.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "modules/bottom-sheet/src/BottomSheetPortal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "modules/bottom-sheet/src/lib/Portal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts": { + "@typescript-eslint/require-await": { + "count": 4 + } + }, + "modules/expo-bluesky-gif-view/src/GifView.tsx": { + "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "modules/expo-bluesky-gif-view/src/GifView.web.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "modules/expo-bluesky-swiss-army/src/PlatformInfo/index.native.ts": { + "@typescript-eslint/no-unsafe-call": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + } + }, + "modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts": { + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts": { + "@typescript-eslint/no-explicit-any": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 9 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts": { + "no-restricted-imports": { + "count": 1 + } + }, + "modules/expo-emoji-picker/src/EmojiPickerView.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, "src/Navigation.tsx": { "@typescript-eslint/no-floating-promises": { "count": 1 diff --git a/modules/expo-bluesky-gif-view/src/GifView.web.tsx b/modules/expo-bluesky-gif-view/src/GifView.web.tsx index c9f439bffe..e51b9bf9fc 100644 --- a/modules/expo-bluesky-gif-view/src/GifView.web.tsx +++ b/modules/expo-bluesky-gif-view/src/GifView.web.tsx @@ -67,7 +67,18 @@ export class GifView extends PureComponent { } async playAsync(): Promise { - this.videoPlayerRef.current?.play() + try { + await this.videoPlayerRef.current?.play() + } catch (err) { + // `play()` rejects with a NotAllowedError when the browser blocks + // playback (e.g. Safari low-power mode or autoplay policy). This is + // expected and benign - the GIF simply stays paused - so swallow it + // rather than letting it surface as an unhandled rejection. + if (err instanceof DOMException && err.name === 'NotAllowedError') { + return + } + throw err + } } async pauseAsync(): Promise { diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx index 9d0e8cf220..aba96c6d4b 100644 --- a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx +++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx @@ -1,12 +1,12 @@ import React from 'react' -import {StyleProp, ViewStyle} from 'react-native' +import {type StyleProp, type ViewStyle} from 'react-native' import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' -import {VisibilityViewProps} from './types' +import {type VisibilityViewProps} from './types' const NativeView: React.ComponentType<{ onChangeStatus: (e: {nativeEvent: {isActive: boolean}}) => void children: React.ReactNode - enabled: Boolean + enabled: boolean style: StyleProp }> = requireNativeViewManager('ExpoBlueskyVisibilityView') diff --git a/package.json b/package.json index e054aca94a..8853eb2210 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "test-watch": "NODE_ENV=test jest --watchAll", "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", "test-coverage": "NODE_ENV=test jest --coverage", - "lint": "eslint --cache --quiet src", + "lint": "eslint --cache --quiet src modules", "lint-native": "swiftlint ./modules && ktlint ./modules", "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", "typecheck": "tsgo --project ./tsconfig.check.json", From c52231a449e33b184c19219e9f694da402cec9e8 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:23:33 -0700 Subject: [PATCH 08/23] Update handling of join link previews (#10798) Co-authored-by: Samuel Newman --- bskyogcard/package.json | 2 +- bskyogcard/pnpm-lock.yaml | 10 +-- bskyogcard/src/routes/chat-invite.tsx | 4 +- package.json | 2 +- pnpm-lock.yaml | 10 +-- .../Post/Embed/JoinRequestEmbed.tsx | 13 ++-- src/components/dms/ChatInvite/Card.tsx | 3 +- src/components/dms/ChatInvite/Context.tsx | 4 +- src/components/dms/ChatInvite/Root.tsx | 19 +++--- src/components/dms/MessageItemInviteEmbed.tsx | 8 ++- src/components/dms/MessagesListHeader.tsx | 6 +- src/components/dms/SystemMessageItem.tsx | 3 +- .../intents/GroupChatJoinDialog.tsx | 17 ++--- src/lib/api/index.ts | 6 +- src/lib/api/resolve.ts | 12 ++-- src/screens/Messages/JoinRequest.tsx | 50 +++++++------- .../Messages/components/MessageInputEmbed.tsx | 28 +++++--- .../components/MessagesListGroupInfoPanel.tsx | 6 +- .../components/MessagesListInfoPanel.tsx | 3 +- .../components/OutgoingRequestListItem.tsx | 15 +++-- src/state/queries/join-links.ts | 65 +++++++++++++++++-- .../queries/messages/disable-join-link.ts | 2 + .../queries/messages/enable-join-link.ts | 2 + src/view/com/composer/Composer.tsx | 5 +- 24 files changed, 192 insertions(+), 103 deletions(-) diff --git a/bskyogcard/package.json b/bskyogcard/package.json index 0fb28334cb..50248e4cf9 100644 --- a/bskyogcard/package.json +++ b/bskyogcard/package.json @@ -18,7 +18,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@atproto/api": "0.20.6", + "@atproto/api": "0.20.11", "@atproto/common": "^0.6.1", "@resvg/resvg-js": "^2.6.2", "express": "^4.19.2", diff --git a/bskyogcard/pnpm-lock.yaml b/bskyogcard/pnpm-lock.yaml index 39685ea8ce..2bebbe145c 100644 --- a/bskyogcard/pnpm-lock.yaml +++ b/bskyogcard/pnpm-lock.yaml @@ -208,8 +208,8 @@ importers: .: dependencies: '@atproto/api': - specifier: 0.20.6 - version: 0.20.6 + specifier: 0.20.11 + version: 0.20.11 '@atproto/common': specifier: ^0.6.1 version: 0.6.1 @@ -259,8 +259,8 @@ importers: packages: - '@atproto/api@0.20.6': - resolution: {integrity: sha512-WnFPcUl+qZdXmt27+Tg93BDIvBt/WpXfLIiBzBTp3ms9aszM5hAsfc7G8KEsnsmnRvcm0xRfiKEjIt5FxTKdYg==} + '@atproto/api@0.20.11': + resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==} engines: {node: '>=22'} '@atproto/common-web@0.5.0': @@ -1154,7 +1154,7 @@ packages: snapshots: - '@atproto/api@0.20.6': + '@atproto/api@0.20.11': dependencies: '@atproto/common-web': 0.5.0 '@atproto/lexicon': 0.7.1 diff --git a/bskyogcard/src/routes/chat-invite.tsx b/bskyogcard/src/routes/chat-invite.tsx index e3267cc0d0..36482743b8 100644 --- a/bskyogcard/src/routes/chat-invite.tsx +++ b/bskyogcard/src/routes/chat-invite.tsx @@ -1,6 +1,6 @@ import assert from 'node:assert' -import {type ChatBskyGroupDefs} from '@atproto/api' +import {ChatBskyGroupDefs} from '@atproto/api' import resvg from '@resvg/resvg-js' import {type Express} from 'express' import satori from 'satori' @@ -32,7 +32,7 @@ export default function (ctx: AppContext, app: Express) { codes: [code], }) const found = result.data.joinLinkPreviews[0] - if (!found) { + if (!ChatBskyGroupDefs.isJoinLinkPreviewView(found)) { return res.status(404).end('not found') } preview = found diff --git a/package.json b/package.json index 8853eb2210..3040ce2e90 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "prettier": "prettier --check ." }, "dependencies": { - "@atproto/api": "0.20.9", + "@atproto/api": "0.20.11", "@atproto/syntax": "0.6.1", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2f65bf409..9d21e055cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,8 +242,8 @@ importers: .: dependencies: '@atproto/api': - specifier: 0.20.9 - version: 0.20.9 + specifier: 0.20.11 + version: 0.20.11 '@atproto/syntax': specifier: 0.6.1 version: 0.6.1 @@ -877,8 +877,8 @@ packages: graphql: optional: true - '@atproto/api@0.20.9': - resolution: {integrity: sha512-Yuw7Ewn+yMJZ8GskbuvI3lKPW65rsXic1xjFA2Dpq6H8WjVYs6xNZ31bkwtTYDDwjKIZcJmAVbAVgdfjo4T9iw==} + '@atproto/api@0.20.11': + resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==} engines: {node: '>=22'} '@atproto/common-web@0.5.0': @@ -9493,7 +9493,7 @@ snapshots: '@0no-co/graphql.web@1.2.0': {} - '@atproto/api@0.20.9': + '@atproto/api@0.20.11': dependencies: '@atproto/common-web': 0.5.0 '@atproto/lexicon': 0.7.1 diff --git a/src/components/Post/Embed/JoinRequestEmbed.tsx b/src/components/Post/Embed/JoinRequestEmbed.tsx index e020bb9333..a3cc08cac6 100644 --- a/src/components/Post/Embed/JoinRequestEmbed.tsx +++ b/src/components/Post/Embed/JoinRequestEmbed.tsx @@ -1,7 +1,11 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' -import {type ChatBskyGroupDefs} from '@atproto/api' +import {ChatBskyGroupDefs} from '@atproto/api' import {Trans} from '@lingui/react/macro' +import { + type ChatInvitePreview, + isKnownJoinLinkPreview, +} from '#/state/queries/join-links' import {atoms as a, useTheme} from '#/alf' import * as ChatInvite from '#/components/dms/ChatInvite' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' @@ -23,11 +27,12 @@ export function JoinRequestEmbed({ onOpen, }: { code?: string - preview?: ChatBskyGroupDefs.JoinLinkPreviewView + preview?: ChatInvitePreview style?: StyleProp onOpen?: () => void }) { - const resolvedCode = code ?? preview?.code + const resolvedCode = + code ?? (isKnownJoinLinkPreview(preview) ? preview.code : undefined) if (!resolvedCode) return null return ( @@ -73,7 +78,7 @@ export function JoinRequestEmbedBody({ ) } - if (!preview) { + if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) { return ( = 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) { + if (preview.memberCount >= preview.memberLimit) { canJoin = false icon = HandIcon label = l`This chat is full` diff --git a/src/components/dms/MessageItemInviteEmbed.tsx b/src/components/dms/MessageItemInviteEmbed.tsx index d85e7fd08a..79416057c9 100644 --- a/src/components/dms/MessageItemInviteEmbed.tsx +++ b/src/components/dms/MessageItemInviteEmbed.tsx @@ -3,6 +3,7 @@ import {useWindowDimensions, View} from 'react-native' import {type $Typed, type ChatBskyEmbedJoinLink} from '@atproto/api' import {useConvoActive} from '#/state/messages/convo' +import {isKnownJoinLinkPreview} from '#/state/queries/join-links' import {atoms as a, native, useTheme, web} from '#/alf' import * as ChatInvite from '#/components/dms/ChatInvite' import {MessageContextProvider} from './MessageContext' @@ -27,6 +28,11 @@ let MessageItemInviteEmbed = ({ const screen = useWindowDimensions() const convo = useConvoActive() + const code = isKnownJoinLinkPreview(embed.joinLinkPreview) + ? embed.joinLinkPreview.code + : undefined + if (!code) return null + return ( diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 745adc62b7..82b00645d2 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -121,7 +121,8 @@ function ProfileHeaderReady({ + numberOfLines={1} + emoji> {displayName} @@ -185,7 +186,8 @@ function GroupHeaderReady({ + numberOfLines={1} + emoji> {convo.details.name} diff --git a/src/components/dms/SystemMessageItem.tsx b/src/components/dms/SystemMessageItem.tsx index 9854255c84..7de6a0cd91 100644 --- a/src/components/dms/SystemMessageItem.tsx +++ b/src/components/dms/SystemMessageItem.tsx @@ -45,7 +45,8 @@ export function SystemMessageItem({ a.text_center, t.atoms.text_contrast_medium, {includeFontPadding: false, textAlignVertical: 'center'}, - ]}> + ]} + emoji> {text} diff --git a/src/components/intents/GroupChatJoinDialog.tsx b/src/components/intents/GroupChatJoinDialog.tsx index 8d235b9a07..dd3d8cb6e8 100644 --- a/src/components/intents/GroupChatJoinDialog.tsx +++ b/src/components/intents/GroupChatJoinDialog.tsx @@ -1,5 +1,6 @@ import {View} from 'react-native' import { + ChatBskyGroupDefs, ChatBskyGroupRequestJoin, ChatBskyGroupWithdrawJoinRequest, moderateProfile, @@ -211,7 +212,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { const joinLinkPreview = data.joinLinkPreviews[0] - if (!joinLinkPreview) { + if (!ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) { return ( <> @@ -253,12 +254,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { ? l`Request to join` : l`Join` let buttonColor: ButtonColor = 'primary' - if (joinLinkPreview.enabledStatus !== 'enabled') { - canJoin = false - ButtonIconImage = WarningIcon - buttonText = l`Chat invite link no longer available` - buttonColor = 'secondary' - } else if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) { + if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) { canJoin = false ButtonIconImage = HandIcon buttonText = l`This chat is full` @@ -311,10 +307,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { - + @@ -371,7 +364,7 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 6f92e2a1dd..40e6743b9a 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -9,6 +9,7 @@ import { AppBskyFeedPost, BlobRef, type BskyAgent, + ChatBskyGroupDefs, type ComAtprotoLabelDefs, type ComAtprotoRepoApplyWrites, type ComAtprotoRepoStrongRef, @@ -463,7 +464,10 @@ async function resolveMedia( }, } } - if (resolvedLink.type === 'chat-invite' && resolvedLink.view) { + if ( + resolvedLink.type === 'chat-invite' && + ChatBskyGroupDefs.isJoinLinkPreviewView(resolvedLink.view) + ) { return { $type: 'app.bsky.embed.external', external: { diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index ca0b342045..476c0f1a20 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -1,8 +1,7 @@ import { type AppBskyFeedDefs, type AppBskyGraphDefs, - type BskyAgent, - type ChatBskyGroupDefs, + type AtpAgent, type ComAtprotoRepoStrongRef, } from '@atproto/api' import {AtUri} from '@atproto/api' @@ -28,6 +27,7 @@ import { } from '#/lib/strings/url-helpers' import {type ComposerImage} from '#/state/gallery' import {createComposerImage} from '#/state/gallery' +import {type ChatInvitePreview} from '#/state/queries/join-links' import {type Gif} from '#/features/gifPicker/types' import {createGIFDescription} from '../gif-alt-text' @@ -77,7 +77,7 @@ type ResolvedChatInvite = { type: 'chat-invite' uri: string code: string - view?: ChatBskyGroupDefs.JoinLinkPreviewView + view?: ChatInvitePreview } export type ResolvedLink = @@ -95,7 +95,7 @@ export class EmbeddingDisabledError extends Error { } export async function resolveLink( - agent: BskyAgent, + agent: AtpAgent, uri: string, ): Promise { if (isShortLink(uri)) { @@ -217,7 +217,7 @@ export async function resolveLink( } export async function resolveGif( - agent: BskyAgent, + agent: AtpAgent, gif: Gif, ): Promise { const gifUrl = gif.media_formats.gif.url @@ -259,7 +259,7 @@ function getFileSlug(url: string | undefined): string | undefined { } async function resolveExternal( - agent: BskyAgent, + agent: AtpAgent, uri: string, ): Promise { const result = await getLinkMeta(agent, uri) diff --git a/src/screens/Messages/JoinRequest.tsx b/src/screens/Messages/JoinRequest.tsx index 1e23c3405b..64c898dc26 100644 --- a/src/screens/Messages/JoinRequest.tsx +++ b/src/screens/Messages/JoinRequest.tsx @@ -1,6 +1,6 @@ import {View} from 'react-native' import {ImageBackground} from 'expo-image' -import {moderateProfile} from '@atproto/api' +import {ChatBskyGroupDefs, moderateProfile} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -52,9 +52,7 @@ export function JoinRequest({setScreenState}: Props) { ? mobileDarkBg : mobileLightBg - const requiresApproval = data?.joinLinkPreviews[0]?.requireApproval - const requiresFollow = - data?.joinLinkPreviews[0]?.joinRule === 'followedByOwner' + const joinLinkPreview = data?.joinLinkPreviews[0] return ( @@ -69,7 +67,9 @@ export function JoinRequest({setScreenState}: Props) { a.justify_center, a.align_center, ]}> - {error ? ( + {error || + (data && + !ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) ? ( - {l`This invite link has expired`} + Chat invite link no longer available - ) : data && moderationOpts ? ( + ) : data && + moderationOpts && + ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview) ? ( - {data.joinLinkPreviews[0].memberCount}/ - {data.joinLinkPreviews[0].memberLimit} + {joinLinkPreview.memberCount}/ + {joinLinkPreview.memberLimit} @@ -144,7 +143,7 @@ export function JoinRequest({setScreenState}: Props) { a.font_bold, t.atoms.text, ]}> - {data.joinLinkPreviews[0].name} + {joinLinkPreview.name} @@ -168,17 +167,17 @@ export function JoinRequest({setScreenState}: Props) { By{' '} {createSanitizedDisplayName( - data.joinLinkPreviews[0].owner, + joinLinkPreview.owner, true, moderateProfile( - data.joinLinkPreviews[0].owner, + joinLinkPreview.owner, moderationOpts, ).ui('displayName'), )} @@ -192,7 +191,7 @@ export function JoinRequest({setScreenState}: Props) { t.atoms.text_contrast_medium, a.max_w_full, ]}> - {sanitizeHandle(data.joinLinkPreviews[0].owner.handle, '@')} + {sanitizeHandle(joinLinkPreview.owner.handle, '@')} - {requiresApproval + {joinLinkPreview.requireApproval ? l`Sign in to request access to this group chat.` : l`Sign in to accept invite.`}{' '} - {requiresFollow && + {joinLinkPreview.joinRule === 'followedByOwner' && l`Only people ${createSanitizedDisplayName( - data.joinLinkPreviews[0].owner, + joinLinkPreview.owner, true, - moderateProfile( - data.joinLinkPreviews[0].owner, - moderationOpts, - ).ui('displayName'), + moderateProfile(joinLinkPreview.owner, moderationOpts).ui( + 'displayName', + ), )} follows can join.`} diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index 21503ce8fa..c38141ce9d 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -4,6 +4,7 @@ import { AppBskyFeedPost, AppBskyRichtextFacet, AtUri, + ChatBskyGroupDefs, moderatePost, RichText as RichTextAPI, } from '@atproto/api' @@ -30,6 +31,7 @@ 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 {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Loader} from '#/components/Loader' import * as MediaPreview from '#/components/MediaPreview' import {ContentHider} from '#/components/moderation/ContentHider' @@ -52,12 +54,12 @@ export function useMessageEmbed() { const navigation = useNavigation() const embedFromParams = route.params.embed - const [embed, setEmbedState] = useState( + const [embed, setEmbed] = useState( embedFromParams ? {type: 'post', uri: embedFromParams} : undefined, ) if (embedFromParams && embed?.type !== 'post') { - setEmbedState({type: 'post', uri: embedFromParams}) + setEmbed({type: 'post', uri: embedFromParams}) } return { @@ -68,7 +70,7 @@ export function useMessageEmbed() { // 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: ''}) - setEmbedState(undefined) + setEmbed(undefined) return } @@ -77,7 +79,7 @@ export function useMessageEmbed() { if (isBskyChatInviteUrl(embedUrl)) { const code = getChatInviteCodeFromUrl(embedUrl) if (code) { - setEmbedState({type: 'invite', code}) + setEmbed({type: 'invite', code}) } return } @@ -86,7 +88,7 @@ export function useMessageEmbed() { 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}) + setEmbed({type: 'post', uri}) } }, [embedFromParams, navigation], @@ -314,11 +316,19 @@ function MessageInputInviteEmbedBody() { ) } - if (!preview) { + if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) { return ( - - - Could not load invite + + + + Chat invite link no longer available ) diff --git a/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx b/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx index 2129182632..8428b1a4b2 100644 --- a/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx +++ b/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx @@ -94,7 +94,8 @@ export function MessagesListGroupInfoPanel({ /> {convo.details.name ? ( + style={[a.text_2xl, a.font_bold, a.mt_lg, a.px_xl, a.text_center]} + emoji> {convo.details.name} ) : null} @@ -107,7 +108,8 @@ export function MessagesListGroupInfoPanel({ a.text_sm, t.atoms.text_contrast_high, showButtons ? null : a.mb_4xl, - ]}> + ]} + emoji> {names} ) : null} diff --git a/src/screens/Messages/components/MessagesListInfoPanel.tsx b/src/screens/Messages/components/MessagesListInfoPanel.tsx index b30bd6fe9d..e1e620adee 100644 --- a/src/screens/Messages/components/MessagesListInfoPanel.tsx +++ b/src/screens/Messages/components/MessagesListInfoPanel.tsx @@ -60,7 +60,8 @@ export function MessagesListInfoPanel({ ]}> + numberOfLines={1} + emoji> {displayName} diff --git a/src/screens/Messages/components/OutgoingRequestListItem.tsx b/src/screens/Messages/components/OutgoingRequestListItem.tsx index 8bd7ba78d0..2f8804092d 100644 --- a/src/screens/Messages/components/OutgoingRequestListItem.tsx +++ b/src/screens/Messages/components/OutgoingRequestListItem.tsx @@ -76,7 +76,14 @@ export function OutgoingRequestListItem({ moderationOpts={moderationOpts} /> - + - - + {convoView.viewer?.requestedAt ? ( + {({timeElapsed}) => ( )} - + ) : null} + | $Typed + | $Typed + +/** + * The full open-union shape, including the `{$type: string}` fallback for + * future variants. + */ +export type ChatInvitePreview = KnownChatInvitePreview | {$type: string} + +/** + * Narrows a preview to one of the three known variants, filtering out the + * `{$type: string}` open-union fallback for unrecognized future shapes. + */ +export function isKnownJoinLinkPreview( + preview: unknown, +): preview is KnownChatInvitePreview { + return ( + ChatBskyGroupDefs.isJoinLinkPreviewView(preview) || + ChatBskyGroupDefs.isDisabledJoinLinkPreviewView(preview) || + ChatBskyGroupDefs.isInvalidJoinLinkPreviewView(preview) + ) +} + const joinLinkPreviewQueryKeyRoot = 'join-link-preview' export const createJoinLinkPreviewQueryKey = (args: { @@ -22,6 +53,29 @@ export const createJoinLinkPreviewQueryKey = (args: { persistedVersion: 1, }) +/** + * Invalidate any join link preview queries whose `codes` include the given + * code. Use this when a link's state changes (e.g. it's disabled) so cached + * previews refetch and reflect the new state. + */ +export function invalidateJoinLinkPreviewsForCode( + queryClient: QueryClient, + code: string, +) { + return queryClient.invalidateQueries({ + predicate: query => { + const [root, args] = query.queryKey as Partial< + StructuredQueryKey<{codes?: string[]}> + > + return ( + root === joinLinkPreviewQueryKeyRoot && + Array.isArray(args?.codes) && + args.codes.includes(code) + ) + }, + }) +} + async function fetchJoinLinkPreviews({ agent, codes, @@ -104,7 +158,7 @@ export function useGetJoinLinkPreview() { }: { code: string hasSession: boolean - }): Promise => { + }): Promise => { try { const data = await queryClient.fetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}), @@ -112,7 +166,8 @@ export function useGetJoinLinkPreview() { fetchJoinLinkPreviews({agent, codes: [code], hasSession}), staleTime: STALE.SECONDS.FIFTEEN, }) - return data.joinLinkPreviews[0] + const found = data.joinLinkPreviews[0] + return isKnownJoinLinkPreview(found) ? found : undefined } catch (error) { logger.error('Failed to fetch join link preview', {safeMessage: error}) return undefined diff --git a/src/state/queries/messages/disable-join-link.ts b/src/state/queries/messages/disable-join-link.ts index 522b5939eb..c143feb6cc 100644 --- a/src/state/queries/messages/disable-join-link.ts +++ b/src/state/queries/messages/disable-join-link.ts @@ -6,6 +6,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' +import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' import {useAgent} from '#/state/session' import { rollbackConvoOptimistic, @@ -59,6 +60,7 @@ export function useDisableJoinLink( } }) } + void invalidateJoinLinkPreviewsForCode(queryClient, data.joinLink.code) onSuccess?.(data) }, onError: (e, _variables, context) => { diff --git a/src/state/queries/messages/enable-join-link.ts b/src/state/queries/messages/enable-join-link.ts index a2ba505f1d..4febc85441 100644 --- a/src/state/queries/messages/enable-join-link.ts +++ b/src/state/queries/messages/enable-join-link.ts @@ -3,6 +3,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' +import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' import {useAgent} from '#/state/session' import { rollbackConvoOptimistic, @@ -56,6 +57,7 @@ export function useEnableJoinLink( } }) } + void invalidateJoinLinkPreviewsForCode(queryClient, data.joinLink.code) onSuccess?.(data) }, onError: (e, _variables, context) => { diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 25ed831c3c..e1d68acea6 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -52,6 +52,7 @@ import { type AppBskyUnspeccedGetPostThreadV2, AtUri, type BskyAgent, + ChatBskyGroupDefs, type RichText, } from '@atproto/api' import {plural} from '@lingui/core/macro' @@ -886,7 +887,9 @@ export const ComposePost = ({ })), }) const hasUnavailableChatInvite = linkQueries.some( - q => q.data?.type === 'chat-invite' && !q.data.view, + q => + q.data?.type === 'chat-invite' && + !ChatBskyGroupDefs.isJoinLinkPreviewView(q.data.view), ) const canPost = From e339a2a18cf22fde14c871773ca3336dccc4941d Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Tue, 9 Jun 2026 11:24:34 -0400 Subject: [PATCH 09/23] version number (#10815) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3040ce2e90..84324a70de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.123.0", + "version": "1.124.0", "private": true, "engines": { "node": ">=24.15.0" From fb4133cdc76eb6e4aac098bdc15c43c5e39a2916 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jun 2026 18:33:04 +0300 Subject: [PATCH 10/23] [Chat] Filter out groups from badge counts when AA'd (#10816) --- src/screens/Messages/ChatList.tsx | 7 +++++-- src/state/queries/messages/list-conversations.tsx | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index ab622f708a..f08d155e06 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -1,7 +1,7 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {View} from 'react-native' import {useAnimatedRef} from 'react-native-reanimated' -import {type ChatBskyActorGetStatus, type ChatBskyConvoDefs} from '@atproto/api' +import {type ChatBskyActorGetStatus, ChatBskyConvoDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' @@ -475,7 +475,10 @@ export function Header({ convo => !leftConvos.includes(convo.id) && !convo.muted && - convo.members.every(member => member.handle !== 'missing.invalid'), + convo.members.every(member => member.handle !== 'missing.invalid') && + (ChatBskyConvoDefs.isGroupConvo(convo.kind) + ? !aa.flags.groupChatDisabled + : true), ) ?? [] const openChatControl = useCallback(() => { diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx index 0bc2f69997..f7ad22fbae 100644 --- a/src/state/queries/messages/list-conversations.tsx +++ b/src/state/queries/messages/list-conversations.tsx @@ -21,6 +21,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAgent, useSession} from '#/state/session' import {parseConvoView} from '#/components/dms/util' import {useAgeAssurance} from '#/ageAssurance' +import {type AgeAssuranceFlags} from '#/ageAssurance/types' import * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import {useLeftConvos} from './leave-conversation' @@ -679,6 +680,7 @@ export function useUnreadMessageCount() { const {currentAccount} = useSession() const {accepted, request} = useListConvos() const moderationOpts = useModerationOpts() + const aa = useAgeAssurance() return useMemo<{ count: number @@ -690,12 +692,14 @@ export function useUnreadMessageCount() { currentAccount?.did, currentConvoId, moderationOpts, + aa.flags, ) const requestCount = calculateCount( request, currentAccount?.did, currentConvoId, moderationOpts, + aa.flags, ) if (acceptedCount > 0) { const total = acceptedCount + Math.min(requestCount, 1) @@ -726,6 +730,7 @@ function calculateCount( currentAccountDid: string | undefined, currentConvoId: string | undefined, moderationOpts: ModerationOpts | undefined, + flags: AgeAssuranceFlags, ) { return ( convos @@ -735,6 +740,8 @@ function calculateCount( if (!convo || !moderationOpts) return acc + if (convo.kind === 'group' && flags.groupChatDisabled) return acc + const shouldIgnore = convo.view.muted || !convo.primaryMember || From be309470891c1ae92c8643dfeeca82919739b48d Mon Sep 17 00:00:00 2001 From: Michael Black Date: Tue, 9 Jun 2026 11:10:33 -0500 Subject: [PATCH 11/23] bskyweb: extract gallery media for SEO (#10813) --- bskyweb/cmd/bskyweb/jsonld.go | 37 ++++++- bskyweb/cmd/bskyweb/jsonld_test.go | 155 +++++++++++++++++++++++++++++ bskyweb/cmd/bskyweb/render_test.go | 34 +++++++ bskyweb/go.mod | 2 +- bskyweb/go.sum | 4 +- 5 files changed, 226 insertions(+), 6 deletions(-) diff --git a/bskyweb/cmd/bskyweb/jsonld.go b/bskyweb/cmd/bskyweb/jsonld.go index 7bc44a02f3..8d06b71262 100644 --- a/bskyweb/cmd/bskyweb/jsonld.go +++ b/bskyweb/cmd/bskyweb/jsonld.go @@ -136,9 +136,9 @@ func bskyProfileURL(handle string) string { return fmt.Sprintf("https://bsky.app/profile/%s", handle) } -// extractPostMedia returns thumbnail URLs for the post's image or video -// embed, byte-identical to what we put in og:image. Callers derive -// thumbnailUrl from urls[0]. +// extractPostMedia returns thumbnail URLs for the post's image, gallery, +// or video embed, byte-identical to what we put in og:image. Callers +// derive thumbnailUrl from urls[0]. func extractPostMedia(pv *appbsky.FeedDefs_PostView, embedHidden bool) []string { if pv == nil || pv.Embed == nil || embedHidden { return nil @@ -147,6 +147,9 @@ func extractPostMedia(pv *appbsky.FeedDefs_PostView, embedHidden bool) []string if pv.Embed.EmbedImages_View != nil { return imageThumbs(pv.Embed.EmbedImages_View.Images) } + if pv.Embed.EmbedGallery_View != nil { + return galleryThumbs(pv.Embed.EmbedGallery_View.Items) + } if pv.Embed.EmbedVideo_View != nil && pv.Embed.EmbedVideo_View.Thumbnail != nil { return []string{*pv.Embed.EmbedVideo_View.Thumbnail} } @@ -155,6 +158,9 @@ func extractPostMedia(pv *appbsky.FeedDefs_PostView, embedHidden bool) []string if media.EmbedImages_View != nil { return imageThumbs(media.EmbedImages_View.Images) } + if media.EmbedGallery_View != nil { + return galleryThumbs(media.EmbedGallery_View.Items) + } if media.EmbedVideo_View != nil && media.EmbedVideo_View.Thumbnail != nil { return []string{*media.EmbedVideo_View.Thumbnail} } @@ -174,6 +180,31 @@ func imageThumbs(images []*appbsky.EmbedImages_ViewImage) []string { return urls } +// galleryThumbs returns the thumbnail URLs of image items in a gallery +// embed, or nil if empty. Items_Elem is a union; non-image variants and +// nil entries are skipped so future gallery item types don't break SEO +// extraction. Empty Thumbnail strings are also skipped to avoid emitting +// if the appview ever returns one. +func galleryThumbs(items []*appbsky.EmbedGallery_View_Items_Elem) []string { + if len(items) == 0 { + return nil + } + urls := make([]string, 0, len(items)) + for _, item := range items { + if item == nil || item.EmbedGallery_ViewImage == nil { + continue + } + if item.EmbedGallery_ViewImage.Thumbnail == "" { + continue + } + urls = append(urls, item.EmbedGallery_ViewImage.Thumbnail) + } + if len(urls) == 0 { + return nil + } + return urls +} + // extractQuotedPostURL returns the canonical URL of a quoted post, or "" // if the embed is blocked / not-found / detached / a non-post record. func extractQuotedPostURL(pv *appbsky.FeedDefs_PostView) string { diff --git a/bskyweb/cmd/bskyweb/jsonld_test.go b/bskyweb/cmd/bskyweb/jsonld_test.go index f14cdb6f78..5c7f1c2bb7 100644 --- a/bskyweb/cmd/bskyweb/jsonld_test.go +++ b/bskyweb/cmd/bskyweb/jsonld_test.go @@ -72,6 +72,60 @@ func withImages(thumbs ...string) func(*appbsky.FeedDefs_PostView) { } } +// withGallery adds an app.bsky.embed.gallery view with image items. +func withGallery(thumbs ...string) func(*appbsky.FeedDefs_PostView) { + return func(pv *appbsky.FeedDefs_PostView) { + var items []*appbsky.EmbedGallery_View_Items_Elem + for _, t := range thumbs { + items = append(items, &appbsky.EmbedGallery_View_Items_Elem{ + EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{ + Thumbnail: t, + Fullsize: t + "_full", + }, + }) + } + pv.Embed = &appbsky.FeedDefs_PostView_Embed{ + EmbedGallery_View: &appbsky.EmbedGallery_View{Items: items}, + } + } +} + +// withRecordWithMediaGallery adds a record-with-media embed whose media slot +// is an app.bsky.embed.gallery view. +func withRecordWithMediaGallery(qHandle, qDid, qRkey string, thumbs ...string) func(*appbsky.FeedDefs_PostView) { + return func(pv *appbsky.FeedDefs_PostView) { + var items []*appbsky.EmbedGallery_View_Items_Elem + for _, t := range thumbs { + items = append(items, &appbsky.EmbedGallery_View_Items_Elem{ + EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{ + Thumbnail: t, + Fullsize: t + "_full", + }, + }) + } + pv.Embed = &appbsky.FeedDefs_PostView_Embed{ + EmbedRecordWithMedia_View: &appbsky.EmbedRecordWithMedia_View{ + Record: &appbsky.EmbedRecord_View{ + Record: &appbsky.EmbedRecord_View_Record{ + EmbedRecord_ViewRecord: &appbsky.EmbedRecord_ViewRecord{ + Uri: "at://" + qDid + "/app.bsky.feed.post/" + qRkey, + Cid: "bafy-quoted", + Author: &appbsky.ActorDefs_ProfileViewBasic{ + Did: qDid, + Handle: qHandle, + }, + IndexedAt: "2024-01-01T00:00:00Z", + }, + }, + }, + Media: &appbsky.EmbedRecordWithMedia_View_Media{ + EmbedGallery_View: &appbsky.EmbedGallery_View{Items: items}, + }, + }, + } + } +} + // withVideo adds a video embed with a thumbnail. func withVideo(thumb string) func(*appbsky.FeedDefs_PostView) { return func(pv *appbsky.FeedDefs_PostView) { @@ -299,6 +353,88 @@ func TestBuildPostJSONLD_WithImages(t *testing.T) { } } +func TestBuildPostJSONLD_WithGallery(t *testing.T) { + thumb1 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g1@jpeg" + thumb2 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g2@jpeg" + thumb3 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g3@jpeg" + pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "gallery", withGallery(thumb1, thumb2, thumb3)) + out, err := buildPostJSONLD(pv, nil, "https://bsky.app/profile/alice.bsky.social/post/abc123", hideEmbedLabels, hideReplyLabels) + if err != nil { + t.Fatal(err) + } + main := unmarshalLD(t, out)["mainEntity"].(map[string]any) + imgs, ok := main["image"].([]any) + if !ok { + t.Fatalf("image should be array, got %T", main["image"]) + } + if len(imgs) != 3 { + t.Errorf("expected 3 gallery images, got %d", len(imgs)) + } + if imgs[0] != thumb1 || imgs[1] != thumb2 || imgs[2] != thumb3 { + t.Errorf("gallery image[] order wrong: %v", imgs) + } + if main["thumbnailUrl"] != thumb1 { + t.Errorf("thumbnailUrl should equal image[0] (Google byte-equality requirement), got %v", main["thumbnailUrl"]) + } +} + +// Gallery in the media slot of a record-with-media embed should still +// produce og:image / JSON-LD image[]. Quote-post URL still emits +// alongside via isBasedOn. +func TestBuildPostJSONLD_GalleryInRecordWithMedia(t *testing.T) { + thumb := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g@jpeg" + pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "quote+gallery", + withRecordWithMediaGallery("bob.example.com", "did:plc:bob", "xyz", thumb)) + out, err := buildPostJSONLD(pv, nil, "u", hideEmbedLabels, hideReplyLabels) + if err != nil { + t.Fatal(err) + } + main := unmarshalLD(t, out)["mainEntity"].(map[string]any) + imgs, ok := main["image"].([]any) + if !ok || len(imgs) != 1 || imgs[0] != thumb { + t.Errorf("expected single gallery thumb in image[], got %v", main["image"]) + } + if main["thumbnailUrl"] != thumb { + t.Errorf("thumbnailUrl wrong: %v", main["thumbnailUrl"]) + } + if main["isBasedOn"] != "https://bsky.app/profile/bob.example.com/post/xyz" { + t.Errorf("isBasedOn should still emit for record-with-media gallery, got %v", main["isBasedOn"]) + } +} + +// Forward-compat: nil items, unknown-variant union elements, and empty +// Thumbnail strings must be skipped, not panic or leak as . Unknown variants are dropped silently +// so older deploys keep working when new gallery item types ship. +func TestExtractPostMedia_GallerySkipsUnknownItems(t *testing.T) { + thumb := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g@jpeg" + pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "gallery") + pv.Embed = &appbsky.FeedDefs_PostView_Embed{ + EmbedGallery_View: &appbsky.EmbedGallery_View{ + Items: []*appbsky.EmbedGallery_View_Items_Elem{ + nil, + {}, // empty union, no variant set + {EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{Thumbnail: ""}}, // empty Thumbnail + {EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{Thumbnail: thumb}}, + }, + }, + } + got := extractPostMedia(pv, false) + if len(got) != 1 || got[0] != thumb { + t.Errorf("expected single thumb, got %v", got) + } + + // All-nil / all-unknown gallery should produce no thumbs (not [""]). + pv.Embed = &appbsky.FeedDefs_PostView_Embed{ + EmbedGallery_View: &appbsky.EmbedGallery_View{ + Items: []*appbsky.EmbedGallery_View_Items_Elem{nil, {}}, + }, + } + if got := extractPostMedia(pv, false); got != nil { + t.Errorf("expected nil for empty/unknown-only gallery, got %v", got) + } +} + func TestBuildPostJSONLD_WithVideo(t *testing.T) { thumb := "https://cdn.bsky.app/img/video_thumbnail/plain/did:plc:alice/v@jpeg" pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "watch", withVideo(thumb)) @@ -361,6 +497,25 @@ func TestBuildPostJSONLD_HiddenEmbed(t *testing.T) { } } +// Symmetric guard for the gallery extraction path. Functionally redundant +// with the early-return at the top of extractPostMedia, but exists so the +// hide-embed contract is asserted directly against the gallery branch - +// catches anyone who later moves the embedHidden check inside an +// embed-shape branch. +func TestBuildPostJSONLD_HiddenEmbed_Gallery(t *testing.T) { + thumb := "https://cdn.bsky.app/img/g@jpeg" + pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "nsfw", + withGallery(thumb), withSelfLabel("porn")) + out, _ := buildPostJSONLD(pv, nil, "u", hideEmbedLabels, hideReplyLabels) + main := unmarshalLD(t, out)["mainEntity"].(map[string]any) + if _, present := main["image"]; present { + t.Errorf("hidden-embed gallery post should not emit image") + } + if _, present := main["thumbnailUrl"]; present { + t.Errorf("hidden-embed gallery post should not emit thumbnailUrl") + } +} + func TestBuildPostJSONLD_TextEscaping(t *testing.T) { // Includes ", \, newline, , and a unicode char. tricky := "hello \"world\" \\ <\\>\n 🎉" diff --git a/bskyweb/cmd/bskyweb/render_test.go b/bskyweb/cmd/bskyweb/render_test.go index f7c9edd677..30b27a53d5 100644 --- a/bskyweb/cmd/bskyweb/render_test.go +++ b/bskyweb/cmd/bskyweb/render_test.go @@ -98,6 +98,40 @@ func TestRenderPost_OGImageMatchesJSONLD(t *testing.T) { } } +// Gallery posts must hit the same og:image / JSON-LD image[] byte-equality +// contract that legacy images posts do. Regression guard for the +// app.bsky.embed.gallery extraction path. +func TestRenderPost_OGImageMatchesJSONLD_Gallery(t *testing.T) { + thumb1 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g1@jpeg" + thumb2 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g2@jpeg" + pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "gallery", withGallery(thumb1, thumb2)) + thumbs := extractPostMedia(pv, false) + ld, _ := buildPostJSONLD(pv, nil, "https://bsky.app/profile/alice.bsky.social/post/abc123", hideEmbedLabels, hideReplyLabels) + html := renderTemplate(t, "post.html", pongo2.Context{ + "postView": pv, + "requestURI": "https://bsky.app/profile/alice.bsky.social/post/abc123", + "canonicalURL": "https://bsky.app/profile/alice.bsky.social/post/abc123", + "postJSONLD": ld, + "imgThumbUrls": thumbs, + }) + + if !strings.Contains(html, ``) { + t.Errorf("og:image[0] not found in rendered HTML for gallery post") + } + if !strings.Contains(html, ``) { + t.Errorf("og:image[1] not found in rendered HTML for gallery post") + } + body := extractJSONLD(t, html) + var parsed map[string]any + _ = json.Unmarshal([]byte(body), &parsed) + main := parsed["mainEntity"].(map[string]any) + imgs := main["image"].([]any) + if len(imgs) != 2 || imgs[0] != thumb1 || main["thumbnailUrl"] != thumb1 { + t.Errorf("JSON-LD image strings drifted from og:image for gallery; image=%v thumbnailUrl=%v", + imgs, main["thumbnailUrl"]) + } +} + func TestRenderPost_FallsBackToCanonicalizeFilter(t *testing.T) { // Without canonicalURL, the template falls back to requestURI|canonicalize_url. pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "hi") diff --git a/bskyweb/go.mod b/bskyweb/go.mod index 50ab9a162a..e25349e947 100644 --- a/bskyweb/go.mod +++ b/bskyweb/go.mod @@ -3,7 +3,7 @@ module github.com/bluesky-social/social-app/bskyweb go 1.26 require ( - github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0 + github.com/bluesky-social/indigo v0.0.0-20260605210604-af2fec94f34c github.com/flosch/pongo2/v6 v6.0.0 github.com/ipfs/go-log v1.0.5 github.com/joho/godotenv v1.5.1 diff --git a/bskyweb/go.sum b/bskyweb/go.sum index 235e37f521..d9b4b0141a 100644 --- a/bskyweb/go.sum +++ b/bskyweb/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0 h1:eijBaF59A5c+kPqufH7YO1GOqDMkyUhtM9P9aAWtfJY= -github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo= +github.com/bluesky-social/indigo v0.0.0-20260605210604-af2fec94f34c h1:Jr82+1HUmwwZzDpt/eeU4sieya27iXjuPMdXZkOXoBc= +github.com/bluesky-social/indigo v0.0.0-20260605210604-af2fec94f34c/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= From 986cf62da12db942a2f1493db209ed61fbc0b58f Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:35:49 -0700 Subject: [PATCH 12/23] Fix text truncating in image menu on iOS (#10802) --- src/components/Lightbox/chrome/ImageMenu.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/Lightbox/chrome/ImageMenu.tsx b/src/components/Lightbox/chrome/ImageMenu.tsx index 8daa9a5b84..92b5c47749 100644 --- a/src/components/Lightbox/chrome/ImageMenu.tsx +++ b/src/components/Lightbox/chrome/ImageMenu.tsx @@ -10,7 +10,7 @@ import Animated, { } from 'react-native-reanimated' import {useLingui} from '@lingui/react/macro' -import {android, atoms as a, ios} from '#/alf' +import {atoms as a} from '#/alf' import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight' import {type Props as IconProps} from '#/components/icons/common' import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid' @@ -25,7 +25,6 @@ type Props = { type Anchor = {x: number; y: number; width: number; height: number} -const MENU_WIDTH = 160 const GAP = 6 const CARD_BG = '#000000' const CARD_BORDER = '#232e3e' @@ -124,9 +123,8 @@ function MenuCard({ Date: Tue, 9 Jun 2026 09:36:35 -0700 Subject: [PATCH 13/23] Display toast after reacting emoji reaction limit (#10799) --- eslint-suppressions.json | 5 ----- src/components/dms/ActionsWrapper.web.tsx | 15 ++++++++++++++- src/components/dms/MessageContextMenu.tsx | 15 ++++++++++++++- src/components/dms/MessageItem.tsx | 2 +- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 59b062cc7b..559f1e564d 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -707,11 +707,6 @@ "count": 1 } }, - "src/components/dms/MessageItem.tsx": { - "@typescript-eslint/no-misused-promises": { - "count": 1 - } - }, "src/components/forms/DateField/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/src/components/dms/ActionsWrapper.web.tsx b/src/components/dms/ActionsWrapper.web.tsx index b89712bd1f..9ea12d7f9e 100644 --- a/src/components/dms/ActionsWrapper.web.tsx +++ b/src/components/dms/ActionsWrapper.web.tsx @@ -1,8 +1,10 @@ import {useCallback, useRef, useState} from 'react' import {Pressable, View} from 'react-native' import {type ChatBskyConvoDefs, type ModerationOpts} from '@atproto/api' +import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' +import {EMOJI_REACTION_LIMIT} from '#/lib/constants' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useConvoActive} from '#/state/messages/convo' import {useSession} from '#/state/session' @@ -72,7 +74,18 @@ export function ActionsWrapper({ .removeReaction(message.id, emoji) .catch(() => Toast.show(l`Failed to remove emoji reaction`)) } else { - if (hasReachedReactionLimit(message, currentAccount?.did)) return + if (hasReachedReactionLimit(message, currentAccount?.did)) { + Toast.show( + l`You cannot add more than ${plural(EMOJI_REACTION_LIMIT, { + one: '# emoji reaction', + other: '# emoji reactions', + })}`, + { + type: 'info', + }, + ) + return + } convo.addReaction(message.id, emoji).catch(() => Toast.show(l`Failed to add emoji reaction`, { type: 'error', diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx index bc77c5bd79..7cd4febd5f 100644 --- a/src/components/dms/MessageContextMenu.tsx +++ b/src/components/dms/MessageContextMenu.tsx @@ -6,8 +6,10 @@ import { type ModerationOpts, RichText, } from '@atproto/api' +import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' +import {EMOJI_REACTION_LIMIT} from '#/lib/constants' import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' @@ -98,7 +100,18 @@ export let MessageContextMenu = ({ .removeReaction(message.id, emoji) .catch(() => Toast.show(l`Failed to remove emoji reaction`)) } else { - if (hasReachedReactionLimit(message, currentAccount?.did)) return + if (hasReachedReactionLimit(message, currentAccount?.did)) { + Toast.show( + l`You cannot add more than ${plural(EMOJI_REACTION_LIMIT, { + one: '# emoji reaction', + other: '# emoji reactions', + })}`, + { + type: 'info', + }, + ) + return + } convo.addReaction(message.id, emoji).catch(() => Toast.show(l`Failed to add emoji reaction`, { type: 'error', diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index f4f3cdacc3..e4081d590a 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -641,7 +641,7 @@ function BlockedPlaceholder({ {}} cta={l`Okay`} color="primary" /> {profile.viewer?.blocking && !profile.viewer.blockingByList && ( queueUnblock()} + onPress={() => void queueUnblock()} cta={l`Unblock`} color="secondary" /> From 3fdfebfe29c070f3687ea9980c44db87ed0f757b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jun 2026 20:15:03 +0300 Subject: [PATCH 14/23] [Chat] Surface blocked members to top of group member list for owner (#10726) Co-authored-by: Claude Opus 4.8 (1M context) --- .../Messages/ConversationSettings/Member.tsx | 35 ++++++++++++++++++- .../ConversationSettings/MemberMenu.tsx | 10 ++++-- .../Messages/ConversationSettings/index.tsx | 7 ++++ .../Messages/ConversationSettings/prompts.tsx | 24 +++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/screens/Messages/ConversationSettings/Member.tsx b/src/screens/Messages/ConversationSettings/Member.tsx index 9786e69f55..20cc0fee52 100644 --- a/src/screens/Messages/ConversationSettings/Member.tsx +++ b/src/screens/Messages/ConversationSettings/Member.tsx @@ -7,18 +7,22 @@ import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-disp import {logger} from '#/logger' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useRemoveFromGroupChat} from '#/state/queries/messages/remove-from-group' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useRequireAuth, useSession} from '#/state/session' import {atoms as a, native, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import { type ConvoWithDetails, type GroupConvoMember, } from '#/components/dms/util' import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' +import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {MemberMenu} from './MemberMenu' +import {RemoveMemberPrompt} from './prompts' import {StatusBadge} from './StatusBadge' import {SubtleHoverWrapper} from './SubtleHoverWrapper' @@ -45,6 +49,14 @@ export function Member({ const [queueFollow] = useProfileFollowMutationQueue(profile, 'GroupChat') const requireAuth = useRequireAuth() + const removeMemberPrompt = Prompt.usePromptControl() + const {mutate: removeMembers} = useRemoveFromGroupChat(convo.view.id, { + onError: e => { + logger.error('Failed to remove group chat member', {message: e}) + Toast.show(l`Failed to remove group chat member`, {type: 'error'}) + }, + }) + const isFollowing = !!profile.viewer?.following const handleFollow = () => { @@ -101,6 +113,9 @@ export function Member({ )}` : l`Added by invite link` + // Surface a prominent remove button to the owner for blocked members. + const showRemoveButton = isOwner && !isSelf && !!isBlockedOrBlocking(profile) + return ( @@ -137,7 +152,17 @@ export function Member({ - {isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : ( + {showRemoveButton ? ( + + ) : isSelf || isFollowing || isBlockedOrBlocking(profile) ? null : ( + {/* Mounted outside the showRemoveButton conditional: confirming the + prompt optimistically drops this row, so gating the prompt on the + button would unmount it mid-close and race the dismiss animation. */} + removeMembers({members: [profile.did]})} + /> ) } diff --git a/src/screens/Messages/ConversationSettings/MemberMenu.tsx b/src/screens/Messages/ConversationSettings/MemberMenu.tsx index aee0cbf54e..2d36bc115f 100644 --- a/src/screens/Messages/ConversationSettings/MemberMenu.tsx +++ b/src/screens/Messages/ConversationSettings/MemberMenu.tsx @@ -26,7 +26,7 @@ import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' -import {BlockMemberPrompt} from './prompts' +import {BlockMemberPrompt, RemoveMemberPrompt} from './prompts' import {StatusBadge} from './StatusBadge' export function MemberMenu({ @@ -50,6 +50,7 @@ export function MemberMenu({ const requireEmailVerification = useRequireEmailVerification() const blockMemberPrompt = Prompt.usePromptControl() + const removeMemberPrompt = Prompt.usePromptControl() const [menuDidOpen, setMenuDidOpen] = useState(false) const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did, { @@ -227,7 +228,7 @@ export function MemberMenu({ removeMembers({members: [profile.did]})}> + onPress={removeMemberPrompt.open}> Remove from chat @@ -241,6 +242,11 @@ export function MemberMenu({ control={blockMemberPrompt} onConfirm={() => void handleBlockMember()} /> + removeMembers({members: [profile.did]})} + /> ) } diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index b4c7e1ce03..2aca54dfe0 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -11,6 +11,7 @@ import {useNavigation} from '@react-navigation/native' import {HITSLOP_10} from '#/lib/constants' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import { type CommonNavigatorParams, type NativeStackScreenProps, @@ -214,6 +215,12 @@ function GroupSettings({ const bIsSelf = b.did === currentAccount?.did if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1 if (aIsSelf !== bIsSelf) return aIsSelf ? -1 : 1 + // Surface blocked members to the owner so they can be removed. + if (isOwner) { + const aBlocked = !!isBlockedOrBlocking(a) + const bBlocked = !!isBlockedOrBlocking(b) + if (aBlocked !== bBlocked) return aBlocked ? -1 : 1 + } return 0 }) diff --git a/src/screens/Messages/ConversationSettings/prompts.tsx b/src/screens/Messages/ConversationSettings/prompts.tsx index 2eb293bdef..0c3ba2e8ca 100644 --- a/src/screens/Messages/ConversationSettings/prompts.tsx +++ b/src/screens/Messages/ConversationSettings/prompts.tsx @@ -151,6 +151,30 @@ export function LeaveAndLockChatPrompt({ ) } +export function RemoveMemberPrompt({ + control, + displayName, + onConfirm, +}: { + control: Dialog.DialogOuterProps['control'] + displayName: string + onConfirm: () => void +}) { + const {t: l} = useLingui() + + return ( + + ) +} + export function BlockMemberPrompt({ control, onConfirm, From df8c731f1d1b5fd1bbb5e9a48f7aa7b2a163c36d Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:16:00 -0700 Subject: [PATCH 15/23] Fix back button behavior on conversation settings screen (#10801) Co-authored-by: Samuel Newman --- .../Messages/ConversationSettings/index.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 2aca54dfe0..0cade00dc3 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -55,6 +55,7 @@ import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {IS_WEB} from '#/env' import * as bsky from '#/types/bsky' import {InviteLinkDialog} from '../components/InviteLinkDialog' import {AddMembersLink} from './AddMembersLink' @@ -87,12 +88,23 @@ type Props = NativeStackScreenProps< > export function MessagesConversationSettingsScreen({route}: Props) { + const navigation = useNavigation() + const convoId = route.params.conversation return ( - + { + if (IS_WEB && !navigation.canGoBack()) { + evt.preventDefault() + navigation.navigate('MessagesConversation', { + conversation: convoId, + }) + } + }} + /> Group chat settings From 994e27ded1533b8eb8b4cc2515920a135da7c80e Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:45:50 -0700 Subject: [PATCH 16/23] Don't suggest users who can't be messaged/added (#10820) --- src/components/dms/AddMembersFlow.tsx | 7 +------ src/components/dms/InitiateChatFlow.tsx | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/dms/AddMembersFlow.tsx b/src/components/dms/AddMembersFlow.tsx index ae2945d2ce..0c0b171e02 100644 --- a/src/components/dms/AddMembersFlow.tsx +++ b/src/components/dms/AddMembersFlow.tsx @@ -209,6 +209,7 @@ export function AddMembersFlow({ if (follows) { for (const page of follows.pages) { for (const profile of page.follows) { + if (!canBeAddedToGroup(profile)) continue _items.push({ type: 'profile', key: profile.did, @@ -216,12 +217,6 @@ export function AddMembersFlow({ }) } } - - _items.sort(item => { - return item.type === 'profile' && canBeAddedToGroup(item.profile) - ? -1 - : 1 - }) } else { for (let i = 0; i < 10; i++) { _items.push({type: 'placeholder', key: i + ''}) diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx index ed519fed23..b42357585d 100644 --- a/src/components/dms/InitiateChatFlow.tsx +++ b/src/components/dms/InitiateChatFlow.tsx @@ -303,6 +303,7 @@ export function InitiateChatFlow({ if (follows) { for (const page of follows.pages) { for (const profile of page.follows) { + if (!checker(profile)) continue _items.push({ type: 'profile', key: profile.did, @@ -310,10 +311,6 @@ export function InitiateChatFlow({ }) } } - - _items = _items.sort(item => { - return item.type === 'profile' && checker(item.profile) ? -1 : 1 - }) } else { _items.push(...placeholders) } From 6ba5154bcc3e3c28fb53e4ede45b0c394e753cfb Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jun 2026 21:07:45 +0300 Subject: [PATCH 17/23] [Chat] Show invite link label in chat list preview (#10811) Co-authored-by: Claude Opus 4.8 (1M context) --- src/components/dms/getMessageInfo.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/dms/getMessageInfo.ts b/src/components/dms/getMessageInfo.ts index 4eade5cb62..b506dcce65 100644 --- a/src/components/dms/getMessageInfo.ts +++ b/src/components/dms/getMessageInfo.ts @@ -1,4 +1,8 @@ -import {AppBskyEmbedRecord, ChatBskyConvoDefs} from '@atproto/api' +import { + AppBskyEmbedRecord, + ChatBskyConvoDefs, + ChatBskyEmbedJoinLink, +} from '@atproto/api' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' @@ -83,6 +87,8 @@ export function getMessageInfo({ } else { message = prefix(defaultEmbeddedContentMessage) } + } else if (ChatBskyEmbedJoinLink.isView(lastMessage.embed)) { + message = prefix(i18n._(msg`(chat invite link)`)) } else { message = prefix(defaultEmbeddedContentMessage) } From de5ae0480930117b975f0bfa9c94a4c76dce0335 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:35:45 -0700 Subject: [PATCH 18/23] Fix link to profile from chat invite embed (#10822) --- src/components/dms/ChatInvite/Card.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/dms/ChatInvite/Card.tsx b/src/components/dms/ChatInvite/Card.tsx index 05829d9047..33183f82cb 100644 --- a/src/components/dms/ChatInvite/Card.tsx +++ b/src/components/dms/ChatInvite/Card.tsx @@ -7,7 +7,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {sanitizeHandle} from '#/lib/strings/handles' import {atoms as a, useTheme} from '#/alf' import {AvatarBubbles} from '#/components/AvatarBubbles' -import {SimpleInlineLinkText} from '#/components/Link' +import {InlineLinkText} from '#/components/Link' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {useChatInvite} from './Context' @@ -78,12 +78,12 @@ export function Card({size}: {size: 'large' | 'small'}) { allowFontScaling={!hasFixedHeight}> By{' '} - {ownerDisplayName} - + Date: Tue, 9 Jun 2026 11:36:08 -0700 Subject: [PATCH 19/23] Address chat design nits (#10823) --- src/components/dms/InitiateChatFlow.tsx | 3 +-- src/components/dms/MessageItem.tsx | 2 +- src/screens/Messages/components/InviteLinkDialog.tsx | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx index b42357585d..5eb2ec3705 100644 --- a/src/components/dms/InitiateChatFlow.tsx +++ b/src/components/dms/InitiateChatFlow.tsx @@ -514,7 +514,6 @@ export function InitiateChatFlow({ a.relative, a.align_center, a.justify_between, - web(a.pb_lg), ]}> {IS_NATIVE ? ( ) : showButton ? ( + + + ) + + if (isLoading || !hasMutualGroupChats) { + return ( + + {listHeader} + {isLoading ? ( + + + + ) : null} + {footer} + + ) + } + + return ( + + + + ) : null + } + footer={ + setFooterHeight(evt.nativeEvent.layout.height)}> + {footer} + + } + contentContainerStyle={[a.gap_0, {paddingBottom: footerHeight}]} + scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}} + onEndReached={() => void onEndReached()} + onEndReachedThreshold={0.5} + style={[web([{maxWidth: 420}])]} + /> + ) +} + +function MutualGroupChat({ + view, + profileDid, + currentConvoId, + onOptimisticallyRemoveConvo, + onRestoreConvo, +}: { + view: ChatBskyConvoDefs.ConvoView + profileDid: string + currentConvoId?: string + onOptimisticallyRemoveConvo: (convoId: string) => void + onRestoreConvo: (convoId: string) => void +}) { + const t = useTheme() + const {t: l} = useLingui() + const {currentAccount} = useSession() + const queryClient = useQueryClient() + + const convo = parseConvoView(view, currentAccount?.did) + + const {mutate: leaveConvo, isPending: isLeavePending} = useLeaveConvo( + convo?.view.id, + { + onSuccess: () => { + Toast.show(l`Left group chat.`) + void queryClient.invalidateQueries({ + queryKey: createListMutualGroupsQueryKey({subject: profileDid}), + }) + }, + onError: error => { + onRestoreConvo(view.id) + logger.error('Error leaving group chat', {message: error}) + let errorMessage = l`Could not leave chat.` + if (isNetworkError(error)) { + errorMessage = l`A network error occurred. Please check your internet connection.` + } else if (error instanceof ChatBskyConvoLeaveConvo.InvalidConvoError) { + errorMessage = l`Chat not found.` + } else if ( + error instanceof ChatBskyConvoLeaveConvo.OwnerCannotLeaveError + ) { + errorMessage = l`Chat owners cannot leave a group chat.` + } + Toast.show(errorMessage, {type: 'error'}) + }, + }, + ) + + const {mutate: removeMembers, isPending: isRemovePending} = + useRemoveFromGroupChat(convo?.view.id, { + onSuccess: () => { + Toast.show(l`Member removed from group chat.`) + void queryClient.invalidateQueries({ + queryKey: createListMutualGroupsQueryKey({subject: profileDid}), + }) + }, + onError: error => { + onRestoreConvo(view.id) + logger.error('Error removing group chat member', {message: error}) + let errorMessage = l`Could not remove member.` + if (isNetworkError(error)) { + errorMessage = l`A network error occurred. Please check your internet connection.` + } else if ( + error instanceof ChatBskyGroupRemoveMembers.InvalidConvoError + ) { + errorMessage = l`Chat not found.` + } else if ( + error instanceof ChatBskyGroupRemoveMembers.InsufficientRoleError + ) { + errorMessage = l`You must be a chat owner to remove a member.` + } + Toast.show(errorMessage, {type: 'error'}) + }, + }) + + if (!convo || convo.kind !== 'group') return null + + const owner = convo.primaryMember + const isViewerOwner = owner?.did != null && owner.did === currentAccount?.did + const isProfileOwner = owner?.did != null && owner.did === profileDid + const isCurrentConvo = view.id === currentConvoId + + return ( + + + + + + {convo.details.name} + + {isViewerOwner ? ( + + You own this chat + + ) : isProfileOwner ? ( + + They own this chat + + ) : null} + + + {isViewerOwner ? ( + + ) : isCurrentConvo ? ( + + Current chat + + ) : ( + + )} + + ) +} diff --git a/src/screens/Messages/ConversationSettings/MemberMenu.tsx b/src/screens/Messages/ConversationSettings/MemberMenu.tsx index 2d36bc115f..60b5b5e2eb 100644 --- a/src/screens/Messages/ConversationSettings/MemberMenu.tsx +++ b/src/screens/Messages/ConversationSettings/MemberMenu.tsx @@ -22,11 +22,12 @@ import { PersonX_Stroke2_Corner0_Rounded as PersonXIcon, } from '#/components/icons/Person' import * as Menu from '#/components/Menu' +import {BlockDialog} from '#/components/moderation/BlockDialog' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' -import {BlockMemberPrompt, RemoveMemberPrompt} from './prompts' +import {RemoveMemberPrompt} from './prompts' import {StatusBadge} from './StatusBadge' export function MemberMenu({ @@ -238,9 +239,11 @@ export function MemberMenu({ - void handleBlockMember()} + profile={profile} + onBlock={handleBlockMember} + currentConvoId={convoId} /> ) } - -export function BlockMemberPrompt({ - control, - onConfirm, -}: { - control: Dialog.DialogOuterProps['control'] - onConfirm: () => void -}) { - const {t: l} = useLingui() - - return ( - - ) -} diff --git a/src/state/queries/messages/list-mutual-groups.ts b/src/state/queries/messages/list-mutual-groups.ts new file mode 100644 index 0000000000..9ff4a1da09 --- /dev/null +++ b/src/state/queries/messages/list-mutual-groups.ts @@ -0,0 +1,39 @@ +import {useInfiniteQuery} from '@tanstack/react-query' + +import {DM_SERVICE_HEADERS} from '#/lib/constants' +import {createQueryKey} from '#/state/queries/util' +import {useAgent} from '#/state/session' + +const listMutualGroupsQueryKeyRoot = 'list-mutual-groups' + +export const createListMutualGroupsQueryKey = (args: {subject: string}) => + createQueryKey(listMutualGroupsQueryKeyRoot, args) + +export function useListMutualGroupsQuery({ + subject, + enabled, + limit = 20, +}: { + subject: string | undefined + enabled?: boolean + limit?: number +}) { + const agent = useAgent() + const isEnabled = enabled !== false && !!subject + + return useInfiniteQuery({ + gcTime: 0, + staleTime: 0, + enabled: isEnabled, + queryKey: createListMutualGroupsQueryKey({subject: subject ?? ''}), + queryFn: async ({pageParam}) => { + const {data} = await agent.chat.bsky.group.listMutualGroups( + {subject: subject!, cursor: pageParam, limit}, + {headers: DM_SERVICE_HEADERS}, + ) + return data + }, + initialPageParam: undefined as string | undefined, + getNextPageParam: page => page.cursor, + }) +} diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 51edee1903..5e6c266ea8 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -1,8 +1,6 @@ import {memo, useCallback, useMemo} from 'react' import {type AppBskyActorDefs} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -46,6 +44,7 @@ import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import {StarterPack} from '#/components/icons/StarterPack' import * as Menu from '#/components/Menu' +import {BlockDialog} from '#/components/moderation/BlockDialog' import { ReportDialog, useReportDialogControl, @@ -72,7 +71,7 @@ let ProfileMenu = ({ }): React.ReactNode => { const t = useTheme() const ax = useAnalytics() - const {_} = useLingui() + const {t: l} = useLingui() const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const reportDialogControl = useReportDialogControl() @@ -116,7 +115,7 @@ let ProfileMenu = ({ }, [currentAccount, profile]) const invalidateProfileQuery = useCallback(() => { - queryClient.invalidateQueries({ + void queryClient.invalidateQueries({ queryKey: profileQueryKey(profile.did), }) }, [queryClient, profile.did]) @@ -124,10 +123,10 @@ let ProfileMenu = ({ const onPressAddToStarterPacks = useCallback(() => { ax.metric('profile:addToStarterPack', {}) addToStarterPacksDialogControl.open() - }, [addToStarterPacksDialogControl]) + }, [addToStarterPacksDialogControl, ax]) const onPressShare = useCallback(() => { - shareUrl(toShareUrl(makeProfileLink(profile))) + void shareUrl(toShareUrl(makeProfileLink(profile))) }, [profile]) const onPressAddRemoveLists = useCallback(() => { @@ -145,11 +144,12 @@ let ProfileMenu = ({ if (profile.viewer?.muted) { try { await queueUnmute() - Toast.show(_(msg({message: 'Account unmuted', context: 'toast'}))) - } catch (e: any) { + Toast.show(l({message: 'Account unmuted', context: 'toast'})) + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { ax.logger.error('Failed to unmute account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), { + Toast.show(l`There was an issue! ${e.toString()}`, { type: 'error', }) } @@ -157,27 +157,29 @@ let ProfileMenu = ({ } else { try { await queueMute() - Toast.show(_(msg({message: 'Account muted', context: 'toast'}))) - } catch (e: any) { + Toast.show(l({message: 'Account muted', context: 'toast'})) + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { ax.logger.error('Failed to mute account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), { + Toast.show(l`There was an issue! ${e.toString()}`, { type: 'error', }) } } } - }, [ax, profile.viewer?.muted, queueUnmute, _, queueMute]) + }, [ax, profile.viewer?.muted, queueUnmute, l, queueMute]) const blockAccount = useCallback(async () => { if (profile.viewer?.blocking) { try { await queueUnblock() - Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) - } catch (e: any) { + Toast.show(l({message: 'Account unblocked', context: 'toast'})) + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { ax.logger.error('Failed to unblock account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), { + Toast.show(l`There was an issue! ${e.toString()}`, { type: 'error', }) } @@ -185,56 +187,59 @@ let ProfileMenu = ({ } else { try { await queueBlock() - Toast.show(_(msg({message: 'Account blocked', context: 'toast'}))) - } catch (e: any) { + Toast.show(l({message: 'Account blocked', context: 'toast'})) + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { ax.logger.error('Failed to block account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), { + Toast.show(l`There was an issue! ${e.toString()}`, { type: 'error', }) } } } - }, [ax, profile.viewer?.blocking, _, queueUnblock, queueBlock]) + }, [ax, profile.viewer?.blocking, l, queueUnblock, queueBlock]) const onPressFollowAccount = useCallback(async () => { try { await queueFollow() - Toast.show(_(msg({message: 'Account followed', context: 'toast'}))) - } catch (e: any) { + Toast.show(l({message: 'Account followed', context: 'toast'})) + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { ax.logger.error('Failed to follow account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), { + Toast.show(l`There was an issue! ${e.toString()}`, { type: 'error', }) } } - }, [_, ax, queueFollow]) + }, [l, ax, queueFollow]) const onPressUnfollowAccount = useCallback(async () => { try { await queueUnfollow() - Toast.show(_(msg({message: 'Account unfollowed', context: 'toast'}))) - } catch (e: any) { + Toast.show(l({message: 'Account unfollowed', context: 'toast'})) + } catch (err) { + const e = err as Error if (e?.name !== 'AbortError') { ax.logger.error('Failed to unfollow account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`), { + Toast.show(l`There was an issue! ${e.toString()}`, { type: 'error', }) } } - }, [_, ax, queueUnfollow]) + }, [l, ax, queueUnfollow]) const onPressReportAccount = useCallback(() => { reportDialogControl.open() }, [reportDialogControl]) const onPressShareATUri = useCallback(() => { - shareText(`at://${profile.did}`) + void shareText(`at://${profile.did}`) }, [profile.did]) const onPressShareDID = useCallback(() => { - shareText(profile.did) + void shareText(profile.did) }, [profile.did]) const onPressSearch = useCallback(() => { @@ -251,14 +256,14 @@ let ProfileMenu = ({ return ( - + {({props}) => { return ( <> - {statusNudgeActive && } ) @@ -278,9 +282,7 @@ let ProfileMenu = ({ { if (showLoggedOutWarning) { loggedOutWarningPromptControl.open() @@ -301,7 +303,7 @@ let ProfileMenu = ({ Search posts @@ -320,14 +322,12 @@ let ProfileMenu = ({ void onPressUnfollowAccount() + : () => void onPressFollowAccount() }> {isFollowing ? ( @@ -343,7 +343,7 @@ let ProfileMenu = ({ )} Add to starter packs @@ -352,7 +352,7 @@ let ProfileMenu = ({ Add to lists @@ -364,10 +364,10 @@ let ProfileMenu = ({ testID="profileHeaderDropdownListAddRemoveBtn" label={ status.isDisabled - ? _(msg`Go live (disabled)`) + ? l`Go live (disabled)` : status.isActive - ? _(msg`Edit live status`) - : _(msg`Go live`) + ? l`Edit live status` + : l`Go live` } onPress={() => { if (status.isDisabled) { @@ -418,7 +418,7 @@ let ProfileMenu = ({ (verification.viewer.hasIssuedVerification ? ( verificationRemovePromptControl.open()}> Remove verification @@ -428,7 +428,7 @@ let ProfileMenu = ({ ) : ( verificationCreatePromptControl.open()}> Verify account @@ -444,10 +444,10 @@ let ProfileMenu = ({ testID="profileHeaderDropdownMuteBtn" label={ profile.viewer?.muted - ? _(msg`Unmute account`) - : _(msg`Mute account`) + ? l`Unmute account` + : l`Mute account` } - onPress={onPressMuteAccount}> + onPress={() => void onPressMuteAccount()}> {profile.viewer?.muted ? ( Unmute account @@ -464,9 +464,9 @@ let ProfileMenu = ({ blockPromptControl.open()}> @@ -485,7 +485,7 @@ let ProfileMenu = ({ )} Report account @@ -503,7 +503,7 @@ let ProfileMenu = ({ Copy at:// URI @@ -512,7 +512,7 @@ let ProfileMenu = ({ Copy DID @@ -524,12 +524,10 @@ let ProfileMenu = ({ ) : null} - - - - - - - {status.isDisabled ? ( Date: Tue, 9 Jun 2026 22:24:28 +0300 Subject: [PATCH 21/23] [Chat] add lil gap between chats on web (#10821) --- src/screens/Messages/components/ChatListItem.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index d60b5f56c2..0bef7b84a4 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -40,11 +40,11 @@ import {getReactionInfo} from '#/components/dms/getReactionInfo' import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo' import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' -import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' +import {Bell2Off_Filled_Corner0_Rounded as BellStrokeIcon} from '#/components/icons/Bell2' import {type Props as SVGIconProps} from '#/components/icons/common' -import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen' +import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpenIcon} from '#/components/icons/EnveopeOpen' import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' -import {Trash_Stroke2_Corner0_Rounded} from '#/components/icons/Trash' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' import {Link} from '#/components/Link' import {useMenuControl} from '#/components/Menu' import {PostAlerts} from '#/components/moderation/PostAlerts' @@ -423,7 +423,7 @@ function BaseChatItem({ const markReadAction = { threshold: 120, color: t.palette.primary_500, - icon: EnvelopeOpen, + icon: EnvelopeOpenIcon, action: () => { markAsRead({ convoId: convo.view.id, @@ -434,7 +434,7 @@ function BaseChatItem({ const deleteAction = { threshold: 225, color: t.palette.negative_500, - icon: Trash_Stroke2_Corner0_Rounded, + icon: TrashIcon, action: () => { leaveConvoControl.open() }, @@ -509,7 +509,7 @@ function BaseChatItem({ a.px_lg, a.py_md, a.gap_md, - isWithinLeftPanel && a.rounded_sm, + isWithinLeftPanel && [a.rounded_sm, a.mt_2xs], { backgroundColor: hasUnread ? t.palette.primary_25 @@ -574,7 +574,7 @@ function BaseChatItem({ web({whiteSpace: 'preserve nowrap'}), ]}> {' '} - From 0558e8b79ad96eeb589ed9182fc8a8fbd4913950 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:40:35 -0700 Subject: [PATCH 22/23] Change Ignore to Reject for chat invites (#10825) --- src/screens/Messages/JoinRequests.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/screens/Messages/JoinRequests.tsx b/src/screens/Messages/JoinRequests.tsx index 5198cd7f19..a5bbdd10ce 100644 --- a/src/screens/Messages/JoinRequests.tsx +++ b/src/screens/Messages/JoinRequests.tsx @@ -474,14 +474,14 @@ function RejectButton({ return ( From 7f6cfaee7104aa3f765567f2256af952e48cb712 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 9 Jun 2026 15:15:37 -0500 Subject: [PATCH 23/23] Fix image collapse in FF (#10826) --- src/components/MediaPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index 9ab631dc36..25a0750b35 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -154,7 +154,7 @@ export function ImageItem({ } return ( - +