From 7285157ab5974da68dcfc7efb5350e3dd681acd0 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Fri, 9 Jan 2026 02:47:50 +0000 Subject: [PATCH 01/17] Nightly source-language update --- src/locale/locales/en/messages.po | 162 +++++++++++++++++------------- 1 file changed, 94 insertions(+), 68 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index f98751b8e1..5291e9ec0c 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -844,7 +844,7 @@ msgstr "" msgid "Add user to list" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:222 +#: src/ageAssurance/components/NoAccessScreen.tsx:231 msgid "Add your birthdate" msgstr "" @@ -914,8 +914,8 @@ msgctxt "toast" msgid "Age assurance inquiry was submitted" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:345 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:191 +#: src/ageAssurance/components/NoAccessScreen.tsx:354 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:199 msgid "Age assurance only takes a few minutes" msgstr "" @@ -1715,15 +1715,15 @@ msgstr "" msgid "By continuing, you consent to this use. You may change your mind any time by visiting settings. <0>Learn more" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:79 +#: src/screens/Signup/StepInfo/Policies.tsx:75 msgid "By creating an account you agree to the <0>Privacy Policy." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:46 +#: src/screens/Signup/StepInfo/Policies.tsx:42 msgid "By creating an account you agree to the <0>Terms of Service and <1>Privacy Policy." msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:66 +#: src/screens/Signup/StepInfo/Policies.tsx:62 msgid "By creating an account you agree to the <0>Terms of Service." msgstr "" @@ -1993,7 +1993,7 @@ msgstr "" msgid "Choose your own timeline! Feeds built by the community help you find content you love." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:244 +#: src/screens/Signup/StepInfo/index.tsx:273 msgid "Choose your password" msgstr "" @@ -2030,7 +2030,7 @@ msgstr "" msgid "Click here to contact our support team" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:236 +#: src/ageAssurance/components/NoAccessScreen.tsx:245 msgid "Click here to log out" msgstr "" @@ -2039,7 +2039,7 @@ msgid "Click here to restart the verification process." msgstr "" #: src/ageAssurance/components/NoAccessScreen.tsx:103 -#: src/ageAssurance/components/NoAccessScreen.tsx:219 +#: src/ageAssurance/components/NoAccessScreen.tsx:228 msgid "Click here to update your birthdate" msgstr "" @@ -2256,8 +2256,8 @@ msgstr "" msgid "Confirm delete account" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:359 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:87 +#: src/ageAssurance/components/NoAccessScreen.tsx:368 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:95 #: src/components/dialogs/DeviceLocationRequestDialog.tsx:40 #: src/components/dialogs/DeviceLocationRequestDialog.tsx:105 msgid "Confirm your location" @@ -2283,8 +2283,8 @@ msgstr "" msgid "Connection issue" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:296 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:130 +#: src/ageAssurance/components/NoAccessScreen.tsx:305 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:138 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:29 msgid "Contact our moderation team" msgstr "" @@ -2735,7 +2735,7 @@ msgstr "" msgid "Dark theme" msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:272 +#: src/screens/Signup/StepInfo/index.tsx:301 msgid "Date of birth" msgstr "" @@ -3065,8 +3065,8 @@ msgstr "" msgid "Don't see an email? <0>Click here to resend." msgstr "" -#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:103 -#: src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx:33 +#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:104 +#: src/components/ageAssurance/AgeAssuranceDismissibleNotice.tsx:37 msgid "Don't show again" msgstr "" @@ -3289,7 +3289,7 @@ msgid "Either the creator of this list has blocked you or you have blocked the c msgstr "" #: src/screens/Settings/AccountSettings.tsx:66 -#: src/screens/Signup/StepInfo/index.tsx:196 +#: src/screens/Signup/StepInfo/index.tsx:225 msgid "Email" msgstr "" @@ -3458,7 +3458,7 @@ msgid "Enter your birthdate" msgstr "" #: src/screens/Login/ForgotPasswordForm.tsx:99 -#: src/screens/Signup/StepInfo/index.tsx:216 +#: src/screens/Signup/StepInfo/index.tsx:245 msgid "Enter your email address" msgstr "" @@ -4427,9 +4427,9 @@ msgstr "" msgid "Go to {firstAuthorName}'s profile" msgstr "" -#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:87 -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:64 -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:73 +#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:91 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:71 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:80 #: src/screens/Moderation/index.tsx:219 msgid "Go to account settings" msgstr "" @@ -4531,6 +4531,10 @@ msgstr "" msgid "Have a code? <0>Click here." msgstr "" +#: src/screens/Signup/StepInfo/index.tsx:332 +msgid "Have we got your location wrong? <0>Tap here to confirm your location with GPS." +msgstr "" + #: src/screens/Signup/index.tsx:220 msgid "Having trouble?" msgstr "" @@ -4564,7 +4568,7 @@ msgstr "" msgid "Hey there!" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:201 +#: src/ageAssurance/components/NoAccessScreen.tsx:210 msgid "Hi there!" msgstr "" @@ -4765,7 +4769,7 @@ msgstr "" msgid "If alt text is long, toggles alt text expanded state" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:110 +#: src/screens/Signup/StepInfo/index.tsx:351 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." msgstr "" @@ -4861,7 +4865,7 @@ msgstr "" msgid "Imported on {0}" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:204 +#: src/ageAssurance/components/NoAccessScreen.tsx:213 msgid "In order to provide an age-appropriate experience, we need to know your birthdate. This is a one-time thing, and your data will be kept private." msgstr "" @@ -4977,7 +4981,7 @@ msgstr "" msgid "Invite {name} to join Bluesky" msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:166 +#: src/screens/Signup/StepInfo/index.tsx:195 msgid "Invite code" msgstr "" @@ -5006,8 +5010,8 @@ msgstr "" msgid "Invites, but personal" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:356 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:84 +#: src/ageAssurance/components/NoAccessScreen.tsx:365 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:92 msgid "Is your location not accurate? <0>Tap here to confirm your location." msgstr "" @@ -5015,7 +5019,7 @@ msgstr "" msgid "It looks like some of your contacts have not tried to find you here yet. You can personally invite them by customizing a draft message we will provide." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:292 +#: src/screens/Signup/StepInfo/index.tsx:384 msgid "It's correct" msgstr "" @@ -5103,13 +5107,13 @@ msgstr "" msgid "Larger" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:339 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:185 +#: src/ageAssurance/components/NoAccessScreen.tsx:348 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:193 msgid "Last initiated {timeAgo} ago" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:337 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:183 +#: src/ageAssurance/components/NoAccessScreen.tsx:346 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:191 msgid "Last initiated just now" msgstr "" @@ -5133,7 +5137,7 @@ msgstr "" msgid "Learn More" msgstr "" -#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:64 +#: src/components/ageAssurance/AgeAssuranceDismissibleFeedBanner.tsx:65 msgid "Learn more about age assurance" msgstr "" @@ -5185,7 +5189,7 @@ msgstr "" msgid "Learn more about what is public on Bluesky." msgstr "" -#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:84 +#: src/components/ageAssurance/AgeAssuranceAdmonition.tsx:88 msgid "Learn more in your <0>account settings." msgstr "" @@ -6661,7 +6665,7 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:229 #: src/screens/Settings/AccountSettings.tsx:121 #: src/screens/Settings/AccountSettings.tsx:125 -#: src/screens/Signup/StepInfo/index.tsx:231 +#: src/screens/Signup/StepInfo/index.tsx:260 #: src/view/com/modals/DeleteAccount.tsx:239 #: src/view/com/modals/DeleteAccount.tsx:246 msgid "Password" @@ -6842,7 +6846,7 @@ msgid "Please choose your handle." msgstr "" #: src/screens/Signup/state.ts:281 -#: src/screens/Signup/StepInfo/index.tsx:122 +#: src/screens/Signup/StepInfo/index.tsx:151 msgid "Please choose your password." msgstr "" @@ -6855,7 +6859,7 @@ msgid "Please complete the verification captcha." msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:101 -#: src/screens/Signup/StepInfo/index.tsx:111 +#: src/screens/Signup/StepInfo/index.tsx:140 msgid "Please double-check that you have entered your email address correctly." msgstr "" @@ -6903,11 +6907,11 @@ msgid "Please enter the security code we sent to your previous email address." msgstr "" #: src/screens/Signup/state.ts:265 -#: src/screens/Signup/StepInfo/index.tsx:93 +#: src/screens/Signup/StepInfo/index.tsx:122 msgid "Please enter your email." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:86 +#: src/screens/Signup/StepInfo/index.tsx:115 msgid "Please enter your invite code." msgstr "" @@ -7356,13 +7360,13 @@ msgstr "" msgid "Read the Bluesky blog" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:56 -#: src/screens/Signup/StepInfo/Policies.tsx:82 +#: src/screens/Signup/StepInfo/Policies.tsx:52 +#: src/screens/Signup/StepInfo/Policies.tsx:78 msgid "Read the Bluesky Privacy Policy" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:49 -#: src/screens/Signup/StepInfo/Policies.tsx:69 +#: src/screens/Signup/StepInfo/Policies.tsx:45 +#: src/screens/Signup/StepInfo/Policies.tsx:65 msgid "Read the Bluesky Terms of Service" msgstr "" @@ -7826,7 +7830,7 @@ msgstr "" msgid "Require an email code to sign in to your account." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:180 +#: src/screens/Signup/StepInfo/index.tsx:209 msgid "Required for this provider" msgstr "" @@ -7892,6 +7896,8 @@ msgstr "" msgid "Retries the last action, which errored out" msgstr "" +#: src/components/ageAssurance/AgeAssuranceErrors.tsx:27 +#: src/components/ageAssurance/AgeAssuranceErrors.tsx:30 #: src/components/contacts/screens/VerifyNumber.tsx:347 #: src/components/contacts/screens/VerifyNumber.tsx:352 #: src/components/dms/MessageItem.tsx:322 @@ -8335,7 +8341,7 @@ msgstr "" msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:273 +#: src/screens/Signup/StepInfo/index.tsx:302 msgid "Select your date of birth" msgstr "" @@ -8451,7 +8457,7 @@ msgstr "" msgid "Set who can reply to your post" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:211 +#: src/ageAssurance/components/NoAccessScreen.tsx:220 msgid "Set your birthdate below and we'll get you back to posting and exploring in no time!" msgstr "" @@ -9172,6 +9178,10 @@ msgstr "" msgid "Tap for more information" msgstr "" +#: src/screens/Signup/StepInfo/index.tsx:336 +msgid "Tap here to confirm your location with GPS." +msgstr "" + #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:164 msgid "Tap to acknowledge that you understand and agree to these updates and continue using Bluesky" msgstr "" @@ -9252,8 +9262,8 @@ msgstr "" msgid "Thanks, you have successfully verified your email address. You can close this dialog." msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:385 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:113 +#: src/ageAssurance/components/NoAccessScreen.tsx:394 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:121 msgid "Thanks! You're all set." msgstr "" @@ -9700,7 +9710,7 @@ 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/Signup/StepInfo/Policies.tsx:35 +#: src/screens/Signup/StepInfo/Policies.tsx:31 msgid "This service has not provided terms of service or a privacy policy." msgstr "" @@ -9801,7 +9811,7 @@ msgstr "" msgid "To disable your email 2FA method, please verify your access to <0>{0}" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:233 +#: src/ageAssurance/components/NoAccessScreen.tsx:242 msgid "To log out, <0>click here." msgstr "" @@ -9940,7 +9950,7 @@ msgstr "" msgid "Unapply Pull Request {currentChannel}" msgstr "" -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:40 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:41 msgid "Unavailable" msgstr "" @@ -10016,10 +10026,14 @@ msgstr "" msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:188 +#: src/ageAssurance/components/NoAccessScreen.tsx:197 msgid "Unfortunately, the birthdate you have saved to your profile makes you too young to access Bluesky." msgstr "" +#: src/ageAssurance/components/NoAccessScreen.tsx:182 +msgid "Unfortunately, your declared age indicates that you are not old enough to access Bluesky in your region." +msgstr "" + #: src/components/verification/VerificationsDialog.tsx:211 msgid "Unknown verifier" msgstr "" @@ -10373,8 +10387,8 @@ msgstr "" msgid "Verify account" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:322 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:168 +#: src/ageAssurance/components/NoAccessScreen.tsx:331 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:176 msgid "Verify again" msgstr "" @@ -10400,10 +10414,10 @@ msgstr "" msgid "Verify email dialog" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:310 -#: src/ageAssurance/components/NoAccessScreen.tsx:324 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:156 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:170 +#: src/ageAssurance/components/NoAccessScreen.tsx:319 +#: src/ageAssurance/components/NoAccessScreen.tsx:333 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:164 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:178 msgid "Verify now" msgstr "" @@ -10740,6 +10754,10 @@ msgstr "" msgid "We were unable to determine if you are allowed to upload videos. Please try again." msgstr "" +#: src/components/ageAssurance/AgeAssuranceErrors.tsx:18 +msgid "We were unable to load the age assurance configuration for your region, probably due to a network error. Some content and features may be unavailable temporarily. Please try again later." +msgstr "" + #: src/components/dialogs/BirthDateSettings.tsx:67 msgid "We were unable to load your birthdate preferences. Please try again." msgstr "" @@ -10799,8 +10817,8 @@ msgstr "" msgid "We're so excited to have you join us!" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:378 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:106 +#: src/ageAssurance/components/NoAccessScreen.tsx:387 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:114 msgid "We're sorry, but based on your device's location, you are currently located in a region that requires age assurance." msgstr "" @@ -11015,8 +11033,8 @@ msgstr "" msgid "You are creating an account on" msgstr "" -#: src/ageAssurance/components/NoAccessScreen.tsx:292 -#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:126 +#: src/ageAssurance/components/NoAccessScreen.tsx:301 +#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:134 msgid "You are currently unable to access Bluesky's Age Assurance flow. Please <0>contact our moderation team if you believe this is an error." msgstr "" @@ -11270,8 +11288,12 @@ msgstr "" msgid "You may only add up to 3 feeds" msgstr "" -#: src/screens/Signup/StepInfo/Policies.tsx:104 -msgid "You must be 13 years of age or older to create an account." +#: src/screens/Signup/StepInfo/index.tsx:322 +msgid "You must be {0} years of age or older to create an account in your region." +msgstr "" + +#: src/screens/Signup/StepInfo/index.tsx:317 +msgid "You must be {MIN_ACCESS_AGE} years of age or older to create an account." msgstr "" #: src/components/dialogs/BirthDateSettings.tsx:178 @@ -11282,7 +11304,7 @@ msgstr "" msgid "You must be following at least seven other people to generate a starter pack." msgstr "" -#: src/components/ageAssurance/AgeRestrictedScreen.tsx:53 +#: src/components/ageAssurance/AgeRestrictedScreen.tsx:60 msgid "You must complete age assurance in order to access this screen." msgstr "" @@ -11462,7 +11484,7 @@ msgstr "" msgid "Your appeal has been submitted. If your appeal succeeds, you will receive an email." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:260 +#: src/screens/Signup/StepInfo/index.tsx:289 msgid "Your birth date" msgstr "" @@ -11507,7 +11529,7 @@ msgstr "" #: src/screens/Login/ForgotPasswordForm.tsx:51 #: src/screens/Settings/components/ChangePasswordDialog.tsx:81 #: src/screens/Signup/state.ts:273 -#: src/screens/Signup/StepInfo/index.tsx:100 +#: src/screens/Signup/StepInfo/index.tsx:129 msgid "Your email appears to be invalid." msgstr "" @@ -11552,6 +11574,10 @@ msgstr "" msgid "Your location data is not tracked and does not leave your device." msgstr "" +#: src/screens/Signup/StepInfo/index.tsx:367 +msgid "Your location has been updated." +msgstr "" + #: src/components/dialogs/MutedWords.tsx:374 msgid "Your muted words" msgstr "" @@ -11560,7 +11586,7 @@ msgstr "" msgid "Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on." msgstr "" -#: src/screens/Signup/StepInfo/index.tsx:129 +#: src/screens/Signup/StepInfo/index.tsx:158 msgid "Your password must be at least 8 characters long." msgstr "" From 85a616eee02f5452783f47260f8f6f5e7d819747 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 9 Jan 2026 17:46:02 +0200 Subject: [PATCH 02/17] Add Claude Code GitHub Workflow (#9667) * "Claude PR Assistant workflow" * "Claude Code Review workflow" --- .github/workflows/claude-code-review.yml | 44 +++++++++++++++++++++ .github/workflows/claude.yml | 50 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000000..b5e8cfd4dc --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000000..d300267f18 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + From edb19dd6cf412d10151f43e5ac6fea0da8dd13e1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 9 Jan 2026 10:19:47 -0600 Subject: [PATCH 03/17] [APP-1750] Add the ability to report livestreams (#9654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add report dialog to LiveStatus dialog * Mr Worldwide™ * Bug fix bug fix * Copy update * Simplify parsing * Bump api sdk * update dev-env * Update yarn.lock * Bump api sdk * Refactor useActorStatus, add disablement and appeal dialog * Fix types * Guard against chat profile views * Go live (disabled) * Fix types * Status reports should only go to bsky * Ah yeah let's not override types --------- Co-authored-by: Samuel Newman --- package.json | 2 +- src/components/ProfileHoverCard/index.web.tsx | 1 + src/components/dialogs/Context.tsx | 7 + src/components/live/GoLiveDisabledDialog.tsx | 147 ++++++++++++++++++ src/components/live/LiveStatusDialog.tsx | 60 ++++++- .../moderation/ReportDialog/action.ts | 1 + .../moderation/ReportDialog/const.ts | 9 ++ .../moderation/ReportDialog/copy.ts | 6 + .../moderation/ReportDialog/index.tsx | 29 ++-- .../moderation/ReportDialog/types.ts | 7 + .../ReportDialog/utils/parseReportSubject.ts | 8 + src/lib/actor-status.ts | 29 +++- src/view/com/profile/ProfileMenu.tsx | 30 +++- src/view/shell/index.tsx | 2 + src/view/shell/index.web.tsx | 2 + yarn.lock | 40 ++--- 16 files changed, 325 insertions(+), 55 deletions(-) create mode 100644 src/components/live/GoLiveDisabledDialog.tsx diff --git a/package.json b/package.json index 92b0374720..185e530bd3 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.18.11", + "@atproto/api": "^0.18.13", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.6", diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 58b30358c9..2f8e3105cc 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -389,6 +389,7 @@ let Card = ({ {data && moderationOpts ? ( status.isActive ? ( ageAssuranceRedirectDialogControl: StatefulControl + reportDialogControl: StatefulControl<{subject: ReportSubject}> } const ControlsContext = createContext(null) @@ -51,6 +53,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }>() const ageAssuranceRedirectDialogControl = useStatefulDialogControl() + const reportDialogControl = useStatefulDialogControl<{ + subject: ReportSubject + }>() const ctx = useMemo( () => ({ @@ -60,6 +65,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { emailDialogControl, linkWarningDialogControl, ageAssuranceRedirectDialogControl, + reportDialogControl, }), [ mutedWordsDialogControl, @@ -68,6 +74,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { emailDialogControl, linkWarningDialogControl, ageAssuranceRedirectDialogControl, + reportDialogControl, ], ) diff --git a/src/components/live/GoLiveDisabledDialog.tsx b/src/components/live/GoLiveDisabledDialog.tsx new file mode 100644 index 0000000000..3d548bbb8f --- /dev/null +++ b/src/components/live/GoLiveDisabledDialog.tsx @@ -0,0 +1,147 @@ +import {useCallback, useState} from 'react' +import {View} from 'react-native' +import {type AppBskyActorDefs, ToolsOzoneReportDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation} from '@tanstack/react-query' + +import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import {atoms as a, web} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Loader} from '#/components/Loader' +import * as Toast from '#/components/Toast' +import {Text} from '#/components/Typography' + +export function GoLiveDisabledDialog({ + control, + status, +}: { + control: Dialog.DialogControlProps + status: AppBskyActorDefs.StatusView +}) { + return ( + + + + + ) +} + +export function DialogInner({ + control, + status, +}: { + control: Dialog.DialogControlProps + status: AppBskyActorDefs.StatusView +}) { + const {_} = useLingui() + const agent = useAgent() + const [details, setDetails] = useState('') + + const {mutate, isPending} = useMutation({ + mutationFn: async () => { + if (!agent.session?.did) { + throw new Error('Not logged in') + } + if (!status.uri || !status.cid) { + throw new Error('Status is missing uri or cid') + } + + if (__DEV__) { + logger.info('Submitting go live appeal', { + details, + }) + } else { + await agent.createModerationReport( + { + reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: status.uri, + cid: status.cid, + }, + reason: details, + }, + { + encoding: 'application/json', + headers: BLUESKY_MOD_SERVICE_HEADERS, + }, + ) + } + }, + onError: () => { + Toast.show(_(msg`Failed to submit appeal, please try again.`), { + type: 'error', + }) + }, + onSuccess: () => { + control.close() + Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})), { + type: 'success', + }) + }, + }) + + const onSubmit = useCallback(() => mutate(), [mutate]) + + return ( + + + + + Going live is currently disabled for your account + + + + You are currently blocked from using the Go Live feature. To + appeal this moderation decision, please submit the form below. + + + + + This appeal will be sent to Bluesky's moderation service. + + + + + + + + + + + + ) +} diff --git a/src/components/live/LiveStatusDialog.tsx b/src/components/live/LiveStatusDialog.tsx index f15650e485..086f0759c7 100644 --- a/src/components/live/LiveStatusDialog.tsx +++ b/src/components/live/LiveStatusDialog.tsx @@ -17,6 +17,9 @@ import {unstableCacheProfileView} from '#/state/queries/profile' import {android, atoms as a, platform, tokens, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' +import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' +import {useGlobalReportDialogControl} from '#/components/moderation/ReportDialog' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' @@ -28,6 +31,7 @@ export function LiveStatusDialog({ control, profile, embed, + status, }: { control: Dialog.DialogControlProps profile: bsky.profile.AnyProfileView @@ -38,7 +42,12 @@ export function LiveStatusDialog({ return ( - + ) } @@ -47,10 +56,12 @@ function DialogInner({ profile, embed, navigation, + status, }: { profile: bsky.profile.AnyProfileView embed: AppBskyEmbedExternal.View navigation: NavigationProp + status: AppBskyActorDefs.StatusView }) { const {_} = useLingui() const control = Dialog.useDialogContext() @@ -69,6 +80,7 @@ function DialogInner({ contentContainerStyle={[a.pt_0, a.px_0]} style={[web({maxWidth: 420}), a.overflow_hidden]}> @@ -205,15 +221,43 @@ export function LiveStatus({ )} - - Live feature is in beta testing - + + + + Live feature is in beta + + + {status && ( + { + function open() { + reportDialogControl.open({ + subject: { + ...status, + $type: 'app.bsky.actor.defs#statusView', + }, + }) + } + if (dialogContext.isWithinDialog) { + dialogContext.close(open) + } else { + open() + } + })} + style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}> + Report + + )} + ) diff --git a/src/components/moderation/ReportDialog/action.ts b/src/components/moderation/ReportDialog/action.ts index 1c35bd0494..e7e88ac265 100644 --- a/src/components/moderation/ReportDialog/action.ts +++ b/src/components/moderation/ReportDialog/action.ts @@ -70,6 +70,7 @@ export function useSubmitReportMutation() { } break } + case 'status': case 'post': case 'list': case 'feed': diff --git a/src/components/moderation/ReportDialog/const.ts b/src/components/moderation/ReportDialog/const.ts index 14ff397955..7e5075cce2 100644 --- a/src/components/moderation/ReportDialog/const.ts +++ b/src/components/moderation/ReportDialog/const.ts @@ -3,6 +3,8 @@ import { ToolsOzoneReportDefs as OzoneReportDefs, } from '@atproto/api' +import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types' + export const DMCA_LINK = 'https://bsky.social/about/support/copyright' export const SUPPORT_PAGE = 'https://bsky.social/about/support' @@ -112,3 +114,10 @@ export const BSKY_LABELER_ONLY_REPORT_REASONS: Set = OzoneReportDefs.REASONCHILDSAFETYOTHER, OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, ]) + +/** + * Set of _parsed_ subject types that should only be sent to Bluesky's + * moderation service. + */ +export const BSKY_LABELER_ONLY_SUBJECT_TYPES: Set = + new Set(['convoMessage', 'status']) diff --git a/src/components/moderation/ReportDialog/copy.ts b/src/components/moderation/ReportDialog/copy.ts index 1027673b12..5fb2ad326d 100644 --- a/src/components/moderation/ReportDialog/copy.ts +++ b/src/components/moderation/ReportDialog/copy.ts @@ -14,6 +14,12 @@ export function useCopyForSubject(subject: ParsedReportSubject) { subtitle: _(msg`Why should this user be reviewed?`), } } + case 'status': { + return { + title: _(msg`Report this livestream`), + subtitle: _(msg`Why should this livestream be reviewed?`), + } + } case 'post': { return { title: _(msg`Report this post`), diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index 157ad18fb4..14354332cb 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -16,6 +16,7 @@ import {atoms as a, useGutters, useTheme} from '#/alf' import * as Admonition from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {useDelayedLoading} from '#/components/hooks/useDelayedLoading' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate' import { @@ -31,6 +32,7 @@ import {Text} from '#/components/Typography' import {useSubmitReportMutation} from './action' import { BSKY_LABELER_ONLY_REPORT_REASONS, + BSKY_LABELER_ONLY_SUBJECT_TYPES, NEW_TO_OLD_REASONS_MAP, SUPPORT_PAGE, } from './const' @@ -44,17 +46,27 @@ import { useReportOptions, } from './utils/useReportOptions' +export {type ReportSubject} from './types' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' +export function useGlobalReportDialogControl() { + return useGlobalDialogsControlContext().reportDialogControl +} + const logger = Logger.create(Logger.Context.ReportDialog) +export function GlobalReportDialog() { + const {value, control} = useGlobalReportDialogControl() + return +} + export function ReportDialog( props: Omit & { - subject: ReportSubject + subject?: ReportSubject }, ) { const subject = React.useMemo( - () => parseReportSubject(props.subject), + () => (props.subject ? parseReportSubject(props.subject) : undefined), [props.subject], ) const onClose = React.useCallback(() => { @@ -116,8 +128,10 @@ function Inner(props: ReportDialogProps) { const isBskyOnlyReason = state?.selectedOption?.reason ? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason) : false - // some subjects (chats) only go to Bluesky - const isBskyOnlySubject = props.subject.type === 'convoMessage' + // some subjects ONLY go to Bluesky + const isBskyOnlySubject = BSKY_LABELER_ONLY_SUBJECT_TYPES.has( + props.subject.type, + ) /** * Labelers that support this `subject` and its NSID collection @@ -824,12 +838,7 @@ function LabelerCard({ {title} + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> By {sanitizeHandle(labeler.creator.handle, '@')} diff --git a/src/components/moderation/ReportDialog/types.ts b/src/components/moderation/ReportDialog/types.ts index 6df3a82ff6..42fcf7c25e 100644 --- a/src/components/moderation/ReportDialog/types.ts +++ b/src/components/moderation/ReportDialog/types.ts @@ -18,6 +18,7 @@ export type ReportSubject = | $Typed | $Typed | $Typed + | $Typed | $Typed | $Typed | $Typed @@ -38,6 +39,12 @@ export type ParsedReportSubject = quote: boolean } } + | { + type: 'status' + uri: string + cid: string + nsid: string + } | { type: 'list' uri: string diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts index 0abfe1d9fd..950536199c 100644 --- a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts +++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts @@ -33,6 +33,14 @@ export function parseReportSubject( did: subject.did, nsid: 'app.bsky.actor.profile', } + } else if (AppBskyActorDefs.isStatusView(subject)) { + if (!subject.uri || !subject.cid) return + return { + type: 'status', + uri: subject.uri, + cid: subject.cid, + nsid: 'app.bsky.actor.status', + } } else if (AppBskyGraphDefs.isListView(subject)) { return { type: 'list', diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts index 7e023be44e..31e532eaa0 100644 --- a/src/lib/actor-status.ts +++ b/src/lib/actor-status.ts @@ -19,15 +19,27 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { return useMemo(() => { tick! // revalidate every minute - if ( - shadowed && - 'status' in shadowed && - shadowed.status && - validateStatus(shadowed.did, shadowed.status, config) && - isStatusStillActive(shadowed.status.expiresAt) - ) { + if (shadowed && 'status' in shadowed && shadowed.status) { + const isValid = validateStatus(shadowed.did, shadowed.status, config) + const isDisabled = shadowed.status.isDisabled || false + const isActive = isStatusStillActive(shadowed.status.expiresAt) + if (isValid && !isDisabled && isActive) { + return { + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled: false, + isActive: true, + status: 'app.bsky.actor.status#live', + embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this + expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this + record: shadowed.status.record, + } satisfies AppBskyActorDefs.StatusView + } return { - isActive: true, + uri: shadowed.status.uri, + cid: shadowed.status.cid, + isDisabled, + isActive: false, status: 'app.bsky.actor.status#live', embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this @@ -36,6 +48,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { } else { return { status: '', + isDisabled: false, isActive: false, record: {}, } satisfies AppBskyActorDefs.StatusView diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 5fc976ecc2..8cf6d2c968 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -49,6 +49,7 @@ import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/ import {StarterPack} from '#/components/icons/StarterPack' import {EditLiveDialog} from '#/components/live/EditLiveDialog' import {GoLiveDialog} from '#/components/live/GoLiveDialog' +import {GoLiveDisabledDialog} from '#/components/live/GoLiveDisabledDialog' import * as Menu from '#/components/Menu' import { ReportDialog, @@ -79,6 +80,7 @@ let ProfileMenu = ({ const [devModeEnabled] = useDevMode() const verification = useFullVerificationState({profile}) const canGoLive = useCanGoLive(currentAccount?.did) + const status = useActorStatus(profile) const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) @@ -90,6 +92,7 @@ let ProfileMenu = ({ const blockPromptControl = Prompt.usePromptControl() const loggedOutWarningPromptControl = Prompt.usePromptControl() const goLiveDialogControl = useDialogControl() + const goLiveDisabledDialogControl = useDialogControl() const addToStarterPacksDialogControl = useDialogControl() const showLoggedOutWarning = React.useMemo(() => { @@ -220,8 +223,6 @@ let ProfileMenu = ({ return v.issuer === currentAccount?.did }) ?? [] - const status = useActorStatus(profile) - return ( @@ -330,13 +331,21 @@ let ProfileMenu = ({ + onPress={ + status.isDisabled + ? goLiveDisabledDialogControl.open + : goLiveDialogControl.open + }> - {status.isActive ? ( + {status.isDisabled ? ( + Go live (disabled) + ) : status.isActive ? ( Edit live status ) : ( Go live @@ -517,7 +526,12 @@ let ProfileMenu = ({ verifications={currentAccountVerifications} /> - {status.isActive ? ( + {status.isDisabled ? ( + + ) : status.isActive ? ( + {/* Until policy update has been completed by the user, don't render anything that is portaled */} {policyUpdateState.completed && ( diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 4dee3d17bd..1f687a1b98 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -24,6 +24,7 @@ import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {NuxDialogs} from '#/components/dialogs/nuxs' import {SigninDialog} from '#/components/dialogs/Signin' import {useWelcomeModal} from '#/components/hooks/useWelcomeModal' +import {GlobalReportDialog} from '#/components/moderation/ReportDialog' import { Outlet as PolicyUpdateOverlayPortalOutlet, usePolicyUpdateContext, @@ -73,6 +74,7 @@ function ShellInner() { + {welcomeModalControl.isOpen && ( diff --git a/yarn.lock b/yarn.lock index 3fea12b360..a010c615be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,12 +82,12 @@ "@atproto/xrpc" "^0.7.6" "@atproto/xrpc-server" "^0.10.0" -"@atproto/api@^0.18.11": - version "0.18.11" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.11.tgz#38b8bacecaae4c24bc29bd98b34f270d8be675b3" - integrity sha512-YhgyL4rOBFOIs+e/8s5S+EjwM3T5q65IKh30ZoIrWcQS/2uteiIzg1xB+iXmexZq4Cwug0NLfRUJDViDK/ut0w== +"@atproto/api@^0.18.13": + version "0.18.13" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.13.tgz#63eee310e6715752eb87748323cf9ab57dd91e4b" + integrity sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q== dependencies: - "@atproto/common-web" "^0.4.10" + "@atproto/common-web" "^0.4.11" "@atproto/lexicon" "^0.6.0" "@atproto/syntax" "^0.4.2" "@atproto/xrpc" "^0.7.7" @@ -208,13 +208,13 @@ pino-http "^8.2.1" typed-emitter "^2.1.0" -"@atproto/common-web@^0.4.10": - version "0.4.10" - resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.10.tgz#3d29df4dc3ea8f5c149161209f55e146f27867c1" - integrity sha512-TLDZSgSKzT8ZgOrBrTGK87J1CXve9TEuY6NVVUBRkOMzRRtQzpFb9/ih5WVS/hnaWVvE30CfuyaetRoma+WKNw== +"@atproto/common-web@^0.4.11": + version "0.4.11" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.11.tgz#eb41dc02c1ea4221388630e193d181fb098186e0" + integrity sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ== dependencies: - "@atproto/lex-data" "0.0.6" - "@atproto/lex-json" "0.0.6" + "@atproto/lex-data" "0.0.7" + "@atproto/lex-json" "0.0.7" zod "^3.23.8" "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": @@ -415,10 +415,10 @@ uint8arrays "3.0.0" unicode-segmenter "^0.14.0" -"@atproto/lex-data@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.6.tgz#280d05ec9579ab091dc4a8b696ebbeddcb8ee37d" - integrity sha512-MBNB4ghRJQzuXK1zlUPljpPbQcF1LZ5dzxy274KqPt4p3uPuRw0mHjgcCoWzRUNBQC685WMQR4IN9DHtsnG57A== +"@atproto/lex-data@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.7.tgz#6aa87423f6d47849bec8ff3ca0b00ce93964adc8" + integrity sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw== dependencies: "@atproto/syntax" "0.4.2" multiformats "^9.9.0" @@ -451,12 +451,12 @@ "@atproto/lex-data" "0.0.3" tslib "^2.8.1" -"@atproto/lex-json@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.6.tgz#50618efb1cc11708b3897de14dcd3bd9c06e064d" - integrity sha512-EILnN5cditPvf+PCNjXt7reMuzjugxAL1fpSzmzJbEMGMUwxOf5pPWxRsaA/M3Boip4NQZ+6DVrPOGUMlnqceg== +"@atproto/lex-json@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.7.tgz#c06e1fc3e06d739bbb74694f5d846055bed37866" + integrity sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g== dependencies: - "@atproto/lex-data" "0.0.6" + "@atproto/lex-data" "0.0.7" tslib "^2.8.1" "@atproto/lex-resolver@0.0.5": From d2aea9161bf68d79e021fb776a465ef20233250f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 9 Jan 2026 18:35:38 +0200 Subject: [PATCH 04/17] Delete claude-code-review.yml (#9671) --- .github/workflows/claude-code-review.yml | 44 ------------------------ 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index b5e8cfd4dc..0000000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - From 966ae68dd2d7edb1b8f06b843ec0799ea03b19ab Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 9 Jan 2026 18:45:38 +0200 Subject: [PATCH 05/17] Set Claude model to Opus (#9672) --- .github/workflows/claude.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d300267f18..247217fe2d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -48,3 +48,7 @@ jobs: # or https://code.claude.com/docs/en/cli-reference for available options # claude_args: '--allowed-tools Bash(gh pr:*)' + # NOTE(sfn): we can add a custom system prompt here + + claude_args: | + --model claude-opus-4-5-20251101 From c4fd9980cccb475fbff05f50fc3bffecba772d3c Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Fri, 9 Jan 2026 14:13:43 -0800 Subject: [PATCH 06/17] Cleaner sidebar layout (#9603) --- src/components/FeedInterstitials.tsx | 9 +- src/components/ProgressGuide/FollowDialog.tsx | 19 +- src/components/ProgressGuide/List.tsx | 145 ++++++++++++-- src/components/ProgressGuide/Task.tsx | 4 +- src/components/TrendingTopics.tsx | 19 +- src/components/interstitials/Trending.tsx | 3 +- src/state/queries/profile.ts | 45 +++++ src/view/shell/desktop/Feeds.tsx | 184 ++++++++++++++---- src/view/shell/desktop/RightNav.tsx | 18 +- .../shell/desktop/SidebarTrendingTopics.tsx | 105 +++++----- 10 files changed, 422 insertions(+), 129 deletions(-) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index a482d5a482..cad86facb4 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -933,8 +933,15 @@ export function SuggestedFeeds() { export function ProgressGuide() { const t = useTheme() + const {gtMobile} = useBreakpoints() return ( - + ) diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index bf567091b3..ca91665e94 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -31,8 +31,8 @@ import { import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass' -import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {boostInterests, InterestTabs} from '#/components/InterestTabs' import * as ProfileCard from '#/components/ProfileCard' @@ -60,10 +60,16 @@ type Item = key: string } -export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) { +export function FollowDialog({ + guide, + showArrow, +}: { + guide: Follow10ProgressGuide + showArrow?: boolean +}) { const {_} = useLingui() const control = Dialog.useDialogControl() - const {gtMobile} = useBreakpoints() + const {gtPhone} = useBreakpoints() const {height: minHeight} = useWindowDimensions() return ( @@ -74,13 +80,12 @@ export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) { control.open() logEvent('progressGuide:followDialog:open', {}) }} - size={gtMobile ? 'small' : 'large'} - color="primary" - variant="solid"> - + size={gtPhone ? 'small' : 'large'} + color="primary"> Find people to follow + {showArrow && } diff --git a/src/components/ProgressGuide/List.tsx b/src/components/ProgressGuide/List.tsx index cae307a6dd..81abc586f3 100644 --- a/src/components/ProgressGuide/List.tsx +++ b/src/components/ProgressGuide/List.tsx @@ -2,37 +2,62 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useProfileFollowsQuery} from '#/state/queries/profile-follows' +import {useSession} from '#/state/session' import { useProgressGuide, useProgressGuideControls, } from '#/state/shell/progress-guide' -import {atoms as a, useTheme} from '#/alf' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' +import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' import {Text} from '#/components/Typography' +import type * as bsky from '#/types/bsky' import {FollowDialog} from './FollowDialog' import {ProgressGuideTask} from './Task' +const TOTAL_AVATARS = 10 + export function ProgressGuideList({style}: {style?: StyleProp}) { const t = useTheme() const {_} = useLingui() + const {gtPhone} = useBreakpoints() + const {rightNavVisible} = useLayoutBreakpoints() + const {currentAccount} = useSession() const followProgressGuide = useProgressGuide('follow-10') const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') const guide = followProgressGuide || followAndLikeProgressGuide const {endProgressGuide} = useProgressGuideControls() + const {data: follows} = useProfileFollowsQuery(currentAccount?.did, { + limit: TOTAL_AVATARS, + }) + + const actualFollowsCount = follows?.pages?.[0]?.follows?.length ?? 0 + + // Hide if user already follows 10+ people + if (guide?.guide === 'follow-10' && actualFollowsCount >= TOTAL_AVATARS) { + return null + } + + // Inline layout when left nav visible but no right sidebar (800-1100px) + const inlineLayout = gtPhone && !rightNavVisible if (guide) { return ( - + - - Getting started + + Follow 10 people to get started {guide.guide === 'follow-10' && ( - <> - - - + + + + )} {guide.guide === 'like-10-and-follow-7' && ( <> @@ -76,3 +109,73 @@ export function ProgressGuideList({style}: {style?: StyleProp}) { } return null } + +function StackedAvatars({follows}: {follows?: bsky.profile.AnyProfileView[]}) { + const t = useTheme() + const {centerColumnOffset} = useLayoutBreakpoints() + + // Smaller avatars for narrower viewport + const avatarSize = centerColumnOffset ? 30 : 37 + const overlap = centerColumnOffset ? 9 : 11 + const iconSize = centerColumnOffset ? 14 : 18 + + // Use actual follows count, not the guide's event counter + const followedAvatars = follows?.slice(0, TOTAL_AVATARS) ?? [] + const remainingSlots = TOTAL_AVATARS - followedAvatars.length + + // Total width calculation: first avatar + (remaining * visible portion) + const totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap) + + return ( + + {/* Show followed user avatars */} + {followedAvatars.map((follow, i) => ( + + + + ))} + {/* Show placeholder avatars for remaining slots */} + {Array(remainingSlots) + .fill(0) + .map((_, i) => ( + + + + ))} + + ) +} diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx index 449a28fcd3..997c777af6 100644 --- a/src/components/ProgressGuide/Task.tsx +++ b/src/components/ProgressGuide/Task.tsx @@ -31,11 +31,11 @@ export function ProgressGuideTask({ size={20} thickness={3} borderWidth={0} - unfilledColor={t.palette.contrast_50} + unfilledColor={t.palette.contrast_100} /> )} - + @@ -93,6 +93,7 @@ export function TrendingTopic({ a.font_semi_bold, a.leading_tight, isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}], + hovered && {textDecorationLine: 'underline'}, ]} numberOfLines={1}> {topic.displayName} diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 2580ef28f3..3d47385ee8 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -99,10 +99,9 @@ export function Inner() { {topic.topic} diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 9d30288d40..94b362657b 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -4,12 +4,14 @@ import { type AppBskyActorGetProfile, type AppBskyActorGetProfiles, type AppBskyActorProfile, + type AppBskyGraphGetFollows, AtUri, type BskyAgent, type ComAtprotoRepoUploadBlob, type Un$Typed, } from '@atproto/api' import { + type InfiniteData, keepPreviousData, type QueryClient, useMutation, @@ -26,6 +28,7 @@ import {type Shadow} from '#/state/cache/types' import {type ImageMeta} from '#/state/gallery' import {STALE} from '#/state/queries' import {resetProfilePostsQueries} from '#/state/queries/post-feed' +import {RQKEY as PROFILE_FOLLOWS_RQKEY} from '#/state/queries/profile-follows' import { unstableCacheProfileView, useUnstableProfileViewCache, @@ -247,6 +250,7 @@ export function useProfileFollowMutationQueue( ) { const agent = useAgent() const queryClient = useQueryClient() + const {currentAccount} = useSession() const did = profile.did const initialFollowingUri = profile.viewer?.following const followMutation = useProfileFollowMutation( @@ -283,6 +287,47 @@ export function useProfileFollowMutationQueue( followingUri: finalFollowingUri, }) + // Optimistically update profile follows cache for avatar displays + if (currentAccount?.did) { + type FollowsQueryData = + InfiniteData + queryClient.setQueryData( + PROFILE_FOLLOWS_RQKEY(currentAccount.did), + old => { + if (!old?.pages?.[0]) return old + if (finalFollowingUri) { + // Add the followed profile to the beginning + const alreadyExists = old.pages[0].follows.some( + f => f.did === profile.did, + ) + if (alreadyExists) return old + return { + ...old, + pages: [ + { + ...old.pages[0], + follows: [ + profile as AppBskyActorDefs.ProfileView, + ...old.pages[0].follows, + ], + }, + ...old.pages.slice(1), + ], + } + } else { + // Remove the unfollowed profile + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + follows: page.follows.filter(f => f.did !== profile.did), + })), + } + } + }, + ) + } + if (finalFollowingUri) { agent.app.bsky.graph .getSuggestedFollowsByActor({ diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 641b90f3e3..57b078e0c1 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -1,4 +1,4 @@ -import {View} from 'react-native' +import {Pressable, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation, useNavigationState} from '@react-navigation/native' @@ -7,10 +7,18 @@ import {getCurrentRoute} from '#/lib/routes/helpers' import {type NavigationProp} from '#/lib/routes/types' import {logger} from '#/logger' import {emitSoftReset} from '#/state/events' -import {usePinnedFeedsInfos} from '#/state/queries/feed' +import { + type SavedFeedSourceInfo, + usePinnedFeedsInfos, +} from '#/state/queries/feed' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' -import {createStaticClick, InlineLinkText} from '#/components/Link' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' +import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' export function DesktopFeeds() { const t = useTheme() @@ -57,13 +65,12 @@ export function DesktopFeeds() { style={[ a.flex_1, web({ - gap: 10, + gap: 2, /* * Small padding prevents overflow prior to actually overflowing the * height of the screen with lots of feeds. */ - paddingVertical: 2, - marginHorizontal: -2, + paddingTop: 2, overflowY: 'auto', }), ]}> @@ -72,10 +79,11 @@ export function DesktopFeeds() { const current = route.name === 'Home' && feed === selectedFeed return ( - { + feedInfo={feedInfo} + current={current} + onPress={() => { logger.metric( 'desktopFeeds:feed:click', { @@ -89,39 +97,143 @@ export function DesktopFeeds() { if (route.name === 'Home' && feed === selectedFeed) { emitSoftReset() } - })} - style={[ - a.text_md, - a.leading_snug, - a.flex_shrink_0, - current - ? [a.font_semi_bold, t.atoms.text] - : [t.atoms.text_contrast_medium], - web({ - marginHorizontal: 2, - width: 'calc(100% - 4px)', - }), - ]} - numberOfLines={1}> - {feedInfo.displayName} - + }} + /> ) })} - - {_(msg`More feeds`)} - + a.flex_row, + a.align_center, + a.gap_sm, + a.self_start, + a.rounded_sm, + {paddingVertical: 6, paddingHorizontal: 8}, + route.name === 'Feeds' && {backgroundColor: t.palette.primary_50}, + ]}> + {({hovered}) => { + const isActive = route.name === 'Feeds' + return ( + <> + + + + + {_(msg`More feeds`)} + + + ) + }} + ) } + +function FeedItem({ + feedInfo, + current, + onPress, +}: { + feedInfo: SavedFeedSourceInfo + current: boolean + onPress: () => void +}) { + const t = useTheme() + const {_} = useLingui() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const isFollowing = feedInfo.feedDescriptor === 'following' + + return ( + + {isFollowing ? ( + + + + ) : ( + + )} + + {feedInfo.displayName} + + + ) +} diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 1d097fc9a8..788df6e64c 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -18,7 +18,6 @@ import { web, } from '#/alf' import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' -import {Divider} from '#/components/Divider' import {CENTER_COLUMN_OFFSET} from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import {ProgressGuideList} from '#/components/ProgressGuide/List' @@ -86,9 +85,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { {hasSession && ( <> - - + )} @@ -102,25 +100,31 @@ export function DesktopRightNav({routeName}: {routeName: string}) { email: currentAccount?.email, handle: currentAccount?.handle, })} + style={[t.atoms.text_contrast_medium]} label={_(msg`Feedback`)}> {_(msg`Feedback`)} - {' • '} + {' ∙ '} )} {_(msg`Privacy`)} - {' • '} + {' ∙ '} {_(msg`Terms`)} - {' • '} - + {' ∙ '} + {_(msg`Help`)} diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx index 11dcff3a4d..913b9ccab9 100644 --- a/src/view/shell/desktop/SidebarTrendingTopics.tsx +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -1,9 +1,8 @@ -import React from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {logEvent} from '#/lib/statsig/statsig' +import {logger} from '#/logger' import { useTrendingSettings, useTrendingSettingsApi, @@ -12,18 +11,13 @@ import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' import {useTrendingConfig} from '#/state/service-config' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' -import {Divider} from '#/components/Divider' -import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' +import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' +import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending' import * as Prompt from '#/components/Prompt' -import { - TrendingTopic, - TrendingTopicLink, - TrendingTopicSkeleton, -} from '#/components/TrendingTopics' +import {TrendingTopicLink} from '#/components/TrendingTopics' import {Text} from '#/components/Typography' -const TRENDING_LIMIT = 6 +const TRENDING_LIMIT = 5 export function SidebarTrendingTopics() { const {enabled} = useTrendingConfig() @@ -39,64 +33,88 @@ function Inner() { const {data: trending, error, isLoading} = useTrendingTopics() const noTopics = !isLoading && !error && !trending?.topics?.length - const onConfirmHide = React.useCallback(() => { - logEvent('trendingTopics:hide', {context: 'sidebar'}) + const onConfirmHide = () => { + logger.metric('trendingTopics:hide', {context: 'sidebar'}) setTrendingDisabled(true) - }, [setTrendingDisabled]) + } return error || noTopics ? null : ( <> - - - - + + + + Trending - + {isLoading ? ( Array(TRENDING_LIMIT) .fill(0) .map((_n, i) => ( - + + + {i + 1}. + + + )) ) : !trending?.topics ? null : ( <> - {trending.topics.slice(0, TRENDING_LIMIT).map(topic => ( + {trending.topics.slice(0, TRENDING_LIMIT).map((topic, i) => ( { - logEvent('trendingTopic:click', {context: 'sidebar'}) + logger.metric('trendingTopic:click', {context: 'sidebar'}) }}> {({hovered}) => ( - + + + {i + 1}. + + + {topic.displayName ?? topic.topic} + + )} ))} @@ -111,7 +129,6 @@ function Inner() { confirmButtonCta={_(msg`Hide`)} onConfirm={onConfirmHide} /> - ) } From 2818c7f5088067e29478c5c5c7ebed30dcebe82c Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Sat, 10 Jan 2026 02:43:34 +0000 Subject: [PATCH 07/17] Nightly source-language update --- src/locale/locales/en/messages.po | 364 +++++++++++++++++------------- 1 file changed, 203 insertions(+), 161 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 5291e9ec0c..d920c4dbe4 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -89,12 +89,12 @@ msgstr "" msgid "{0, plural, one {#mo} other {#mo}}" msgstr "" -#: src/components/ProfileHoverCard/index.web.tsx:444 +#: src/components/ProfileHoverCard/index.web.tsx:445 #: src/screens/Profile/Header/Metrics.tsx:22 msgid "{0, plural, one {follower} other {followers}}" msgstr "" -#: src/components/ProfileHoverCard/index.web.tsx:448 +#: src/components/ProfileHoverCard/index.web.tsx:449 #: src/screens/Profile/Header/Metrics.tsx:26 msgid "{0, plural, one {following} other {following}}" msgstr "" @@ -155,7 +155,7 @@ msgstr "" msgid "{0} following" msgstr "" -#: src/components/live/LiveStatusDialog.tsx:68 +#: src/components/live/LiveStatusDialog.tsx:79 msgid "{0} is live" msgstr "" @@ -406,7 +406,7 @@ msgstr "" msgid "{firstAuthorName} verified you" msgstr "" -#: src/components/ProfileHoverCard/index.web.tsx:577 +#: src/components/ProfileHoverCard/index.web.tsx:578 msgid "{following} following" msgstr "" @@ -623,18 +623,18 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:393 #: src/screens/Messages/components/RequestButtons.tsx:91 -#: src/view/com/profile/ProfileMenu.tsx:166 +#: src/view/com/profile/ProfileMenu.tsx:169 msgctxt "toast" msgid "Account blocked" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:179 +#: src/view/com/profile/ProfileMenu.tsx:182 msgctxt "toast" msgid "Account followed" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:416 -#: src/view/com/profile/ProfileMenu.tsx:142 +#: src/view/com/profile/ProfileMenu.tsx:145 msgctxt "toast" msgid "Account muted" msgstr "" @@ -662,18 +662,18 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:84 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:290 -#: src/view/com/profile/ProfileMenu.tsx:156 +#: src/view/com/profile/ProfileMenu.tsx:159 msgctxt "toast" msgid "Account unblocked" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:191 +#: src/view/com/profile/ProfileMenu.tsx:194 msgctxt "toast" msgid "Account unfollowed" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:406 -#: src/view/com/profile/ProfileMenu.tsx:132 +#: src/view/com/profile/ProfileMenu.tsx:135 msgctxt "toast" msgid "Account unmuted" msgstr "" @@ -779,8 +779,8 @@ msgstr "" msgid "Add media to post" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:506 -#: src/components/moderation/ReportDialog/index.tsx:510 +#: src/components/moderation/ReportDialog/index.tsx:520 +#: src/components/moderation/ReportDialog/index.tsx:524 msgid "Add more details (optional)" msgstr "" @@ -825,8 +825,8 @@ msgstr "" msgid "Add this feed to your feeds" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:322 -#: src/view/com/profile/ProfileMenu.tsx:325 +#: src/view/com/profile/ProfileMenu.tsx:323 +#: src/view/com/profile/ProfileMenu.tsx:326 msgid "Add to lists" msgstr "" @@ -835,8 +835,8 @@ msgid "Add to saved posts" msgstr "" #: src/components/dialogs/StarterPackDialog.tsx:176 -#: src/view/com/profile/ProfileMenu.tsx:313 -#: src/view/com/profile/ProfileMenu.tsx:316 +#: src/view/com/profile/ProfileMenu.tsx:314 +#: src/view/com/profile/ProfileMenu.tsx:317 msgid "Add to starter packs" msgstr "" @@ -861,7 +861,7 @@ msgstr "" msgid "Additional details (limit 1000 characters)" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:524 +#: src/components/moderation/ReportDialog/index.tsx:538 msgid "Additional details (limit 300 characters)" msgstr "" @@ -1252,6 +1252,11 @@ msgstr "" msgid "Appeal \"{0}\" label" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:92 +msgid "Appeal livestream suspension" +msgstr "" + +#: src/components/live/GoLiveDisabledDialog.tsx:82 #: src/components/moderation/LabelsOnMeDialog.tsx:271 #: src/screens/Messages/components/ChatDisabled.tsx:103 msgctxt "toast" @@ -1462,7 +1467,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:792 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 -#: src/view/com/profile/ProfileMenu.tsx:495 +#: src/view/com/profile/ProfileMenu.tsx:504 msgid "Block" msgstr "" @@ -1472,13 +1477,13 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:679 #: src/screens/Messages/components/RequestButtons.tsx:144 #: src/screens/Messages/components/RequestButtons.tsx:146 -#: src/view/com/profile/ProfileMenu.tsx:401 -#: src/view/com/profile/ProfileMenu.tsx:408 +#: src/view/com/profile/ProfileMenu.tsx:410 +#: src/view/com/profile/ProfileMenu.tsx:417 msgid "Block account" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:787 -#: src/view/com/profile/ProfileMenu.tsx:478 +#: src/view/com/profile/ProfileMenu.tsx:487 msgid "Block Account?" msgstr "" @@ -1530,7 +1535,7 @@ msgid "Blocked Accounts" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:789 -#: src/view/com/profile/ProfileMenu.tsx:490 +#: src/view/com/profile/ProfileMenu.tsx:499 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" @@ -1546,7 +1551,7 @@ msgstr "" msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:487 +#: src/view/com/profile/ProfileMenu.tsx:496 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." msgstr "" @@ -1580,8 +1585,7 @@ msgstr "" msgid "Bluesky is an open network where you can choose your own provider. If you're new here, we recommend sticking with the default Bluesky Social option." msgstr "" -#: src/components/ProgressGuide/List.tsx:53 -#: src/components/ProgressGuide/List.tsx:70 +#: src/components/ProgressGuide/List.tsx:103 msgid "Bluesky is better with friends!" msgstr "" @@ -1668,15 +1672,15 @@ msgstr "" msgid "Browse other feeds" msgstr "" -#: src/components/TrendingTopics.tsx:178 +#: src/components/TrendingTopics.tsx:179 msgid "Browse posts about {displayName}" msgstr "" -#: src/components/TrendingTopics.tsx:186 +#: src/components/TrendingTopics.tsx:187 msgid "Browse posts tagged with {displayName}" msgstr "" -#: src/components/TrendingTopics.tsx:195 +#: src/components/TrendingTopics.tsx:196 msgid "Browse starter pack {displayName}" msgstr "" @@ -1684,7 +1688,7 @@ msgstr "" msgid "Browse topic {0}" msgstr "" -#: src/components/TrendingTopics.tsx:232 +#: src/components/TrendingTopics.tsx:233 msgid "Browse topic {displayName}" msgstr "" @@ -1697,7 +1701,7 @@ msgid "Button to go back to the home timeline" msgstr "" #: src/components/LabelingServiceCard/index.tsx:62 -#: src/components/moderation/ReportDialog/index.tsx:833 +#: src/components/moderation/ReportDialog/index.tsx:842 #: src/screens/Search/components/StarterPackCard.tsx:106 #: src/screens/Search/Explore.tsx:936 msgid "By {0}" @@ -1837,7 +1841,7 @@ msgstr "" msgid "Change Handle" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:423 +#: src/components/moderation/ReportDialog/index.tsx:437 msgid "Change moderation service" msgstr "" @@ -1850,11 +1854,11 @@ msgstr "" msgid "Change password dialog" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:288 +#: src/components/moderation/ReportDialog/index.tsx:302 msgid "Change report category" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:368 +#: src/components/moderation/ReportDialog/index.tsx:382 msgid "Change report reason" msgstr "" @@ -2086,7 +2090,7 @@ msgstr "" #: src/components/NewskieDialog.tsx:167 #: src/components/NewskieDialog.tsx:173 #: src/components/Post/Embed/ExternalEmbed/Gif.tsx:208 -#: src/components/ProgressGuide/FollowDialog.tsx:440 +#: src/components/ProgressGuide/FollowDialog.tsx:445 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:118 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:124 #: src/components/verification/VerificationsDialog.tsx:144 @@ -2125,7 +2129,7 @@ msgstr "" msgid "Close dialog" msgstr "" -#: src/view/shell/index.web.tsx:128 +#: src/view/shell/index.web.tsx:130 msgid "Close drawer menu" msgstr "" @@ -2439,8 +2443,8 @@ msgstr "" msgid "Copy App Password" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:438 -#: src/view/com/profile/ProfileMenu.tsx:441 +#: src/view/com/profile/ProfileMenu.tsx:447 +#: src/view/com/profile/ProfileMenu.tsx:450 msgid "Copy at:// URI" msgstr "" @@ -2455,8 +2459,8 @@ msgid "Copy code" msgstr "" #: src/screens/Settings/components/ChangeHandleDialog.tsx:502 -#: src/view/com/profile/ProfileMenu.tsx:447 -#: src/view/com/profile/ProfileMenu.tsx:450 +#: src/view/com/profile/ProfileMenu.tsx:456 +#: src/view/com/profile/ProfileMenu.tsx:459 msgid "Copy DID" msgstr "" @@ -2485,8 +2489,8 @@ msgstr "" msgid "Copy link to post" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:251 -#: src/view/com/profile/ProfileMenu.tsx:262 +#: src/view/com/profile/ProfileMenu.tsx:252 +#: src/view/com/profile/ProfileMenu.tsx:263 msgid "Copy link to profile" msgstr "" @@ -2668,8 +2672,8 @@ msgid "Create new account" msgstr "" #. Accessibility label for button to create a moderation report for the selected option -#: src/components/moderation/ReportDialog/index.tsx:688 -#: src/components/moderation/ReportDialog/index.tsx:734 +#: src/components/moderation/ReportDialog/index.tsx:702 +#: src/components/moderation/ReportDialog/index.tsx:748 msgid "Create report for {0}" msgstr "" @@ -3010,7 +3014,7 @@ msgstr "" msgid "Dismiss error" msgstr "" -#: src/components/ProgressGuide/List.tsx:42 +#: src/components/ProgressGuide/List.tsx:67 msgid "Dismiss getting started guide" msgstr "" @@ -3225,8 +3229,8 @@ msgstr "" msgid "Edit list details" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:334 -#: src/view/com/profile/ProfileMenu.tsx:340 +#: src/view/com/profile/ProfileMenu.tsx:337 +#: src/view/com/profile/ProfileMenu.tsx:349 msgid "Edit live status" msgstr "" @@ -3821,6 +3825,7 @@ msgstr "" msgid "Failed to send email, please try again." msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:76 #: src/components/moderation/LabelsOnMeDialog.tsx:265 #: src/screens/Messages/components/ChatDisabled.tsx:99 msgid "Failed to submit appeal, please try again." @@ -3907,8 +3912,8 @@ msgstr "" msgid "Feed unavailable" msgstr "" +#: src/view/shell/desktop/RightNav.tsx:104 #: src/view/shell/desktop/RightNav.tsx:105 -#: src/view/shell/desktop/RightNav.tsx:106 #: src/view/shell/Drawer.tsx:368 msgid "Feedback" msgstr "" @@ -4010,9 +4015,9 @@ msgid "Find my friends" msgstr "" #. Starter packs suggested to the user for them to follow -#: src/components/ProgressGuide/FollowDialog.tsx:72 -#: src/components/ProgressGuide/FollowDialog.tsx:82 -#: src/components/ProgressGuide/FollowDialog.tsx:426 +#: src/components/ProgressGuide/FollowDialog.tsx:78 +#: src/components/ProgressGuide/FollowDialog.tsx:86 +#: src/components/ProgressGuide/FollowDialog.tsx:431 #: src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx:42 msgid "Find people to follow" msgstr "" @@ -4063,8 +4068,8 @@ msgstr "" #. User is not following this account, click to follow #: src/components/ProfileCard.tsx:559 -#: src/components/ProfileHoverCard/index.web.tsx:496 -#: src/components/ProfileHoverCard/index.web.tsx:507 +#: src/components/ProfileHoverCard/index.web.tsx:497 +#: src/components/ProfileHoverCard/index.web.tsx:508 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:131 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:378 #: src/screens/VideoFeed/index.tsx:879 @@ -4082,17 +4087,20 @@ msgstr "" msgid "Follow {handle}" msgstr "" -#: src/components/ProgressGuide/List.tsx:52 #: src/state/shell/progress-guide.tsx:231 msgid "Follow 10 accounts" msgstr "" -#: src/components/ProgressGuide/List.tsx:69 +#: src/components/ProgressGuide/List.tsx:60 +msgid "Follow 10 people to get started" +msgstr "" + +#: src/components/ProgressGuide/List.tsx:102 msgid "Follow 7 accounts" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:292 -#: src/view/com/profile/ProfileMenu.tsx:303 +#: src/view/com/profile/ProfileMenu.tsx:293 +#: src/view/com/profile/ProfileMenu.tsx:304 msgid "Follow account" msgstr "" @@ -4157,8 +4165,8 @@ msgstr "" #. User is following this account, click to unfollow #. User is following this account, click to unfollow #: src/components/ProfileCard.tsx:546 -#: src/components/ProfileHoverCard/index.web.tsx:495 -#: src/components/ProfileHoverCard/index.web.tsx:506 +#: src/components/ProfileHoverCard/index.web.tsx:496 +#: src/components/ProfileHoverCard/index.web.tsx:507 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:134 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:374 #: src/screens/VideoFeed/index.tsx:877 @@ -4341,10 +4349,6 @@ msgstr "" msgid "Get started" msgstr "" -#: src/components/ProgressGuide/List.tsx:35 -msgid "Getting started" -msgstr "" - #: src/components/MediaPreview.tsx:114 msgid "GIF" msgstr "" @@ -4407,8 +4411,8 @@ msgstr "" msgid "Go Home" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:335 -#: src/view/com/profile/ProfileMenu.tsx:342 +#: src/view/com/profile/ProfileMenu.tsx:338 +#: src/view/com/profile/ProfileMenu.tsx:351 msgid "Go live" msgstr "" @@ -4419,6 +4423,11 @@ msgstr "" msgid "Go Live" msgstr "" +#: src/view/com/profile/ProfileMenu.tsx:335 +#: src/view/com/profile/ProfileMenu.tsx:347 +msgid "Go live (disabled)" +msgstr "" + #: src/components/live/GoLiveDialog.tsx:171 msgid "Go live for" msgstr "" @@ -4456,6 +4465,10 @@ msgstr "" msgid "Go to user's profile" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:104 +msgid "Going live is currently disabled for your account" +msgstr "" + #: src/lib/moderation/useGlobalLabelStrings.ts:46 #: src/lib/moderation/useGlobalLabelStrings.ts:50 #: src/view/com/composer/labels/LabelsBtn.tsx:196 @@ -4545,8 +4558,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:254 #: src/screens/Settings/Settings.tsx:258 -#: src/view/shell/desktop/RightNav.tsx:123 -#: src/view/shell/desktop/RightNav.tsx:124 +#: src/view/shell/desktop/RightNav.tsx:125 +#: src/view/shell/desktop/RightNav.tsx:128 #: src/view/shell/Drawer.tsx:381 msgid "Help" msgstr "" @@ -4585,7 +4598,7 @@ msgstr "" msgid "Hidden list" msgstr "" -#: src/components/interstitials/Trending.tsx:131 +#: src/components/interstitials/Trending.tsx:130 #: src/components/interstitials/TrendingVideos.tsx:138 #: src/components/moderation/ContentHider.tsx:208 #: src/components/moderation/LabelPreference.tsx:140 @@ -4595,7 +4608,7 @@ msgstr "" #: src/lib/moderation/useLabelBehaviorDescription.ts:23 #: src/lib/moderation/useLabelBehaviorDescription.ts:28 #: src/lib/moderation/useLabelBehaviorDescription.ts:33 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:111 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:129 msgid "Hide" msgstr "" @@ -4640,13 +4653,12 @@ msgstr "" msgid "Hide this reply?" msgstr "" -#: src/components/interstitials/Trending.tsx:113 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:62 +#: src/components/interstitials/Trending.tsx:112 msgid "Hide trending topics" msgstr "" -#: src/components/interstitials/Trending.tsx:129 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:109 +#: src/components/interstitials/Trending.tsx:128 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:127 msgid "Hide trending topics?" msgstr "" @@ -4965,7 +4977,7 @@ msgstr "" msgid "Invalid phone number" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:80 +#: src/components/moderation/ReportDialog/index.tsx:92 msgid "Invalid report subject" msgstr "" @@ -5261,7 +5273,7 @@ msgstr "" msgid "Like ({0, plural, one {# like} other {# likes}})" msgstr "" -#: src/components/ProgressGuide/List.tsx:63 +#: src/components/ProgressGuide/List.tsx:96 msgid "Like 10 posts" msgstr "" @@ -5433,8 +5445,8 @@ msgstr "" msgid "LIVE" msgstr "" -#: src/components/live/LiveStatusDialog.tsx:215 -msgid "Live feature is in beta testing" +#: src/components/live/LiveStatusDialog.tsx:235 +msgid "Live feature is in beta" msgstr "" #: src/components/live/EditLiveDialog.tsx:148 @@ -5488,11 +5500,11 @@ msgstr "" msgid "Logged-out visibility" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:133 +#: src/view/shell/desktop/RightNav.tsx:137 msgid "Logo by @sawaratsuki.bsky.social" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:130 +#: src/view/shell/desktop/RightNav.tsx:134 #: src/view/shell/Drawer.tsx:709 msgid "Logo by <0>@sawaratsuki.bsky.social" msgstr "" @@ -5706,8 +5718,8 @@ msgstr "" msgid "Moderator has chosen to set a general warning on the content." msgstr "" -#: src/view/shell/desktop/Feeds.tsx:113 -#: src/view/shell/desktop/Feeds.tsx:123 +#: src/view/shell/desktop/Feeds.tsx:107 +#: src/view/shell/desktop/Feeds.tsx:154 msgid "More feeds" msgstr "" @@ -5717,8 +5729,8 @@ msgid "More languages..." msgstr "" #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:149 -#: src/view/com/profile/ProfileMenu.tsx:228 -#: src/view/com/profile/ProfileMenu.tsx:234 +#: src/view/com/profile/ProfileMenu.tsx:229 +#: src/view/com/profile/ProfileMenu.tsx:235 msgid "More options" msgstr "" @@ -5751,8 +5763,8 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:660 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:666 -#: src/view/com/profile/ProfileMenu.tsx:380 -#: src/view/com/profile/ProfileMenu.tsx:387 +#: src/view/com/profile/ProfileMenu.tsx:389 +#: src/view/com/profile/ProfileMenu.tsx:396 msgid "Mute account" msgstr "" @@ -5872,8 +5884,8 @@ msgstr "" msgid "Navigates to your profile" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:319 -#: src/components/moderation/ReportDialog/index.tsx:336 +#: src/components/moderation/ReportDialog/index.tsx:333 +#: src/components/moderation/ReportDialog/index.tsx:350 msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?" msgstr "" @@ -6137,7 +6149,7 @@ msgid "No result" msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:223 -#: src/components/ProgressGuide/FollowDialog.tsx:224 +#: src/components/ProgressGuide/FollowDialog.tsx:229 msgid "No results" msgstr "" @@ -6226,7 +6238,7 @@ msgstr "" msgid "Not Found" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:502 +#: src/view/com/profile/ProfileMenu.tsx:511 msgid "Note about sharing" msgstr "" @@ -6459,8 +6471,8 @@ msgstr "" msgid "Open post options menu" msgstr "" -#: src/components/live/LiveStatusDialog.tsx:189 -#: src/components/live/LiveStatusDialog.tsx:203 +#: src/components/live/LiveStatusDialog.tsx:205 +#: src/components/live/LiveStatusDialog.tsx:219 msgid "Open profile" msgstr "" @@ -6481,6 +6493,10 @@ msgstr "" msgid "Open system log" msgstr "" +#: src/view/shell/desktop/Feeds.tsx:186 +msgid "Opens {0} feed" +msgstr "" + #: src/view/com/composer/labels/LabelsBtn.tsx:62 msgid "Opens a dialog to add a content warning to your post" msgstr "" @@ -6939,6 +6955,10 @@ msgstr "" msgid "Please explain why you think your chats were incorrectly disabled" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:123 +msgid "Please explain why you think your Go Live access was incorrectly disabled." +msgstr "" + #: src/components/FocusScope/index.tsx:91 #: src/components/FocusScope/index.tsx:115 msgid "Please go back, or activate this element to return to the start of the active content." @@ -7548,8 +7568,8 @@ msgstr "" #: src/components/verification/VerificationRemovePrompt.tsx:46 #: src/components/verification/VerificationsDialog.tsx:252 -#: src/view/com/profile/ProfileMenu.tsx:353 -#: src/view/com/profile/ProfileMenu.tsx:356 +#: src/view/com/profile/ProfileMenu.tsx:362 +#: src/view/com/profile/ProfileMenu.tsx:365 msgid "Remove verification" msgstr "" @@ -7680,11 +7700,12 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:168 #: src/components/dms/MessagesListBlockedFooter.tsx:85 #: src/components/dms/MessagesListBlockedFooter.tsx:92 +#: src/components/live/LiveStatusDialog.tsx:257 msgid "Report" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:420 -#: src/view/com/profile/ProfileMenu.tsx:423 +#: src/view/com/profile/ProfileMenu.tsx:429 +#: src/view/com/profile/ProfileMenu.tsx:432 msgid "Report account" msgstr "" @@ -7696,8 +7717,8 @@ msgstr "" msgid "Report conversation" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:78 -#: src/components/moderation/ReportDialog/index.tsx:239 +#: src/components/moderation/ReportDialog/index.tsx:90 +#: src/components/moderation/ReportDialog/index.tsx:253 msgid "Report dialog" msgstr "" @@ -7730,27 +7751,32 @@ msgstr "" msgid "Report submitted" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:45 +#: src/components/moderation/ReportDialog/copy.ts:51 msgid "Report this conversation" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:31 +#: src/components/moderation/ReportDialog/copy.ts:37 msgid "Report this feed" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:25 +#: src/components/moderation/ReportDialog/copy.ts:31 msgid "Report this list" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:51 +#: src/components/live/LiveStatusDialog.tsx:240 +#: src/components/moderation/ReportDialog/copy.ts:19 +msgid "Report this livestream" +msgstr "" + +#: src/components/moderation/ReportDialog/copy.ts:57 msgid "Report this message" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:19 +#: src/components/moderation/ReportDialog/copy.ts:25 msgid "Report this post" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:37 +#: src/components/moderation/ReportDialog/copy.ts:43 msgid "Report this starter pack" msgstr "" @@ -7903,7 +7929,7 @@ msgstr "" #: src/components/dms/MessageItem.tsx:322 #: src/components/Error.tsx:65 #: src/components/Lists.tsx:114 -#: src/components/moderation/ReportDialog/index.tsx:273 +#: src/components/moderation/ReportDialog/index.tsx:287 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:58 #: src/components/StarterPack/ProfileStarterPacks.tsx:374 @@ -7925,7 +7951,7 @@ msgstr "" msgid "Retry" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:270 +#: src/components/moderation/ReportDialog/index.tsx:284 #: src/view/screens/Storybook/Admonitions.tsx:60 msgid "Retry loading report options" msgstr "" @@ -8074,7 +8100,7 @@ msgstr "" msgid "Search @{0}'s posts" msgstr "" -#: src/components/ProgressGuide/FollowDialog.tsx:657 +#: src/components/ProgressGuide/FollowDialog.tsx:662 msgid "Search by name or interest" msgstr "" @@ -8087,12 +8113,12 @@ msgid "Search feeds" msgstr "" #. Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is not currently active and can be selected. -#: src/components/ProgressGuide/FollowDialog.tsx:486 +#: src/components/ProgressGuide/FollowDialog.tsx:491 msgid "Search for \"{interestsDisplayName}\"" msgstr "" #. Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is currently selected. -#: src/components/ProgressGuide/FollowDialog.tsx:479 +#: src/components/ProgressGuide/FollowDialog.tsx:484 msgid "Search for \"{interestsDisplayName}\" (active)" msgstr "" @@ -8138,13 +8164,13 @@ msgstr "" msgid "Search my posts" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:271 -#: src/view/com/profile/ProfileMenu.tsx:274 +#: src/view/com/profile/ProfileMenu.tsx:272 +#: src/view/com/profile/ProfileMenu.tsx:275 msgid "Search posts" msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:534 -#: src/components/ProgressGuide/FollowDialog.tsx:676 +#: src/components/ProgressGuide/FollowDialog.tsx:681 msgid "Search profiles" msgstr "" @@ -8157,7 +8183,7 @@ msgid "Search..." msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:535 -#: src/components/ProgressGuide/FollowDialog.tsx:677 +#: src/components/ProgressGuide/FollowDialog.tsx:682 msgid "Searches for profiles" msgstr "" @@ -8225,7 +8251,7 @@ msgstr "" msgid "Select a color" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:358 +#: src/components/moderation/ReportDialog/index.tsx:372 msgid "Select a reason" msgstr "" @@ -8300,7 +8326,7 @@ msgstr "" msgid "Select languages" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:408 +#: src/components/moderation/ReportDialog/index.tsx:422 msgid "Select moderation service" msgstr "" @@ -8404,7 +8430,7 @@ msgstr "" msgid "Send post to..." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:799 +#: src/components/moderation/ReportDialog/index.tsx:813 msgid "Send report to {title}" msgstr "" @@ -8549,7 +8575,7 @@ msgstr "" msgid "Share a fun fact!" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:507 +#: src/view/com/profile/ProfileMenu.tsx:516 msgid "Share anyway" msgstr "" @@ -8597,8 +8623,8 @@ msgstr "" #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:171 #: src/screens/StarterPack/StarterPackScreen.tsx:613 #: src/screens/StarterPack/StarterPackScreen.tsx:621 -#: src/view/com/profile/ProfileMenu.tsx:251 -#: src/view/com/profile/ProfileMenu.tsx:264 +#: src/view/com/profile/ProfileMenu.tsx:252 +#: src/view/com/profile/ProfileMenu.tsx:265 msgid "Share via..." msgstr "" @@ -8875,7 +8901,7 @@ msgstr "" msgid "Someone reacted {0} to {1}" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:83 +#: src/components/moderation/ReportDialog/index.tsx:95 msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" @@ -8884,7 +8910,7 @@ msgid "Something went wrong" msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:138 -#: src/components/moderation/ReportDialog/index.tsx:265 +#: src/components/moderation/ReportDialog/index.tsx:279 #: src/screens/Deactivated.tsx:85 #: src/screens/Settings/components/DeactivateAccountDialog.tsx:59 #: src/view/screens/Storybook/Admonitions.tsx:55 @@ -8904,7 +8930,7 @@ msgstr "" msgid "Something went wrong. Please try again in a moment." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:219 +#: src/components/moderation/ReportDialog/index.tsx:233 msgid "Something went wrong. Please try again." msgstr "" @@ -9034,6 +9060,8 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:117 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:123 +#: src/components/live/GoLiveDisabledDialog.tsx:138 +#: src/components/live/GoLiveDisabledDialog.tsx:139 #: src/components/moderation/LabelsOnMeDialog.tsx:342 #: src/components/moderation/LabelsOnMeDialog.tsx:343 #: src/screens/Messages/components/ChatDisabled.tsx:154 @@ -9049,9 +9077,9 @@ msgstr "" msgid "Submit Appeal" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:486 -#: src/components/moderation/ReportDialog/index.tsx:547 -#: src/components/moderation/ReportDialog/index.tsx:554 +#: src/components/moderation/ReportDialog/index.tsx:500 +#: src/components/moderation/ReportDialog/index.tsx:561 +#: src/components/moderation/ReportDialog/index.tsx:568 msgid "Submit report" msgstr "" @@ -9208,7 +9236,7 @@ msgstr "" msgid "Task complete - 10 likes!" msgstr "" -#: src/components/ProgressGuide/List.tsx:64 +#: src/components/ProgressGuide/List.tsx:97 msgid "Teach our algorithm what you like" msgstr "" @@ -9228,8 +9256,8 @@ msgstr "" msgid "Tell us a little more" msgstr "" -#: src/view/shell/desktop/RightNav.tsx:119 #: src/view/shell/desktop/RightNav.tsx:120 +#: src/view/shell/desktop/RightNav.tsx:121 msgid "Terms" msgstr "" @@ -9249,6 +9277,7 @@ msgstr "" msgid "Text & tags" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:121 #: src/components/moderation/LabelsOnMeDialog.tsx:306 #: src/screens/Messages/components/ChatDisabled.tsx:120 msgid "Text input field" @@ -9294,7 +9323,7 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:186 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:391 -#: src/view/com/profile/ProfileMenu.tsx:483 +#: src/view/com/profile/ProfileMenu.tsx:492 msgid "The account will be able to interact with you after unblocking." msgstr "" @@ -9477,12 +9506,12 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:253 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:279 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:294 -#: src/view/com/profile/ProfileMenu.tsx:136 -#: src/view/com/profile/ProfileMenu.tsx:146 -#: src/view/com/profile/ProfileMenu.tsx:160 -#: src/view/com/profile/ProfileMenu.tsx:170 -#: src/view/com/profile/ProfileMenu.tsx:183 -#: src/view/com/profile/ProfileMenu.tsx:195 +#: src/view/com/profile/ProfileMenu.tsx:139 +#: src/view/com/profile/ProfileMenu.tsx:149 +#: src/view/com/profile/ProfileMenu.tsx:163 +#: src/view/com/profile/ProfileMenu.tsx:173 +#: src/view/com/profile/ProfileMenu.tsx:186 +#: src/view/com/profile/ProfileMenu.tsx:198 msgid "There was an issue! {0}" msgstr "" @@ -9545,6 +9574,7 @@ msgstr "" msgid "This appeal will be sent to <0>{sourceName}." msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:113 #: src/screens/Messages/components/ChatDisabled.tsx:116 msgid "This appeal will be sent to Bluesky's moderation service." msgstr "" @@ -9702,7 +9732,7 @@ msgstr "" msgid "This post's author has disabled quote posts." msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:504 +#: src/view/com/profile/ProfileMenu.tsx:513 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in." msgstr "" @@ -9874,10 +9904,14 @@ msgstr "" msgid "Tree view" msgstr "" -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:59 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:48 msgid "Trending" msgstr "" +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:55 +msgid "Trending options" +msgstr "" + #: src/components/interstitials/TrendingVideos.tsx:86 msgid "Trending Videos" msgstr "" @@ -9967,7 +10001,7 @@ msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:394 #: src/screens/ProfileList/components/Header.tsx:171 #: src/screens/ProfileList/components/Header.tsx:178 -#: src/view/com/profile/ProfileMenu.tsx:495 +#: src/view/com/profile/ProfileMenu.tsx:504 msgid "Unblock" msgstr "" @@ -9978,14 +10012,14 @@ msgstr "" #: src/components/dms/ConvoMenu.tsx:261 #: src/components/dms/ConvoMenu.tsx:264 -#: src/view/com/profile/ProfileMenu.tsx:400 -#: src/view/com/profile/ProfileMenu.tsx:406 +#: src/view/com/profile/ProfileMenu.tsx:409 +#: src/view/com/profile/ProfileMenu.tsx:415 msgid "Unblock account" msgstr "" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:184 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:389 -#: src/view/com/profile/ProfileMenu.tsx:477 +#: src/view/com/profile/ProfileMenu.tsx:486 msgid "Unblock Account?" msgstr "" @@ -10013,8 +10047,8 @@ msgstr "" msgid "Unfollow {0}" msgstr "" -#: src/view/com/profile/ProfileMenu.tsx:291 -#: src/view/com/profile/ProfileMenu.tsx:301 +#: src/view/com/profile/ProfileMenu.tsx:292 +#: src/view/com/profile/ProfileMenu.tsx:302 msgid "Unfollow account" msgstr "" @@ -10022,7 +10056,7 @@ msgstr "" msgid "Unfollows the user" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:470 +#: src/components/moderation/ReportDialog/index.tsx:484 msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" @@ -10073,8 +10107,8 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:659 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:665 -#: src/view/com/profile/ProfileMenu.tsx:379 -#: src/view/com/profile/ProfileMenu.tsx:385 +#: src/view/com/profile/ProfileMenu.tsx:388 +#: src/view/com/profile/ProfileMenu.tsx:394 msgid "Unmute account" msgstr "" @@ -10382,8 +10416,8 @@ msgstr "" #: src/components/verification/VerificationCreatePrompt.tsx:84 #: src/components/verification/VerificationCreatePrompt.tsx:86 -#: src/view/com/profile/ProfileMenu.tsx:363 -#: src/view/com/profile/ProfileMenu.tsx:366 +#: src/view/com/profile/ProfileMenu.tsx:372 +#: src/view/com/profile/ProfileMenu.tsx:375 msgid "Verify account" msgstr "" @@ -10539,7 +10573,7 @@ msgstr "" msgid "View {displayName}'s profile" msgstr "" -#: src/components/ProfileHoverCard/index.web.tsx:480 +#: src/components/ProfileHoverCard/index.web.tsx:481 msgid "View blocked user's profile" msgstr "" @@ -10579,9 +10613,9 @@ msgstr "" msgid "View post" msgstr "" -#: src/components/ProfileHoverCard/index.web.tsx:466 -#: src/components/ProfileHoverCard/index.web.tsx:486 -#: src/components/ProfileHoverCard/index.web.tsx:513 +#: src/components/ProfileHoverCard/index.web.tsx:467 +#: src/components/ProfileHoverCard/index.web.tsx:487 +#: src/components/ProfileHoverCard/index.web.tsx:514 #: src/view/com/posts/PostFeedErrorMessage.tsx:182 #: src/view/com/util/PostMeta.tsx:90 #: src/view/com/util/PostMeta.tsx:127 @@ -10677,8 +10711,8 @@ msgstr "" msgid "Warn content and filter from feeds" msgstr "" -#: src/components/live/LiveStatusDialog.tsx:156 -#: src/components/live/LiveStatusDialog.tsx:169 +#: src/components/live/LiveStatusDialog.tsx:172 +#: src/components/live/LiveStatusDialog.tsx:185 msgid "Watch now" msgstr "" @@ -10805,7 +10839,7 @@ msgid "We're having issues initializing the age assurance process for your accou msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:107 -#: src/components/ProgressGuide/FollowDialog.tsx:184 +#: src/components/ProgressGuide/FollowDialog.tsx:189 msgid "We're having network issues, try again" msgstr "" @@ -10928,27 +10962,31 @@ msgstr "" msgid "Why are you appealing?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:46 +#: src/components/moderation/ReportDialog/copy.ts:52 msgid "Why should this conversation be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:32 +#: src/components/moderation/ReportDialog/copy.ts:38 msgid "Why should this feed be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:26 +#: src/components/moderation/ReportDialog/copy.ts:32 msgid "Why should this list be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:52 +#: src/components/moderation/ReportDialog/copy.ts:20 +msgid "Why should this livestream be reviewed?" +msgstr "" + +#: src/components/moderation/ReportDialog/copy.ts:58 msgid "Why should this message be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:20 +#: src/components/moderation/ReportDialog/copy.ts:26 msgid "Why should this post be reviewed?" msgstr "" -#: src/components/moderation/ReportDialog/copy.ts:38 +#: src/components/moderation/ReportDialog/copy.ts:44 msgid "Why should this starter pack be reviewed?" msgstr "" @@ -11033,6 +11071,10 @@ msgstr "" msgid "You are creating an account on" msgstr "" +#: src/components/live/GoLiveDisabledDialog.tsx:107 +msgid "You are currently blocked from using the Go Live feature. To appeal this moderation decision, please submit the form below." +msgstr "" + #: src/ageAssurance/components/NoAccessScreen.tsx:301 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:134 msgid "You are currently unable to access Bluesky's Age Assurance flow. Please <0>contact our moderation team if you believe this is an error." @@ -11135,9 +11177,9 @@ msgstr "" msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." msgstr "" -#: src/components/interstitials/Trending.tsx:130 +#: src/components/interstitials/Trending.tsx:129 #: src/components/interstitials/TrendingVideos.tsx:137 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:110 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:128 msgid "You can update this later from your settings." msgstr "" @@ -11619,7 +11661,7 @@ msgstr "" msgid "Your reply was sent" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:497 +#: src/components/moderation/ReportDialog/index.tsx:511 msgid "Your report will be sent to <0>{0}." msgstr "" From c540dae4e7db67031ee5f67feb076927999e364d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 10 Jan 2026 11:52:59 +0200 Subject: [PATCH 08/17] Set up conductor.json with yarn install and web server configuration (#9676) Add conductor setup and run scripts that install dependencies and start the dev server with a dynamically allocated port. Co-authored-by: Claude Haiku 4.5 --- conductor.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 conductor.json diff --git a/conductor.json b/conductor.json new file mode 100644 index 0000000000..ef5c740b55 --- /dev/null +++ b/conductor.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "setup": "yarn install", + "run": "yarn web --port $CONDUCTOR_PORT" + } +} \ No newline at end of file From 2d8de1ddc3d9916a598b14411c879e651b66adf3 Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Tue, 13 Jan 2026 02:42:26 -0800 Subject: [PATCH 09/17] Standardize metadata for client events in feeds (#9653) --- .../PostControls/BookmarkButton.tsx | 16 +++- .../PostControls/PostMenu/PostMenuItems.tsx | 26 ++++++ .../PostControls/PostMenu/index.tsx | 3 + .../PostControls/ShareMenu/index.tsx | 24 +++++- src/components/PostControls/index.tsx | 20 ++++- src/logger/metrics.ts | 86 +++++++++++++++++-- .../components/ThreadItemAnchor.tsx | 12 +++ src/state/feed-feedback.tsx | 21 +---- src/state/queries/post.ts | 2 - src/view/com/posts/PostFeedItem.tsx | 22 ++++- 10 files changed, 194 insertions(+), 38 deletions(-) diff --git a/src/components/PostControls/BookmarkButton.tsx b/src/components/PostControls/BookmarkButton.tsx index f729515202..ac391a1ae9 100644 --- a/src/components/PostControls/BookmarkButton.tsx +++ b/src/components/PostControls/BookmarkButton.tsx @@ -8,6 +8,7 @@ import type React from 'react' import {useCleanError} from '#/lib/hooks/useCleanError' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/post-shadow' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' import {useRequireAuth} from '#/state/session' import {useTheme} from '#/alf' @@ -32,6 +33,7 @@ export const BookmarkButton = memo(function BookmarkButton({ const {mutateAsync: bookmark} = useBookmarkMutation() const cleanError = useCleanError() const requireAuth = useRequireAuth() + const {feedDescriptor} = useFeedFeedbackContext() const {viewer} = post const isBookmarked = !!viewer?.bookmarked @@ -50,7 +52,12 @@ export const BookmarkButton = memo(function BookmarkButton({ post, }) - logger.metric('post:bookmark', {logContext}) + logger.metric('post:bookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) toast.show( @@ -85,7 +92,12 @@ export const BookmarkButton = memo(function BookmarkButton({ uri: post.uri, }) - logger.metric('post:unbookmark', {logContext}) + logger.metric('post:unbookmark', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) toast.show( diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index f8b410be97..ce93e81ab1 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -98,6 +98,7 @@ let PostMenuItems = ({ richText, threadgateRecord, onShowLess, + logContext, }: { testID: string post: Shadow @@ -111,6 +112,7 @@ let PostMenuItems = ({ timestamp: string threadgateRecord?: AppBskyFeedThreadgate.Record onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const {_} = useLingui() @@ -210,9 +212,21 @@ let PostMenuItems = ({ try { if (isThreadMuted) { unmuteThread() + logger.metric('post:unmute', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show(_(msg`You will now receive notifications for this thread`)) } else { muteThread() + logger.metric('post:mute', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show( _(msg`You will no longer receive notifications for this thread`), ) @@ -272,6 +286,12 @@ let PostMenuItems = ({ feedContext: postFeedContext, reqId: postReqId, }) + logger.metric('post:showMore', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) Toast.show( _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), ) @@ -284,6 +304,12 @@ let PostMenuItems = ({ feedContext: postFeedContext, reqId: postReqId, }) + logger.metric('post:showLess', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) if (onShowLess) { onShowLess({ item: postUri, diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx index 950bc4f6d9..f418587a5c 100644 --- a/src/components/PostControls/PostMenu/index.tsx +++ b/src/components/PostControls/PostMenu/index.tsx @@ -29,6 +29,7 @@ let PostMenuButton = ({ threadgateRecord, onShowLess, hitSlop, + logContext, }: { testID: string post: Shadow @@ -41,6 +42,7 @@ let PostMenuButton = ({ threadgateRecord?: AppBskyFeedThreadgate.Record onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void hitSlop?: Insets + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {_} = useLingui() @@ -87,6 +89,7 @@ let PostMenuButton = ({ timestamp={timestamp} threadgateRecord={threadgateRecord} onShowLess={onShowLess} + logContext={logContext} /> )} diff --git a/src/components/PostControls/ShareMenu/index.tsx b/src/components/PostControls/ShareMenu/index.tsx index 6127ca41db..3fd9583113 100644 --- a/src/components/PostControls/ShareMenu/index.tsx +++ b/src/components/PostControls/ShareMenu/index.tsx @@ -16,6 +16,7 @@ import {useGate} from '#/lib/statsig/statsig' import {toShareUrl} from '#/lib/strings/url-helpers' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/post-shadow' +import {useFeedFeedbackContext} from '#/state/feed-feedback' import {EventStopper} from '#/view/com/util/EventStopper' import {native} from '#/alf' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' @@ -35,6 +36,7 @@ let ShareMenuButton = ({ threadgateRecord, onShare, hitSlop, + logContext, }: { testID: string post: Shadow @@ -45,9 +47,11 @@ let ShareMenuButton = ({ threadgateRecord?: AppBskyFeedThreadgate.Record onShare: () => void hitSlop?: Insets + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' }): React.ReactNode => { const {_} = useLingui() const gate = useGate() + const {feedDescriptor} = useFeedFeedbackContext() const ShareIcon = gate('alt_share_icon') ? ArrowShareRightIcon @@ -65,13 +69,27 @@ let ShareMenuButton = ({ setTimeout(menuControl.open) logger.metric( - 'share:open', - {context: big ? 'thread' : 'feed'}, + 'post:share', + { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + postContext: big ? 'thread' : 'feed', + }, {statsig: true}, ) }, }), - [menuControl, setHasBeenOpen, big], + [ + menuControl, + setHasBeenOpen, + big, + logContext, + feedDescriptor, + post.uri, + post.author.did, + ], ) const onNativeLongPress = () => { diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index ec0016815f..45688c2617 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -13,6 +13,7 @@ import {CountWheel} from '#/lib/custom-animations/CountWheel' import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {useHaptics} from '#/lib/haptics' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {logger} from '#/logger' import {type Shadow} from '#/state/cache/types' import {useFeedFeedbackContext} from '#/state/feed-feedback' import { @@ -174,6 +175,12 @@ let PostControls = ({ feedContext, reqId, }) + logger.metric('post:clickQuotePost', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) openComposer({ quote: post, onPost: onPostReply, @@ -217,7 +224,16 @@ let PostControls = ({ testID="replyBtn" onPress={ !replyDisabled - ? () => requireAuth(() => onPressReply()) + ? () => + requireAuth(() => { + logger.metric('post:clickReply', { + uri: post.uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) + onPressReply() + }) : undefined } label={_( @@ -315,6 +331,7 @@ let PostControls = ({ left: secondaryControlSpacingStyles.gap / 2, right: secondaryControlSpacingStyles.gap / 2, }} + logContext={logContext} /> diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 949c883b7c..dddca72a17 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -176,13 +176,19 @@ export type MetricEvents = { 'feed:suggestion:press': { feedUrl: string } - 'feed:showMore': { - feed: string - feedContext: string + 'post:showMore': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } - 'feed:showLess': { - feed: string - feedContext: string + 'post:showLess': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'feed:clickthrough': { feed: string @@ -257,15 +263,70 @@ export type MetricEvents = { logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } - 'post:mute': {} - 'post:unmute': {} + 'post:mute': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:unmute': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } 'post:pin': {} 'post:unpin': {} 'post:bookmark': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'post:unbookmark': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickReply': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickQuotePost': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughAuthor': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughItem': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number + } + 'post:clickthroughEmbed': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + position?: number } 'post:view': { uri: string @@ -565,7 +626,14 @@ export type MetricEvents = { 'live:view:profile': {subject: string} 'live:view:post': {subject: string; feed?: string} - 'share:open': {context: 'feed' | 'thread'} + 'post:share': { + uri: string + authorDid: string + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' + feedDescriptor?: string + postContext: 'feed' | 'thread' + position?: number + } 'share:press:copyLink': {} 'share:press:nativeShare': {} 'share:press:openDmSearch': {} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 7785afe2ca..fbed7965b1 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -281,6 +281,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ ]) const onOpenAuthor = () => { + logger.metric('post:clickthroughAuthor', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }) if (postSource) { feedFeedback.sendInteraction({ item: post.uri, @@ -292,6 +298,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ } const onOpenEmbed = () => { + logger.metric('post:clickthroughEmbed', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'PostThreadItem', + feedDescriptor: feedFeedback.feedDescriptor, + }) if (postSource) { feedFeedback.sendInteraction({ item: post.uri, diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 1d719978a5..28f13fb5f2 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -137,7 +137,6 @@ export function useFeedFeedback( sendOrAggregateInteractionsForStats( aggregatedStats.current, interactionsToSend, - feed?.feedDescriptor ?? 'unknown', ) throttledFlushAggregatedStats() logger.debug('flushed') @@ -274,28 +273,10 @@ function createAggregatedStats(): AggregatedStats { function sendOrAggregateInteractionsForStats( stats: AggregatedStats, interactions: AppBskyFeedDefs.Interaction[], - feed: string, ) { for (let interaction of interactions) { switch (interaction.event) { - // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. - // This lets us send the feed context together with them. - case 'app.bsky.feed.defs#requestLess': { - logger.metric('feed:showLess', { - feed, - feedContext: interaction.feedContext ?? '', - }) - break - } - case 'app.bsky.feed.defs#requestMore': { - logger.metric('feed:showMore', { - feed, - feedContext: interaction.feedContext ?? '', - }) - break - } - - // The rest of the events are aggregated and sent later in batches. + // The events are aggregated and sent later in batches. case 'app.bsky.feed.defs#clickthroughAuthor': case 'app.bsky.feed.defs#clickthroughEmbed': case 'app.bsky.feed.defs#clickthroughItem': diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 09ced9874e..8fdfc584b6 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -373,7 +373,6 @@ function useThreadMuteMutation() { {uri: string} // the root post's uri >({ mutationFn: ({uri}) => { - logger.metric('post:mute', {}) return agent.api.app.bsky.graph.muteThread({root: uri}) }, }) @@ -383,7 +382,6 @@ function useThreadUnmuteMutation() { const agent = useAgent() return useMutation<{}, Error, {uri: string}>({ mutationFn: ({uri}) => { - logger.metric('post:unmute', {}) return agent.api.app.bsky.graph.unmuteThread({root: uri}) }, }) diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 244ebde05f..5b6ce7841a 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -21,6 +21,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {countLines} from '#/lib/strings/helpers' +import {logger} from '#/logger' import { POST_TOMBSTONE, type Shadow, @@ -173,7 +174,8 @@ let FeedItemInner = ({ const urip = new AtUri(post.uri) return [makeProfileLink(post.author, 'post', urip.rkey), urip.rkey] }, [post.uri, post.author]) - const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext() + const {sendInteraction, feedSourceInfo, feedDescriptor} = + useFeedFeedbackContext() const onPressReply = () => { sendInteraction({ @@ -209,6 +211,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughAuthor', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) } const onOpenReposter = () => { @@ -227,6 +235,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughEmbed', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) } const onBeforePress = () => { @@ -236,6 +250,12 @@ let FeedItemInner = ({ feedContext, reqId, }) + logger.metric('post:clickthroughItem', { + uri: post.uri, + authorDid: post.author.did, + logContext: 'FeedItem', + feedDescriptor, + }) unstableCacheProfileView(queryClient, post.author) setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { feedSourceInfo, From 9aa360d32853e55c05cceaa3a2151625954b567e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 13 Jan 2026 09:37:00 -0600 Subject: [PATCH 10/17] Do not render links if uri is invalid (#9663) * Do not render links if uri is invalid * Update comment --- src/components/RichText.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 9908679c57..eca0d7c635 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -11,6 +11,9 @@ import {RichTextTag} from '#/components/RichTextTag' import {Text, type TextProps} from '#/components/Typography' const WORD_WRAP = {wordWrap: 1} +// lifted from facet detection in `RichText` impl, _without_ `gm` flags +const URL_REGEX = + /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/i export type RichTextProps = TextStyleProp & Pick & { @@ -115,7 +118,8 @@ export function RichText({ , ) } else if (link && AppBskyRichtextFacet.validateLink(link).success) { - if (disableLinks) { + const isValidLink = URL_REGEX.test(link.uri) + if (!isValidLink || disableLinks) { els.push(toShortUrl(segment.text)) } else { els.push( From 61c919110e8446d9c8f90d082854f16b8524f22e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 13 Jan 2026 19:56:06 +0200 Subject: [PATCH 11/17] Expose QueryClient for TanStack Query DevTools browser extension (#9678) Co-authored-by: Claude Opus 4.5 --- src/lib/react-query.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index fe3ec6f4c7..e788bf410c 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -1,4 +1,4 @@ -import {useRef, useState} from 'react' +import {useEffect, useRef, useState} from 'react' import {AppState, type AppStateStatus} from 'react-native' import AsyncStorage from '@react-native-async-storage/async-storage' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' @@ -9,9 +9,15 @@ import { } from '@tanstack/react-query-persist-client' import type React from 'react' -import {isNative} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' +declare global { + interface Window { + __TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient + } +} + // any query keys in this array will be persisted to AsyncStorage export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info' const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot] @@ -180,6 +186,11 @@ function QueryProviderInner({ dehydrateOptions, } }) + useEffect(() => { + if (isWeb) { + window.__TANSTACK_QUERY_CLIENT__ = queryClient + } + }, [queryClient]) return ( Date: Wed, 14 Jan 2026 02:52:08 +0000 Subject: [PATCH 12/17] Nightly source-language update --- src/locale/locales/en/messages.po | 226 +++++++++++++++--------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index d920c4dbe4..213c0a9038 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -99,7 +99,7 @@ msgstr "" msgid "{0, plural, one {following} other {following}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:477 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:489 msgid "{0, plural, one {like} other {likes}}" msgstr "" @@ -107,15 +107,15 @@ msgstr "" msgid "{0, plural, one {post} other {posts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:461 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:473 msgid "{0, plural, one {quote} other {quotes}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:443 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:455 msgid "{0, plural, one {repost} other {reposts}}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:488 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:500 msgid "{0, plural, one {save} other {saves}}" msgstr "" @@ -621,7 +621,7 @@ msgstr "" msgid "Account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:393 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:419 #: src/screens/Messages/components/RequestButtons.tsx:91 #: src/view/com/profile/ProfileMenu.tsx:169 msgctxt "toast" @@ -633,7 +633,7 @@ msgctxt "toast" msgid "Account followed" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:416 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:442 #: src/view/com/profile/ProfileMenu.tsx:145 msgctxt "toast" msgid "Account muted" @@ -672,7 +672,7 @@ msgctxt "toast" msgid "Account unfollowed" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:406 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:432 #: src/view/com/profile/ProfileMenu.tsx:135 msgctxt "toast" msgid "Account unmuted" @@ -830,7 +830,7 @@ msgstr "" msgid "Add to lists" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:129 +#: src/components/PostControls/BookmarkButton.tsx:141 msgid "Add to saved posts" msgstr "" @@ -1296,12 +1296,12 @@ msgstr "" msgid "Apply Pull Request" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:658 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:670 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:627 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:666 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:639 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:678 msgid "Archived post" msgstr "" @@ -1358,8 +1358,8 @@ msgstr "" msgid "Artistic or non-erotic nudity." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:529 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:531 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:555 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:557 msgid "Assign topic for algo" msgstr "" @@ -1465,7 +1465,7 @@ msgstr "" msgid "Birthday" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:792 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:818 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 #: src/view/com/profile/ProfileMenu.tsx:504 msgid "Block" @@ -1473,8 +1473,8 @@ msgstr "" #: src/components/dms/ConvoMenu.tsx:261 #: src/components/dms/ConvoMenu.tsx:264 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:677 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:679 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:703 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:705 #: src/screens/Messages/components/RequestButtons.tsx:144 #: src/screens/Messages/components/RequestButtons.tsx:146 #: src/view/com/profile/ProfileMenu.tsx:410 @@ -1482,7 +1482,7 @@ msgstr "" msgid "Block account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:787 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:813 #: src/view/com/profile/ProfileMenu.tsx:487 msgid "Block Account?" msgstr "" @@ -1534,7 +1534,7 @@ msgstr "" msgid "Blocked Accounts" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:789 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:815 #: src/view/com/profile/ProfileMenu.tsx:499 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." msgstr "" @@ -1564,7 +1564,7 @@ msgstr "" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:688 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:700 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -1803,9 +1803,9 @@ msgstr "" msgid "Cancel search" msgstr "" -#: src/components/PostControls/index.tsx:106 -#: src/components/PostControls/index.tsx:137 -#: src/components/PostControls/index.tsx:165 +#: src/components/PostControls/index.tsx:107 +#: src/components/PostControls/index.tsx:138 +#: src/components/PostControls/index.tsx:166 #: src/state/shell/composer/index.tsx:95 msgid "Cannot interact with a blocked user" msgstr "" @@ -2419,7 +2419,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:57 #: src/components/PostControls/DiscoverDebug.tsx:36 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:235 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:249 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:72 #: src/lib/sharing.ts:25 #: src/lib/sharing.ts:41 @@ -2508,8 +2508,8 @@ msgstr "" msgid "Copy post at:// URI" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:484 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:486 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:510 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:512 msgid "Copy post text" msgstr "" @@ -2770,7 +2770,7 @@ msgid "Default icons" msgstr "" #: src/components/dms/MessageContextMenu.tsx:202 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:736 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:762 #: src/screens/Messages/components/ChatStatusInfo.tsx:55 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:280 #: src/screens/Settings/AppPasswords.tsx:212 @@ -2842,8 +2842,8 @@ msgstr "" msgid "Delete my account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:717 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:719 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:743 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:745 #: src/view/com/composer/Composer.tsx:969 msgid "Delete post" msgstr "" @@ -2861,7 +2861,7 @@ msgstr "" msgid "Delete this list?" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:731 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:757 msgid "Delete this post?" msgstr "" @@ -2896,12 +2896,12 @@ msgstr "" msgid "Descriptive alt text" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:621 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:631 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:647 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:657 msgid "Detach quote" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:767 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:793 msgid "Detach quote post?" msgstr "" @@ -3214,8 +3214,8 @@ msgstr "" msgid "Edit image" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:698 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:711 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:724 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:737 msgid "Edit interaction settings" msgstr "" @@ -3695,7 +3695,7 @@ msgstr "" msgid "Failed to delete message" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:204 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:206 msgid "Failed to delete post, please try again" msgstr "" @@ -3831,7 +3831,7 @@ msgstr "" msgid "Failed to submit appeal, please try again." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:224 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:238 msgid "Failed to toggle thread mute, please try again" msgstr "" @@ -3918,8 +3918,8 @@ msgstr "" msgid "Feedback" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:276 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:294 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:296 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:320 msgctxt "toast" msgid "Feedback sent to feed operator" msgstr "" @@ -4603,7 +4603,7 @@ msgstr "" #: src/components/moderation/ContentHider.tsx:208 #: src/components/moderation/LabelPreference.tsx:140 #: src/components/moderation/PostHider.tsx:137 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:747 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:773 #: src/lib/moderation/useLabelBehaviorDescription.ts:18 #: src/lib/moderation/useLabelBehaviorDescription.ts:23 #: src/lib/moderation/useLabelBehaviorDescription.ts:28 @@ -4625,18 +4625,18 @@ msgstr "" msgid "Hide lists" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:578 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:584 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:604 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:610 msgid "Hide post for me" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:595 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:605 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:621 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:631 msgid "Hide reply for everyone" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:577 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:583 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:603 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:609 msgid "Hide reply for me" msgstr "" @@ -4644,12 +4644,12 @@ msgstr "" msgid "Hide this card" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:742 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:768 msgid "Hide this post?" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:742 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:777 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:768 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:803 msgid "Hide this reply?" msgstr "" @@ -4805,7 +4805,7 @@ msgstr "" msgid "If you need to update your email, <0>click here." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:733 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:759 msgid "If you remove this post, you won't be able to recover it." msgstr "" @@ -4967,7 +4967,7 @@ msgstr "" msgid "Invalid handle. Please try a different one." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:365 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:391 msgctxt "toast" msgid "Invalid interaction settings." msgstr "" @@ -5269,7 +5269,7 @@ msgid "Like" msgstr "" #. Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form -#: src/components/PostControls/index.tsx:270 +#: src/components/PostControls/index.tsx:286 msgid "Like ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -5334,7 +5334,7 @@ msgstr "" msgid "Likes of your reposts notifications" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:470 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:482 msgid "Likes on this post" msgstr "" @@ -5761,8 +5761,8 @@ msgstr "" msgid "Mute {tag}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:660 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:666 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:692 #: src/view/com/profile/ProfileMenu.tsx:389 #: src/view/com/profile/ProfileMenu.tsx:396 msgid "Mute account" @@ -5814,13 +5814,13 @@ msgstr "" msgid "Mute this word until you unmute it" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:544 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:548 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:570 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:574 msgid "Mute thread" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:558 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:560 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:584 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:586 msgid "Mute words & tags" msgstr "" @@ -6338,7 +6338,7 @@ msgid "OK" msgstr "" #: src/screens/Login/PasswordUpdatedForm.tsx:37 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:693 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:705 msgid "Okay" msgstr "" @@ -6467,7 +6467,7 @@ msgstr "" msgid "Open pack" msgstr "" -#: src/components/PostControls/PostMenu/index.tsx:64 +#: src/components/PostControls/PostMenu/index.tsx:66 msgid "Open post options menu" msgstr "" @@ -6476,7 +6476,7 @@ msgstr "" msgid "Open profile" msgstr "" -#: src/components/PostControls/ShareMenu/index.tsx:89 +#: src/components/PostControls/ShareMenu/index.tsx:107 msgid "Open share menu" msgstr "" @@ -6792,8 +6792,8 @@ msgstr "" msgid "Pin to Home" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:452 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:459 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:478 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:485 msgid "Pin to your profile" msgstr "" @@ -7046,7 +7046,7 @@ msgstr "" msgid "Post by @{0}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:184 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:186 msgctxt "toast" msgid "Post deleted" msgstr "" @@ -7096,7 +7096,7 @@ msgctxt "toast" msgid "Post pinned" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:59 +#: src/components/PostControls/BookmarkButton.tsx:66 msgid "Post saved" msgstr "" @@ -7299,11 +7299,11 @@ msgstr "" msgid "Quote post" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:314 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:340 msgid "Quote post was re-attached" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:313 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:339 msgid "Quote post was successfully detached" msgstr "" @@ -7321,7 +7321,7 @@ msgstr "" msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:454 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:466 msgid "Quotes of this post" msgstr "" @@ -7333,8 +7333,8 @@ msgstr "" msgid "Rate limit exceeded. Please try again later." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:620 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:630 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:646 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:656 msgid "Re-attach quote" msgstr "" @@ -7518,7 +7518,7 @@ msgstr "" msgid "Remove from saved feeds" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:128 +#: src/components/PostControls/BookmarkButton.tsx:140 #: src/screens/Bookmarks/index.tsx:260 msgid "Remove from saved posts" msgstr "" @@ -7594,7 +7594,7 @@ msgstr "" msgid "Removed from saved feeds" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:94 +#: src/components/PostControls/BookmarkButton.tsx:106 #: src/screens/Bookmarks/index.tsx:218 msgid "Removed from saved posts" msgstr "" @@ -7662,7 +7662,7 @@ msgid "Reply" msgstr "" #. Accessibility label for the reply button, verb form followed by number of replies and noun form -#: src/components/PostControls/index.tsx:224 +#: src/components/PostControls/index.tsx:240 msgid "Reply ({0, plural, one {# reply} other {# replies}})" msgstr "" @@ -7688,12 +7688,12 @@ msgstr "" msgid "Reply sorting" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:351 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:377 msgctxt "toast" msgid "Reply visibility updated" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:350 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:376 msgid "Reply was successfully hidden" msgstr "" @@ -7736,8 +7736,8 @@ msgstr "" msgid "Report message" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:688 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:714 msgid "Report post" msgstr "" @@ -7828,7 +7828,7 @@ msgstr "" msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:436 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:448 msgid "Reposts of this post" msgstr "" @@ -8671,8 +8671,8 @@ msgstr "" msgid "Show customization options" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:515 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:517 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:541 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:543 msgid "Show less like this" msgstr "" @@ -8688,8 +8688,8 @@ msgstr "" msgid "Show More" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:507 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:509 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:533 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:535 msgid "Show more like this" msgstr "" @@ -8715,8 +8715,8 @@ msgstr "" msgid "Show replies as" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:594 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:604 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:620 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:630 msgid "Show reply for everyone" msgstr "" @@ -8738,7 +8738,7 @@ msgstr "" msgid "Show warning and filter from feeds" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:629 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 msgid "Shows information about when this post was created" msgstr "" @@ -8798,8 +8798,8 @@ msgstr "" msgid "Sign in to Bluesky or create a new account" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:493 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:495 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:519 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:521 msgid "Sign in to view post" msgstr "" @@ -9497,9 +9497,9 @@ msgstr "" msgid "There was an issue updating your feeds, please check your internet connection and try again." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:397 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:410 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:420 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:423 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:436 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:446 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:90 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:101 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:88 @@ -9708,7 +9708,7 @@ msgstr "" msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:669 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:681 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -9724,7 +9724,7 @@ msgstr "" msgid "This post was deleted by its author" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:744 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:770 msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" @@ -9736,7 +9736,7 @@ msgstr "" msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:779 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:805 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 "" @@ -9797,7 +9797,7 @@ msgstr "" msgid "This will remove @{0} from the quick access list." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:769 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:795 msgid "This will remove your post from this quote post for all users, and replace it with a placeholder." msgstr "" @@ -9892,10 +9892,10 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:139 #: src/components/dms/MessageContextMenu.tsx:141 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:476 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:478 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:591 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:594 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:502 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:504 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:603 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:606 msgid "Translate" msgstr "" @@ -10028,7 +10028,7 @@ msgstr "" msgid "Unblock list" msgstr "" -#: src/components/PostControls/BookmarkButton.tsx:40 +#: src/components/PostControls/BookmarkButton.tsx:42 msgctxt "Button label to undo saving/removing a post from saved posts." msgid "Undo" msgstr "" @@ -10085,7 +10085,7 @@ msgid "Unlike" msgstr "" #. Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun -#: src/components/PostControls/index.tsx:260 +#: src/components/PostControls/index.tsx:276 msgid "Unlike ({0, plural, one {# like} other {# likes}})" msgstr "" @@ -10105,8 +10105,8 @@ msgstr "" msgid "Unmute {tag}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:659 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:665 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:685 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:691 #: src/view/com/profile/ProfileMenu.tsx:388 #: src/view/com/profile/ProfileMenu.tsx:394 msgid "Unmute account" @@ -10121,8 +10121,8 @@ msgstr "" msgid "Unmute list" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:544 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:548 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:570 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:574 msgid "Unmute thread" msgstr "" @@ -10150,8 +10150,8 @@ msgstr "" msgid "Unpin from home" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:451 -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:458 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:477 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:484 msgid "Unpin from profile" msgstr "" @@ -10223,12 +10223,12 @@ msgstr "" msgid "Update your email" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:318 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:344 msgctxt "toast" msgid "Updating quote attachment failed" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:370 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:396 msgctxt "toast" msgid "Updating reply visibility failed" msgstr "" @@ -11043,11 +11043,11 @@ msgstr "" msgid "Yes, delete this starter pack" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:772 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:798 msgid "Yes, detach" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:782 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:808 msgid "Yes, hide" msgstr "" @@ -11146,7 +11146,7 @@ msgstr "" msgid "You can continue ongoing conversations regardless of which setting you choose." msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:357 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:383 msgctxt "toast" msgid "You can hide a maximum of {MAX_HIDDEN_REPLIES} replies." msgstr "" @@ -11392,11 +11392,11 @@ msgstr "" msgid "You will no longer receive notifications for {0}" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:217 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:231 msgid "You will no longer receive notifications for this thread" msgstr "" -#: src/components/PostControls/PostMenu/PostMenuItems.tsx:213 +#: src/components/PostControls/PostMenu/PostMenuItems.tsx:221 msgid "You will now receive notifications for this thread" msgstr "" From af92610864af4fbd8fdfc4a79e69be2466361bf6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 14 Jan 2026 21:16:34 +0200 Subject: [PATCH 13/17] Add comprehensive CLAUDE.md development guide (#9666) * Add comprehensive CLAUDE.md development guide Document the codebase architecture, styling system (ALF), component patterns (Dialog, Menu, Button), i18n with Lingui, state management with TanStack Query, and navigation patterns to help Claude work effectively in this codebase. * Add footguns section to CLAUDE.md Document critical pitfalls including: - Dialog close callback pattern (control.close(() => ...)) for avoiding race conditions with navigation, state updates, and opening other dialogs - Controlled vs uncontrolled input guidance - Platform-specific component behavior differences * Add React Compiler note to footguns section Document that useMemo/useCallback are unnecessary since React Compiler handles memoization automatically. Only use them for specific cases like effect dependencies or non-React library interop. --------- Co-authored-by: Claude --- CLAUDE.md | 591 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..e1a7d174ed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,591 @@ +# CLAUDE.md - Bluesky Social App Development Guide + +This document provides guidance for working effectively in the Bluesky Social app codebase. + +## Project Overview + +Bluesky Social is a cross-platform social media application built with React Native and Expo. It runs on iOS, Android, and Web, connecting to the AT Protocol (atproto) decentralized social network. + +**Tech Stack:** +- React Native 0.81 with Expo 54 +- TypeScript +- React Navigation for routing +- TanStack Query (React Query) for data fetching +- Lingui for internationalization +- Custom design system called ALF (Application Layout Framework) + +## Essential Commands + +```bash +# Development +yarn start # Start Expo dev server +yarn web # Start web version +yarn android # Run on Android +yarn ios # Run on iOS + +# Testing & Quality +yarn test # Run Jest tests +yarn lint # Run ESLint +yarn typecheck # Run TypeScript type checking + +# Internationalization +yarn intl:extract # Extract translation strings +yarn intl:compile # Compile translations for runtime + +# Build +yarn build-web # Build web version +yarn prebuild # Generate native projects +``` + +## Project Structure + +``` +src/ +├── alf/ # Design system (ALF) - themes, atoms, tokens +├── components/ # Shared UI components (Button, Dialog, Menu, etc.) +├── screens/ # Full-page screen components (newer pattern) +├── view/ +│ ├── screens/ # Full-page screens (legacy location) +│ ├── com/ # Reusable view components +│ └── shell/ # App shell (navigation bars, tabs) +├── state/ +│ ├── queries/ # TanStack Query hooks +│ ├── preferences/ # User preferences (React Context) +│ ├── session/ # Authentication state +│ └── persisted/ # Persistent storage layer +├── lib/ # Utilities, constants, helpers +├── locale/ # i18n configuration and language files +└── Navigation.tsx # Main navigation configuration +``` + +## Styling System (ALF) + +ALF is the custom design system. It uses Tailwind-inspired naming with underscores instead of hyphens. + +### Basic Usage + +```tsx +import {atoms as a, useTheme} from '#/alf' + +function MyComponent() { + const t = useTheme() + + return ( + + + Hello + + + ) +} +``` + +### Key Concepts + +**Static Atoms** - Theme-independent styles imported from `atoms`: +```tsx +import {atoms as a} from '#/alf' +// a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc. +``` + +**Theme Atoms** - Theme-dependent colors from `useTheme()`: +```tsx +const t = useTheme() +// t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc. +// t.palette.primary_500, t.palette.negative_400, etc. +``` + +**Platform Utilities** - For platform-specific styles: +```tsx +import {web, native, ios, android, platform} from '#/alf' + +const styles = [ + a.p_md, + web({cursor: 'pointer'}), + native({paddingBottom: 20}), + platform({ios: {...}, android: {...}, web: {...}}), +] +``` + +**Breakpoints** - Responsive design: +```tsx +import {useBreakpoints} from '#/alf' + +const {gtPhone, gtMobile, gtTablet} = useBreakpoints() +if (gtMobile) { + // Tablet or desktop layout +} +``` + +### Naming Conventions + +- Spacing: `xxs`, `xs`, `sm`, `md`, `lg`, `xl`, `xxl` (t-shirt sizes) +- Text: `text_xs`, `text_sm`, `text_md`, `text_lg`, `text_xl` +- Gaps/Padding: `gap_sm`, `p_md`, `px_lg`, `py_xl` +- Flex: `flex_row`, `flex_1`, `align_center`, `justify_between` +- Borders: `border`, `border_t`, `rounded_md`, `rounded_full` + +## Component Patterns + +### Dialog Component + +Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state. + +```tsx +import * as Dialog from '#/components/Dialog' + +function MyFeature() { + const control = Dialog.useDialogControl() + + return ( + <> + + + + {/* Native drag handle */} + + + Title + + + Dialog content here + + + + + + ) +} +``` + +### Menu Component + +Menus render as a dropdown on web and a bottom sheet dialog on native. + +```tsx +import * as Menu from '#/components/Menu' + +function MyMenu() { + return ( + + + {({props}) => ( + + )} + + + + + + + Edit + + + + Delete + + + + + ) +} +``` + +### Button Component + +```tsx +import {Button, ButtonText, ButtonIcon} from '#/components/Button' + +// Solid primary button (most common) + + +// With icon + + +// Icon-only button + + +// Ghost variant (deprecated - use color prop) + +``` + +**Button Props:** +- `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` +- `size`: `'tiny'` | `'small'` | `'large'` +- `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'` +- `variant`: `'solid'` | `'outline'` | `'ghost'` (deprecated, use `color`) + +### Typography + +```tsx +import {Text, H1, H2, P} from '#/components/Typography' + +

Heading

+

Paragraph text with default styling.

+Custom text + +// For text with emoji, add the emoji prop +Hello! 👋 +``` + +### TextField + +```tsx +import * as TextField from '#/components/forms/TextField' + +Email + + + + +``` + +## Internationalization (i18n) + +All user-facing strings must be wrapped for translation using Lingui. + +```tsx +import {msg, Trans, plural} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +function MyComponent() { + const {_} = useLingui() + + // Simple strings - use msg() with _() function + const title = _(msg`Settings`) + const errorMessage = _(msg`Something went wrong`) + + // Strings with variables + const greeting = _(msg`Hello, ${name}!`) + + // Pluralization + const countLabel = _(plural(count, { + one: '# item', + other: '# items', + })) + + // JSX content - use Trans component + return ( + + Welcome to Bluesky + + ) +} +``` + +**Commands:** +```bash +yarn intl:extract # Extract new strings to locale files +yarn intl:compile # Compile for runtime (required after changes) +``` + +## State Management + +### TanStack Query (Data Fetching) + +```tsx +// src/state/queries/profile.ts +import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' + +// Query key pattern +const RQKEY_ROOT = 'profile' +export const RQKEY = (did: string) => [RQKEY_ROOT, did] + +// Query hook +export function useProfileQuery({did}: {did: string}) { + const agent = useAgent() + + return useQuery({ + queryKey: RQKEY(did), + queryFn: async () => { + const res = await agent.getProfile({actor: did}) + return res.data + }, + staleTime: STALE.MINUTES.FIVE, + enabled: !!did, + }) +} + +// Mutation hook +export function useUpdateProfile() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (data) => { + // Update logic + }, + onSuccess: (_, variables) => { + queryClient.invalidateQueries({queryKey: RQKEY(variables.did)}) + }, + }) +} +``` + +**Stale Time Constants** (from `src/state/queries/index.ts`): +```tsx +STALE.SECONDS.FIFTEEN // 15 seconds +STALE.MINUTES.ONE // 1 minute +STALE.MINUTES.FIVE // 5 minutes +STALE.HOURS.ONE // 1 hour +STALE.INFINITY // Never stale +``` + +### Preferences (React Context) + +```tsx +// Simple boolean preference pattern +import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' + +function SettingsScreen() { + const autoplayDisabled = useAutoplayDisabled() + const setAutoplayDisabled = useSetAutoplayDisabled() + + return ( + + ) +} +``` + +### Session State + +```tsx +import {useSession, useAgent} from '#/state/session' + +function MyComponent() { + const {hasSession, currentAccount} = useSession() + const agent = useAgent() + + if (!hasSession) { + return + } + + // Use agent for API calls + const response = await agent.getProfile({actor: currentAccount.did}) +} +``` + +## Navigation + +Navigation uses React Navigation with type-safe route parameters. + +```tsx +// Screen component +import {type NativeStackScreenProps} from '@react-navigation/native-stack' +import {type CommonNavigatorParams} from '#/lib/routes/types' + +type Props = NativeStackScreenProps + +export function ProfileScreen({route, navigation}: Props) { + const {name} = route.params // Type-safe params + + return ( + + {/* Screen content */} + + ) +} + +// Programmatic navigation +import {useNavigation} from '@react-navigation/native' + +const navigation = useNavigation() +navigation.navigate('Profile', {name: 'alice.bsky.social'}) + +// Or use the navigate helper +import {navigate} from '#/Navigation' +navigate('Profile', {name: 'alice.bsky.social'}) +``` + +## Platform-Specific Code + +Use file extensions for platform-specific implementations: + +``` +Component.tsx # Shared/default +Component.web.tsx # Web-only +Component.native.tsx # iOS + Android +Component.ios.tsx # iOS-only +Component.android.tsx # Android-only +``` + +Example from Dialog: +- `src/components/Dialog/index.tsx` - Native (uses BottomSheet) +- `src/components/Dialog/index.web.tsx` - Web (uses modal with Radix primitives) + +Platform detection: +```tsx +import {isWeb, isNative, isIOS, isAndroid} from '#/platform/detection' + +if (isNative) { + // Native-specific logic +} +``` + +## Import Aliases + +Always use the `#/` alias for absolute imports: + +```tsx +// Good +import {useSession} from '#/state/session' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' + +// Avoid +import {useSession} from '../../../state/session' +``` + +## Footguns + +Common pitfalls to avoid in this codebase: + +### Dialog Close Callback (Critical) + +**Always use `control.close(() => ...)` when performing actions after closing a dialog.** The callback ensures the action runs after the dialog's close animation completes. Failing to do this causes race conditions with React state updates. + +```tsx +// WRONG - causes bugs with state updates, navigation, opening other dialogs +const onConfirm = () => { + control.close() + navigation.navigate('Home') // May race with dialog animation +} + +// WRONG - same problem +const onConfirm = () => { + control.close() + otherDialogControl.open() // Will likely fail or cause visual glitches +} + +// CORRECT - action runs after dialog fully closes +const onConfirm = () => { + control.close(() => { + navigation.navigate('Home') + }) +} + +// CORRECT - opening another dialog after close +const onConfirm = () => { + control.close(() => { + otherDialogControl.open() + }) +} + +// CORRECT - state updates after close +const onConfirm = () => { + control.close(() => { + setSomeState(newValue) + onCallback?.() + }) +} +``` + +This applies to: +- Navigation (`navigation.navigate()`, `navigation.push()`) +- Opening other dialogs or menus +- State updates that affect UI (`setState`, `queryClient.invalidateQueries`) +- Callbacks passed from parent components + +The Menu component on iOS specifically uses this pattern - see `src/components/Menu/index.tsx:151`. + +### Controlled vs Uncontrolled Inputs + +Prefer `defaultValue` over `value` for TextInput on the old architecture: + +```tsx +// Preferred - uncontrolled + + +// Avoid when possible - controlled (can cause performance issues) + +``` + +### Platform-Specific Behavior + +Some components behave differently across platforms: +- `Dialog.Handle` - Only renders on native (drag handle for bottom sheet) +- `Dialog.Close` - Only renders on web (X button) +- `Menu.Divider` - Only renders on web +- `Menu.ContainerItem` - Only works on native + +Always test on multiple platforms when using these components. + +### React Compiler is Enabled + +This codebase uses React Compiler, so **don't proactively add `useMemo` or `useCallback`**. The compiler handles memoization automatically. + +```tsx +// UNNECESSARY - React Compiler handles this +const handlePress = useCallback(() => { + doSomething() +}, [doSomething]) + +// JUST WRITE THIS +const handlePress = () => { + doSomething() +} +``` + +Only use `useMemo`/`useCallback` when you have a specific reason, such as: +- The value is immediately used in an effect's dependency array +- You're passing a callback to a non-React library that needs referential stability + +## Best Practices + +1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful + +2. **Translations**: Wrap ALL user-facing strings with `msg()` or `` + +3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles + +4. **State**: Use TanStack Query for server state, React Context for UI preferences + +5. **Components**: Check if a component exists in `#/components/` before creating new ones + +6. **Types**: Define explicit types for props, use `NativeStackScreenProps` for screens + +7. **Testing**: Components should have `testID` props for E2E testing + +## Key Files Reference + +| Purpose | Location | +|---------|----------| +| Theme definitions | `src/alf/themes.ts` | +| Design tokens | `src/alf/tokens.ts` | +| Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) | +| Navigation config | `src/Navigation.tsx` | +| Route definitions | `src/routes.ts` | +| Route types | `src/lib/routes/types.ts` | +| Query hooks | `src/state/queries/*.ts` | +| Session state | `src/state/session/index.tsx` | +| i18n setup | `src/locale/i18n.ts` | From cb6e65e3135d9c92b953839de79fe9d9ad90b5b8 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 14 Jan 2026 21:16:49 +0200 Subject: [PATCH 14/17] pin AsyncStorage to v2.2.0 (#9656) --- package.json | 1 + yarn.lock | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 185e530bd3..61e042e05b 100644 --- a/package.json +++ b/package.json @@ -284,6 +284,7 @@ "@react-native/babel-preset": "0.81.5", "@react-native/normalize-colors": "0.81.5", "**/@expo/image-utils": "0.8.7", + "**/@react-native-async-storage/async-storage": "2.2.0", "**/expo-constants": "18.0.8", "**/expo-device": "7.1.4", "**/zod": "3.23.8", diff --git a/yarn.lock b/yarn.lock index a010c615be..d0741715da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6144,20 +6144,13 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== -"@react-native-async-storage/async-storage@2.2.0": +"@react-native-async-storage/async-storage@2.2.0", "@react-native-async-storage/async-storage@^1.15.2": version "2.2.0" resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz#a3aa565253e46286655560172f4e366e8969f5ad" integrity sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw== dependencies: merge-options "^3.0.4" -"@react-native-async-storage/async-storage@^1.15.2": - version "1.22.0" - resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.0.tgz#202a9afd15a5b829c39b709d0ca3942612441efc" - integrity sha512-b5KD010iiZnot86RbAaHpLuHwmPW2qA3SSN/OSZhd1kBoINEQEVBuv+uFtcaTxAhX27bT0wd13GOb2IOSDUXSA== - dependencies: - merge-options "^3.0.4" - "@react-native/assets-registry@0.81.5": version "0.81.5" resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.81.5.tgz#d22c924fa6f6d4a463c5af34ce91f38756c0fa7d" From 226b19ae4327bbd4621e2eb25470b02df1e5e933 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 14 Jan 2026 22:22:26 +0200 Subject: [PATCH 15/17] Reduce startup hang by not querying fonts (#9670) * query fonts lazily * use `lazy var` --- patches/expo-font+14.0.10.patch | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 patches/expo-font+14.0.10.patch diff --git a/patches/expo-font+14.0.10.patch b/patches/expo-font+14.0.10.patch new file mode 100644 index 0000000000..ac7526e23d --- /dev/null +++ b/patches/expo-font+14.0.10.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/expo-font/ios/FontLoaderModule.swift b/node_modules/expo-font/ios/FontLoaderModule.swift +index 183480f..7b64f6e 100644 +--- a/node_modules/expo-font/ios/FontLoaderModule.swift ++++ b/node_modules/expo-font/ios/FontLoaderModule.swift +@@ -2,10 +2,9 @@ import ExpoModulesCore + + public final class FontLoaderModule: Module { + // could be a Set, but to be able to pass to JS we keep it as an array +- private var registeredFonts: [String] ++ private lazy var registeredFonts: [String] = queryCustomNativeFonts() + + public required init(appContext: AppContext) { +- self.registeredFonts = queryCustomNativeFonts() + super.init(appContext: appContext) + } + From e684c21572074d34c2ada7875c5e790f6dbe2f63 Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Wed, 14 Jan 2026 12:53:24 -0800 Subject: [PATCH 16/17] Improved client events for feed interactions (#9695) --- src/logger/metrics.ts | 8 ++++++++ src/state/queries/post.ts | 40 +++++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index dddca72a17..0af92a6e7c 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -244,6 +244,8 @@ export type MetricEvents = { isReply: boolean } 'post:like': { + uri: string + authorDid: string doesLikerFollowPoster: boolean | undefined doesPosterFollowLiker: boolean | undefined likerClout: number | undefined @@ -252,14 +254,20 @@ export type MetricEvents = { feedDescriptor?: string } 'post:repost': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } 'post:unlike': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } 'post:unrepost': { + uri: string + authorDid: string logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' feedDescriptor?: string } diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 8fdfc584b6..3043485b3e 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -111,7 +111,7 @@ export function usePostLikeMutationQueue( const postCid = post.cid const initialLikeUri = post.viewer?.like const likeMutation = usePostLikeMutation(feedDescriptor, logContext, post) - const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext) + const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext, post) const queueToggle = useToggleMutationQueue({ initialState: initialLikeUri, @@ -182,6 +182,8 @@ function usePostLikeMutation( ownProfile = findProfileQueryData(queryClient, currentAccount.did) } logger.metric('post:like', { + uri, + authorDid: postAuthor.did, logContext, doesPosterFollowLiker: postAuthor.viewer ? Boolean(postAuthor.viewer.followedBy) @@ -206,11 +208,17 @@ function usePostLikeMutation( function usePostUnlikeMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:unlike']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation({ - mutationFn: ({likeUri}) => { - logger.metric('post:unlike', {logContext, feedDescriptor}) + mutationFn: ({postUri, likeUri}) => { + logger.metric('post:unlike', { + uri: postUri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.deleteLike(likeUri) }, }) @@ -227,8 +235,12 @@ export function usePostRepostMutationQueue( const postUri = post.uri const postCid = post.cid const initialRepostUri = post.viewer?.repost - const repostMutation = usePostRepostMutation(feedDescriptor, logContext) - const unrepostMutation = usePostUnrepostMutation(feedDescriptor, logContext) + const repostMutation = usePostRepostMutation(feedDescriptor, logContext, post) + const unrepostMutation = usePostUnrepostMutation( + feedDescriptor, + logContext, + post, + ) const queueToggle = useToggleMutationQueue({ initialState: initialRepostUri, @@ -280,6 +292,7 @@ export function usePostRepostMutationQueue( function usePostRepostMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:repost']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation< @@ -288,7 +301,12 @@ function usePostRepostMutation( {uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present >({ mutationFn: ({uri, cid, via}) => { - logger.metric('post:repost', {logContext, feedDescriptor}) + logger.metric('post:repost', { + uri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.repost(uri, cid, via) }, }) @@ -297,11 +315,17 @@ function usePostRepostMutation( function usePostUnrepostMutation( feedDescriptor: string | undefined, logContext: LogEvents['post:unrepost']['logContext'], + post: Shadow, ) { const agent = useAgent() return useMutation({ - mutationFn: ({repostUri}) => { - logger.metric('post:unrepost', {logContext, feedDescriptor}) + mutationFn: ({postUri, repostUri}) => { + logger.metric('post:unrepost', { + uri: postUri, + authorDid: post.author.did, + logContext, + feedDescriptor, + }) return agent.deleteRepost(repostUri) }, }) From 5922c6622fcc61f008296c09406be1c54bf8667b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 14 Jan 2026 15:09:01 -0600 Subject: [PATCH 17/17] [APP-1767] Live event feeds (#9696) * Update env config for new worker setup * Add liveEvents context * Add live event feed card to Explore * Add Discover banner/trending handling, device storage * Add prefs methods, hook up to Discover banner * Add settings toggle * Fix up metrics and mutations * Simplify undo logic * Handle previews * Update overlay color and text color handling * Update metrics * Handle live event in feed card * Show multiple feeds on Explore for bsky team * Fix dev/preview handling bug * Add sidebar cards, update types * Fix type error * Fix type error * Feedback * Blurhash fix * Copy update * Dev only debug hook --- .env.example | 7 +- .eslintrc.js | 1 + package.json | 2 +- src/App.native.tsx | 93 +++++----- src/App.web.tsx | 85 +++++---- src/alf/index.tsx | 7 +- src/components/FeedCard.tsx | 38 +++- src/components/FeedInterstitials.tsx | 1 + src/components/Post/Embed/FeedEmbed.tsx | 6 +- src/components/interstitials/Trending.tsx | 2 +- src/env/common.ts | 20 +- ...verFeedLiveEventFeedsAndTrendingBanner.tsx | 89 +++++++++ .../ExploreScreenLiveEventFeedsBanner.tsx | 17 ++ .../components/LiveEventFeedCardCompact.tsx | 132 ++++++++++++++ .../components/LiveEventFeedCardWide.tsx | 139 ++++++++++++++ .../components/LiveEventFeedOptionsMenu.tsx | 171 ++++++++++++++++++ .../LiveEventFeedsSettingsToggle.tsx | 43 +++++ .../SidebarLiveEventFeedsBanner.tsx | 13 ++ src/features/liveEvents/context.tsx | 110 +++++++++++ src/features/liveEvents/preferences.ts | 161 +++++++++++++++++ src/features/liveEvents/types.ts | 27 +++ src/geolocation/const.ts | 4 +- src/lib/hooks/useIsBskyTeam.ts | 8 + src/lib/statsig/gates.ts | 1 + src/logger/index.ts | 2 + src/logger/metrics.ts | 24 +++ src/screens/Search/Explore.tsx | 10 + .../Settings/ContentAndMediaSettings.tsx | 9 +- src/state/queries/preferences/const.ts | 4 + src/view/com/feeds/ComposerPrompt.tsx | 2 - src/view/com/posts/PostFeed.tsx | 22 ++- src/view/shell/desktop/RightNav.tsx | 7 +- yarn.lock | 40 ++-- 33 files changed, 1162 insertions(+), 135 deletions(-) create mode 100644 src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx create mode 100644 src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedCardCompact.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedCardWide.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx create mode 100644 src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx create mode 100644 src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx create mode 100644 src/features/liveEvents/context.tsx create mode 100644 src/features/liveEvents/preferences.ts create mode 100644 src/features/liveEvents/types.ts create mode 100644 src/lib/hooks/useIsBskyTeam.ts diff --git a/.env.example b/.env.example index 96a1548d66..e90541cabb 100644 --- a/.env.example +++ b/.env.example @@ -34,5 +34,8 @@ EXPO_PUBLIC_SENTRY_DSN= # Bitdrift API key. If undefined, Bitdrift will be disabled. EXPO_PUBLIC_BITDRIFT_API_KEY= -# bapp-config web worker URL -BAPP_CONFIG_DEV_URL= +# geolocation web worker URL +GEOLOCATION_DEV_URL= + +# live-events web worker URL +LIVE_EVENTS_DEV_URL= diff --git a/.eslintrc.js b/.eslintrc.js index 37ed895aa4..16e844a050 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { 'Admonition', 'Admonition.Admonition', 'Toast.Action', + 'toast.Action', 'AgeAssuranceAdmonition', 'Span', 'StackedButton', diff --git a/package.json b/package.json index 61e042e05b..32b78e18fb 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.18.13", + "@atproto/api": "^0.18.15", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.6", diff --git a/src/App.native.tsx b/src/App.native.tsx index 34c3cc204c..2c4d6fa413 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -69,6 +69,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' @@ -92,6 +96,7 @@ if (isAndroid) { */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -141,49 +146,51 @@ function InnerApp() { - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 956c52005c..460d9ff174 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -57,6 +57,10 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {ToastOutlet} from '#/components/Toast' import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' import {prefetchAgeAssuranceConfig} from '#/ageAssurance' +import { + prefetchLiveEvents, + Provider as LiveEventsProvider, +} from '#/features/liveEvents/context' import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' @@ -67,6 +71,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom */ Geo.resolve() prefetchAgeAssuranceConfig() +prefetchLiveEvents() function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -117,45 +122,47 @@ function InnerApp() { - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/alf/index.tsx b/src/alf/index.tsx index eed3fcbeb2..3aff9ddb57 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -11,7 +11,12 @@ import { import {themes} from '#/alf/themes' import {type Device} from '#/storage' -export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf' +export { + type TextStyleProp, + type Theme, + utils, + type ViewStyleProp, +} from '@bsky.app/alf' export {atoms} from '#/alf/atoms' export * from '#/alf/breakpoints' export * from '#/alf/fonts' diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index c5e6200527..1fcedc6916 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import {type GestureResponderEvent, View} from 'react-native' import { type AppBskyFeedDefs, @@ -21,19 +21,21 @@ import { import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, select, useTheme} from '#/alf' import { Button, ButtonIcon, type ButtonProps, ButtonText, } from '#/components/Button' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin' import {Link as InternalLink, type LinkProps} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {RichText, type RichTextProps} from '#/components/RichText' import {Text} from '#/components/Typography' +import {useActiveLiveEventFeedUris} from '#/features/liveEvents/context' import type * as bsky from '#/types/bsky' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from './icons/Trash' @@ -49,7 +51,11 @@ export function Default(props: Props) {
- +
@@ -118,14 +124,40 @@ export function AvatarPlaceholder({size = 40}: Omit) { export function TitleAndByline({ title, creator, + uri, }: { title: string creator?: bsky.profile.AnyProfileView + uri?: string }) { const t = useTheme() + const activeLiveEvents = useActiveLiveEventFeedUris() + const liveColor = useMemo( + () => + select(t.name, { + dark: t.palette.negative_600, + dim: t.palette.negative_600, + light: t.palette.negative_500, + }), + [t], + ) return ( + {uri && activeLiveEvents.has(uri) && ( + + + + Happening now + + + )} + style={[a.border, t.atoms.border_contrast_low, a.p_sm, a.rounded_md]}> - + - ) diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 3d47385ee8..830dfb3b66 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -41,7 +41,7 @@ export function Inner() { }, [setTrendingDisabled]) return error || noTopics ? null : ( - + + } else { + // no feed, no trending + return null + } + } + + // On desktop, we show in the sidebar + if (rightNavVisible) return null + + return events.feeds.map(feed => ) +} + +function Inner({feed}: {feed: LiveEventFeed}) { + const {_} = useLingui() + const optionsMenuControl = useDialogControl() + const layout = feed.layouts.wide + + return ( + <> + + + + + + + + + + + ) +} diff --git a/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx new file mode 100644 index 0000000000..bf80ed6a2e --- /dev/null +++ b/src/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner.tsx @@ -0,0 +1,17 @@ +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {LiveEventFeedCardWide} from '#/features/liveEvents/components/LiveEventFeedCardWide' +import {useLiveEvents} from '#/features/liveEvents/context' + +export function ExploreScreenLiveEventFeedsBanner() { + const t = useTheme() + const events = useLiveEvents() + return events.feeds.map(feed => ( + + + + )) +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx new file mode 100644 index 0000000000..69c24b2dc5 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx @@ -0,0 +1,132 @@ +import {useEffect, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {atoms as a, utils} from '#/alf' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +const roundedStyles = [a.rounded_md, a.curve_continuous] + +export function LiveEventFeedCardCompact({ + feed, + metricContext, +}: { + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + + const layout = feed.layouts.compact + const overlayColor = layout.overlayColor + const textColor = layout.textColor + const url = useMemo(() => { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname + } + return '/' + }, [feed.url]) + + useEffect(() => { + logger.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + { + logger.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }) + }}> + {({hovered, pressed}) => ( + + + + + + + + + + + + + {layout.title} + + + + + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedCardWide.tsx b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx new file mode 100644 index 0000000000..1ed3a49df2 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedCardWide.tsx @@ -0,0 +1,139 @@ +import {useEffect, useMemo} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {LinearGradient} from 'expo-linear-gradient' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isBskyCustomFeedUrl} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {atoms as a, useBreakpoints, utils} from '#/alf' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +const roundedStyles = [a.rounded_lg, a.curve_continuous] + +export function LiveEventFeedCardWide({ + feed, + metricContext, +}: { + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + const {gtPhone} = useBreakpoints() + + const layout = feed.layouts.wide + const overlayColor = layout.overlayColor + const textColor = layout.textColor + const url = useMemo(() => { + // Validated in multiple places on the backend + if (isBskyCustomFeedUrl(feed.url)) { + return new URL(feed.url).pathname + } + return '/' + }, [feed.url]) + + useEffect(() => { + logger.metric('liveEvents:feedBanner:seen', { + feed: feed.url, + context: metricContext, + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + { + logger.metric('liveEvents:feedBanner:click', { + feed: feed.url, + context: metricContext, + }) + }}> + {({hovered, pressed}) => ( + + + + + + + + + + + + {feed.preview ? ( + Preview + ) : ( + Happening now + )} + + + {layout.title} + + + + + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx new file mode 100644 index 0000000000..b2f50840a9 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx @@ -0,0 +1,171 @@ +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useCleanError} from '#/lib/hooks/useCleanError' +import {isNative} from '#/platform/detection' +import {atoms as a, web} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Loader} from '#/components/Loader' +import * as toast from '#/components/Toast' +import {Span, Text} from '#/components/Typography' +import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +export {useDialogControl} from '#/components/Dialog' + +export function LiveEventFeedOptionsMenu({ + control, + feed, + metricContext, +}: { + control: Dialog.DialogControlProps + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + return ( + + + + + + + + ) +} + +function Inner({ + control, + feed, + metricContext, +}: { + control: Dialog.DialogControlProps + feed: LiveEventFeed + metricContext: LiveEventFeedMetricContext +}) { + const {_} = useLingui() + const { + isPending, + mutate: update, + error: rawError, + variables, + } = useUpdateLiveEventPreferences({ + feed, + metricContext, + onUpdateSuccess({undoAction}) { + toast.show( + + + + Your live event preferences have been updated. + + {undoAction && ( + { + if (undoAction) { + update(undoAction) + } + }}> + Undo + + )} + , + { + type: 'success', + }, + ) + + /* + * If there is no `undoAction`, it means that the action was already + * undone, and therefore the menu would have been closed prior to the + * undo happening. + */ + if (undoAction) { + control.close() + } + }, + }) + const cleanError = useCleanError() + const error = rawError ? cleanError(rawError) : undefined + + const isHidingFeed = variables?.type === 'hideFeed' && isPending + const isHidingAllFeeds = variables?.type === 'toggleHideAllFeeds' && isPending + + return ( + + + + Live event options + + + + + Live events appear occasionally when something exciting is + happening. If you'd like, you can hide this particular event, or all + events for this placement in your app interface. + + + + + + If you choose to hide all events, you can always re-enable them from{' '} + Settings → Content & Media. + + + + + + + + {isNative && ( + + )} + + + {error && ( + + {error.clean || error.raw || _(msg`An unknown error occurred.`)} + + )} + + ) +} diff --git a/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx new file mode 100644 index 0000000000..35d4039ce1 --- /dev/null +++ b/src/features/liveEvents/components/LiveEventFeedsSettingsToggle.tsx @@ -0,0 +1,43 @@ +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import * as Toggle from '#/components/forms/Toggle' +import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' +import { + useLiveEventPreferences, + useUpdateLiveEventPreferences, +} from '#/features/liveEvents/preferences' + +export function LiveEventFeedsSettingsToggle() { + const {_} = useLingui() + const {data: prefs} = useLiveEventPreferences() + const { + isPending, + data: updatedPrefs, + mutate: update, + } = useUpdateLiveEventPreferences({ + metricContext: 'settings', + }) + const hideAllFeeds = !!(updatedPrefs || prefs)?.hideAllFeeds + + return ( + { + if (!isPending) { + update({type: 'toggleHideAllFeeds'}) + } + }}> + + + + Show live events in your Discover Feed + + + + + ) +} diff --git a/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx new file mode 100644 index 0000000000..25a5db8c8c --- /dev/null +++ b/src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx @@ -0,0 +1,13 @@ +import {LiveEventFeedCardCompact} from '#/features/liveEvents/components/LiveEventFeedCardCompact' +import {useLiveEvents} from '#/features/liveEvents/context' + +export function SidebarLiveEventFeedsBanner() { + const events = useLiveEvents() + return events.feeds.map(feed => ( + + )) +} diff --git a/src/features/liveEvents/context.tsx b/src/features/liveEvents/context.tsx new file mode 100644 index 0000000000..fa2aba2260 --- /dev/null +++ b/src/features/liveEvents/context.tsx @@ -0,0 +1,110 @@ +import {createContext, useContext, useMemo} from 'react' +import {QueryClient, useQuery} from '@tanstack/react-query' + +import {useIsBskyTeam} from '#/lib/hooks/useIsBskyTeam' +import { + convertBskyAppUrlIfNeeded, + isBskyCustomFeedUrl, + makeRecordUri, +} from '#/lib/strings/url-helpers' +import {IS_DEV, LIVE_EVENTS_URL} from '#/env' +import {useLiveEventPreferences} from '#/features/liveEvents/preferences' +import {type LiveEventsWorkerResponse} from '#/features/liveEvents/types' +import {useDevMode} from '#/storage/hooks/dev-mode' + +const qc = new QueryClient() +const liveEventsQueryKey = ['live-events'] + +export const DEFAULT_LIVE_EVENTS = { + feeds: [], +} + +async function fetchLiveEvents(): Promise { + try { + const res = await fetch(`${LIVE_EVENTS_URL}/config`) + if (!res.ok) return null + const data = await res.json() + return data + } catch { + return null + } +} + +const Context = createContext(DEFAULT_LIVE_EVENTS) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [isDevMode] = useDevMode() + const isBskyTeam = useIsBskyTeam() + const {data} = useQuery( + { + staleTime: IS_DEV ? 5e3 : 1000 * 60, + queryKey: liveEventsQueryKey, + async queryFn() { + return fetchLiveEvents() + }, + }, + qc, + ) + + const ctx = useMemo(() => { + if (!data) return DEFAULT_LIVE_EVENTS + const feeds = data.feeds.filter(f => { + if (f.preview && !isBskyTeam) return false + return true + }) + return { + ...data, + // only one at a time for now, unless bsky team and dev mode + feeds: isBskyTeam && isDevMode ? feeds : feeds.slice(0, 1), + } + }, [data, isBskyTeam, isDevMode]) + + return {children} +} + +export async function prefetchLiveEvents() { + const data = await fetchLiveEvents() + if (data) { + qc.setQueryData(liveEventsQueryKey, data) + } +} + +export function useLiveEvents() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useLiveEventsContext must be used within a Provider') + } + return ctx +} + +export function useUserPreferencedLiveEvents() { + const events = useLiveEvents() + const {data, isLoading} = useLiveEventPreferences() + if (isLoading) return DEFAULT_LIVE_EVENTS + const {hideAllFeeds, hiddenFeedIds} = data + return { + ...events, + feeds: hideAllFeeds + ? [] + : events.feeds.filter(f => { + const hidden = f?.id ? hiddenFeedIds.includes(f?.id || '') : false + return !hidden + }), + } +} + +export function useActiveLiveEventFeedUris() { + const {feeds} = useLiveEvents() + + return new Set( + feeds + // insurance + .filter(f => isBskyCustomFeedUrl(f.url)) + .map(f => { + const uri = convertBskyAppUrlIfNeeded(f.url) + const [_0, did, _1, rkey] = uri.split('/').filter(Boolean) + const urip = makeRecordUri(did, 'app.bsky.feed.generator', rkey) + return urip.toString() + }), + ) +} diff --git a/src/features/liveEvents/preferences.ts b/src/features/liveEvents/preferences.ts new file mode 100644 index 0000000000..2f7af094a8 --- /dev/null +++ b/src/features/liveEvents/preferences.ts @@ -0,0 +1,161 @@ +import {useEffect} from 'react' +import {type Agent, AppBskyActorDefs, asPredicate} from '@atproto/api' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import { + preferencesQueryKey, + usePreferencesQuery, +} from '#/state/queries/preferences' +import {useAgent} from '#/state/session' +import * as env from '#/env' +import { + type LiveEventFeed, + type LiveEventFeedMetricContext, +} from '#/features/liveEvents/types' + +export type LiveEventPreferencesAction = Parameters< + Agent['updateLiveEventPreferences'] +>[0] & { + /** + * Flag that is internal to this hook, do not set when updating prefs + */ + __canUndo?: boolean +} + +export function useLiveEventPreferences() { + const query = usePreferencesQuery() + useWebOnlyDebugLiveEventPreferences() + return { + ...query, + data: query.data?.liveEventPreferences || { + hideAllFeeds: false, + hiddenFeedIds: [], + }, + } +} + +function useWebOnlyDebugLiveEventPreferences() { + const queryClient = useQueryClient() + const agent = useAgent() + + useEffect(() => { + if (env.IS_DEV && isWeb && typeof window !== 'undefined') { + // @ts-ignore + window.__updateLiveEventPreferences = async ( + action: LiveEventPreferencesAction, + ) => { + await agent.updateLiveEventPreferences(action) + // triggers a refetch + await queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + } + } + }, [agent, queryClient]) +} + +export function useUpdateLiveEventPreferences(props: { + feed?: LiveEventFeed + metricContext: LiveEventFeedMetricContext + onUpdateSuccess?: (props: { + undoAction: LiveEventPreferencesAction | null + }) => void +}) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation< + AppBskyActorDefs.LiveEventPreferences, + Error, + LiveEventPreferencesAction, + {undoAction: LiveEventPreferencesAction | null} + >({ + onSettled(data, error, variables) { + /* + * `onSettled` runs after the mutation completes, success or no. The idea + * here is that we want to invert the action that was just passed in, and + * provide it as an `undoAction` to the `onUpdateSuccess` callback. + * + * If the operation was not a success, we don't provide the `undoAction`. + * + * Upon the first call of the mutation, the `__canUndo` flag is undefined, + * so we allow the undo. However, when we create the `undoAction`, we + * set its `__canUndo` flag to false, so that if the user were to call + * the undo action, we would not provide another undo for that. + */ + const canUndo = variables.__canUndo === undefined ? true : false + let undoAction: LiveEventPreferencesAction | null = null + + switch (variables.type) { + case 'hideFeed': + undoAction = {type: 'unhideFeed', id: variables.id, __canUndo: false} + break + case 'unhideFeed': + undoAction = {type: 'hideFeed', id: variables.id, __canUndo: false} + break + case 'toggleHideAllFeeds': + undoAction = {type: 'toggleHideAllFeeds', __canUndo: false} + break + } + + if (data && !error) { + props?.onUpdateSuccess?.({ + undoAction: canUndo ? undoAction : null, + }) + } + }, + mutationFn: async action => { + const updated = await agent.updateLiveEventPreferences(action) + const prefs = updated.find(p => + asPredicate(AppBskyActorDefs.validateLiveEventPreferences)(p), + ) + + switch (action.type) { + case 'hideFeed': + case 'unhideFeed': { + if (!props.feed) { + logger.error( + `useUpdateLiveEventPreferences: feed is missing, but required for hiding/unhiding`, + { + action, + }, + ) + break + } + + logger.metric( + action.type === 'hideFeed' + ? 'liveEvents:feedBanner:hide' + : 'liveEvents:feedBanner:unhide', + { + feed: props.feed.url, + context: props.metricContext, + }, + ) + break + } + case 'toggleHideAllFeeds': { + if (prefs!.hideAllFeeds) { + logger.metric('liveEvents:hideAllFeedBanners', { + context: props.metricContext, + }) + } else { + logger.metric('liveEvents:unhideAllFeedBanners', { + context: props.metricContext, + }) + } + break + } + } + + // triggers a refetch + queryClient.invalidateQueries({ + queryKey: preferencesQueryKey, + }) + + return prefs! + }, + }) +} diff --git a/src/features/liveEvents/types.ts b/src/features/liveEvents/types.ts new file mode 100644 index 0000000000..8bc88c448c --- /dev/null +++ b/src/features/liveEvents/types.ts @@ -0,0 +1,27 @@ +export type LiveEventFeedImageLayout = 'wide' | 'compact' // maybe more in the future + +export type LiveEventFeedLayout = { + title: string + overlayColor: string + textColor: string + image: string + blurhash: string +} + +export type LiveEventFeed = { + id: string + preview: boolean + title: string + url: string + layouts: Record +} + +export type LiveEventsWorkerResponse = { + feeds: LiveEventFeed[] +} + +export type LiveEventFeedMetricContext = + | 'explore' + | 'discover' + | 'sidebar' + | 'settings' diff --git a/src/geolocation/const.ts b/src/geolocation/const.ts index b12f37140f..653e829bad 100644 --- a/src/geolocation/const.ts +++ b/src/geolocation/const.ts @@ -1,7 +1,7 @@ -import {BAPP_CONFIG_URL} from '#/env' +import {GEOLOCATION_URL} from '#/env' import {type Geolocation} from '#/geolocation/types' -export const GEOLOCATION_SERVICE_URL = `${BAPP_CONFIG_URL}/geolocation` +export const GEOLOCATION_SERVICE_URL = `${GEOLOCATION_URL}/geolocation` /** * Default geolocation config. diff --git a/src/lib/hooks/useIsBskyTeam.ts b/src/lib/hooks/useIsBskyTeam.ts new file mode 100644 index 0000000000..d653e7fd5e --- /dev/null +++ b/src/lib/hooks/useIsBskyTeam.ts @@ -0,0 +1,8 @@ +import {useMemo} from 'react' + +import {useGate} from '#/lib/statsig/statsig' + +export function useIsBskyTeam() { + const gate = useGate() + return useMemo(() => gate('is_bsky_team_member'), [gate]) +} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 357548d3b1..dcb330764c 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -7,6 +7,7 @@ export type Gate = | 'disable_settings_find_contacts' | 'explore_show_suggested_feeds' | 'feed_reply_button_open_thread' + | 'is_bsky_team_member' // special, do not remove | 'old_postonboarding' | 'onboarding_add_video_feed' | 'onboarding_suggested_starterpacks' diff --git a/src/logger/index.ts b/src/logger/index.ts index 998d025818..57d430b5dd 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -16,6 +16,8 @@ import {enabledLogLevels} from '#/logger/util' import {isNative} from '#/platform/detection' import {ENV} from '#/env' +export {type MetricEvents as Metrics} from '#/logger/metrics' + const TRANSPORTS: Transport[] = (function configureTransports() { switch (ENV) { case 'production': { diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 0af92a6e7c..7afe7f3bbe 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -1,5 +1,6 @@ import {type NotificationReason} from '#/lib/hooks/useNotificationHandler' import {type FeedDescriptor} from '#/state/queries/post-feed' +import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types' export type MetricEvents = { // App events @@ -796,4 +797,27 @@ export type MetricEvents = { } // user pressed the remove all data button 'contacts:settings:removeData': {} + + 'liveEvents:feedBanner:seen': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:click': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:hide': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:feedBanner:unhide': { + feed: string + context: LiveEventFeedMetricContext + } + 'liveEvents:hideAllFeedBanners': { + context: LiveEventFeedMetricContext + } + 'liveEvents:unhideAllFeedBanners': { + context: LiveEventFeedMetricContext + } } diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index b3e26dcfd3..6b49588db9 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -68,6 +68,7 @@ import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import {SubtleHover} from '#/components/SubtleHover' import {Text} from '#/components/Typography' +import {ExploreScreenLiveEventFeedsBanner} from '#/features/liveEvents/components/ExploreScreenLiveEventFeedsBanner' import * as ModuleHeader from './components/ModuleHeader' import { SuggestedAccountsTabBar, @@ -201,6 +202,10 @@ type ExploreScreenItems = type: 'interests-card' key: 'interests-card' } + | { + type: 'liveEventFeedsBanner' + key: string + } export function Explore({ focusSearchInput, @@ -684,6 +689,8 @@ export function Explore({ i.push(topBorder) i.push(...interestsNuxModule) + i.push({type: 'liveEventFeedsBanner', key: 'liveEventFeedsBanner'}) + if (useFullExperience) { i.push(trendingTopicsModule) i.push(...suggestedFeedsModule) @@ -998,6 +1005,9 @@ export function Explore({ case 'interests-card': { return } + case 'liveEventFeedsBanner': { + return + } } }, [ diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index e1144d0645..ebcf95a7af 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -26,6 +26,7 @@ import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import * as Layout from '#/components/Layout' +import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -124,7 +125,7 @@ export function ContentAndMediaSettingsScreen({}: Props) { - {trendingEnabled && ( + {trendingEnabled ? ( <> + + ) : ( + <> + + + )} diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 54d063a502..7508fa4f6d 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -45,4 +45,8 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { verificationPrefs: { hideBadges: false, }, + liveEventPreferences: { + hideAllFeeds: false, + hiddenFeedIds: [], + }, } diff --git a/src/view/com/feeds/ComposerPrompt.tsx b/src/view/com/feeds/ComposerPrompt.tsx index be88ee2dc1..5d4685a4e6 100644 --- a/src/view/com/feeds/ComposerPrompt.tsx +++ b/src/view/com/feeds/ComposerPrompt.tsx @@ -148,8 +148,6 @@ export function ComposerPrompt() { a.relative, a.flex_row, a.align_start, - a.border_t, - t.atoms.border_contrast_low, { paddingLeft: 18, paddingRight: 15, diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 4f25468c95..9e6fcdcccb 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -70,6 +70,7 @@ import { } from '#/components/feeds/PostFeedVideoGridRow' import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' +import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' import {ComposerPrompt} from '../feeds/ComposerPrompt' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FeedShutdownMsg} from './FeedShutdownMsg' @@ -155,6 +156,10 @@ type FeedRow = type: 'composerPrompt' key: string } + | { + type: 'liveEventFeedsAndTrendingBanner' + key: string + } export function getItemsForFeedback(feedRow: FeedRow): { item: FeedPostSliceItem @@ -360,7 +365,7 @@ let PostFeed = ({ const showProgressIntersitial = (followProgressGuide || followAndLikeProgressGuide) && !rightNavVisible - const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() + const {trendingVideoDisabled} = useTrendingSettings() const ageAssuranceBannerState = useAgeAssuranceBannerState() const selectedFeed = useSelectedFeed() @@ -510,13 +515,10 @@ let PostFeed = ({ }) } } - if (!rightNavVisible && !trendingDisabled) { - arr.push({ - type: 'interstitialTrending', - key: - 'interstitial2-' + sliceIndex + '-' + lastFetchedAt, - }) - } + arr.push({ + type: 'liveEventFeedsAndTrendingBanner', + key: 'liveEventFeedsAndTrendingBanner-' + sliceIndex, + }) // Show composer prompt for Discover and Following feeds if ( hasSession && @@ -672,9 +674,7 @@ let PostFeed = ({ feedTab, hasSession, showProgressIntersitial, - trendingDisabled, trendingVideoDisabled, - rightNavVisible, gtMobile, isVideoFeed, areVideoFeedsEnabled, @@ -773,6 +773,8 @@ let PostFeed = ({ return } else if (row.type === 'interstitialTrending') { return + } else if (row.type === 'liveEventFeedsAndTrendingBanner') { + return } else if (row.type === 'composerPrompt') { return } else if (row.type === 'interstitialTrendingVideos') { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 788df6e64c..04dddaf739 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -22,6 +22,7 @@ import {CENTER_COLUMN_OFFSET} from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import {ProgressGuideList} from '#/components/ProgressGuide/List' import {Text} from '#/components/Typography' +import {SidebarLiveEventFeedsBanner} from '#/features/liveEvents/components/SidebarLiveEventFeedsBanner' function useWebQueryParams() { const navigation = useNavigation() @@ -49,7 +50,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { const isSearchScreen = routeName === 'Search' const webqueryParams = useWebQueryParams() const searchQuery = webqueryParams?.q - const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery) + const showExploreScreenDuplicatedContent = + !isSearchScreen || (isSearchScreen && !!searchQuery) const {rightNavVisible, centerColumnOffset, leftNavMinimal} = useLayoutBreakpoints() @@ -90,7 +92,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) { )} - {showTrending && } + {showExploreScreenDuplicatedContent && } + {showExploreScreenDuplicatedContent && } {hasSession && ( diff --git a/yarn.lock b/yarn.lock index d0741715da..ff0610ac4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,12 +82,12 @@ "@atproto/xrpc" "^0.7.6" "@atproto/xrpc-server" "^0.10.0" -"@atproto/api@^0.18.13": - version "0.18.13" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.13.tgz#63eee310e6715752eb87748323cf9ab57dd91e4b" - integrity sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q== +"@atproto/api@^0.18.15": + version "0.18.15" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.18.15.tgz#25ce82081216bdefbf5397220de76ac3ba9e3f5d" + integrity sha512-GeaTP7HMRZa8jD6trMuTACa8t2jkFtRmcwWgrB0FT7l9jVCXrKpYupWeIeauEgWHNwWUUiaq3LmCox+HBy8ZMQ== dependencies: - "@atproto/common-web" "^0.4.11" + "@atproto/common-web" "^0.4.12" "@atproto/lexicon" "^0.6.0" "@atproto/syntax" "^0.4.2" "@atproto/xrpc" "^0.7.7" @@ -208,13 +208,13 @@ pino-http "^8.2.1" typed-emitter "^2.1.0" -"@atproto/common-web@^0.4.11": - version "0.4.11" - resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.11.tgz#eb41dc02c1ea4221388630e193d181fb098186e0" - integrity sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ== +"@atproto/common-web@^0.4.12": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.12.tgz#04135bef480d9e12cfef124ee45d8236764e7509" + integrity sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw== dependencies: - "@atproto/lex-data" "0.0.7" - "@atproto/lex-json" "0.0.7" + "@atproto/lex-data" "0.0.8" + "@atproto/lex-json" "0.0.8" zod "^3.23.8" "@atproto/common-web@^0.4.4", "@atproto/common-web@^0.4.6": @@ -415,10 +415,10 @@ uint8arrays "3.0.0" unicode-segmenter "^0.14.0" -"@atproto/lex-data@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.7.tgz#6aa87423f6d47849bec8ff3ca0b00ce93964adc8" - integrity sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw== +"@atproto/lex-data@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.8.tgz#46cc261efbfa6cc05bf04439d2d73cd8386b467d" + integrity sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA== dependencies: "@atproto/syntax" "0.4.2" multiformats "^9.9.0" @@ -451,12 +451,12 @@ "@atproto/lex-data" "0.0.3" tslib "^2.8.1" -"@atproto/lex-json@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.7.tgz#c06e1fc3e06d739bbb74694f5d846055bed37866" - integrity sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g== +"@atproto/lex-json@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.8.tgz#03290762d9368b029488ee0a0766d1a34063255c" + integrity sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg== dependencies: - "@atproto/lex-data" "0.0.7" + "@atproto/lex-data" "0.0.8" tslib "^2.8.1" "@atproto/lex-resolver@0.0.5":