diff --git a/.claude/.pre-pr-sweep-state b/.claude/.pre-pr-sweep-state new file mode 100644 index 0000000000..f6f2d8d645 --- /dev/null +++ b/.claude/.pre-pr-sweep-state @@ -0,0 +1 @@ +alt-blur@3076a2b5a593b0d7b37198b5aebfe3a222afd52d diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..9404af75f7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,44 @@ +version: 2 +# Dependabot auto-update config. +# +# Cooldown (7 days) is the point of this config: it delays version-update +# PRs until a newly-published version has aged. Supply-chain attacks like +# the tanstack Shai-Hulud compromise (2026-05-11) live minutes-to-hours +# before the registry yanks them; a 7-day cooldown keeps poisoned +# versions out of our lockfiles. +# +# Security updates bypass cooldown and continue to flow immediately. See: +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#cooldown +# +# Auto-merge is deliberately NOT enabled. Every dependabot PR gets human +# review. + +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + groups: + production: + dependency-type: production + update-types: [minor, patch] + development: + dependency-type: development + update-types: [minor, patch] + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + cooldown: + default-days: 7 + open-pull-requests-limit: 3 + groups: + actions: + patterns: ["*"] + update-types: [minor, patch] diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index 719b2e836e..fe9d5b1c5b 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -128,7 +128,7 @@ jobs: - name: 🔔 Notify Slack of Production Build if: ${{ inputs.profile == 'production' }} - uses: slackapi/slack-github-action@v2.1.1 + uses: slackapi/slack-github-action@v3.0.3 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook @@ -138,7 +138,7 @@ jobs: - name: 🔔 Notify Slack of Testflight Build if: ${{ inputs.profile != 'production' }} - uses: slackapi/slack-github-action@v2.1.1 + uses: slackapi/slack-github-action@v3.0.3 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook @@ -174,7 +174,7 @@ jobs: - name: 🔔 Notify Slack of Production APK Build if: ${{ inputs.profile == 'production' }} - uses: slackapi/slack-github-action@v2.1.1 + uses: slackapi/slack-github-action@v3.0.3 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index 151941f5e4..d2cc7a7abb 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -155,7 +155,7 @@ jobs: - name: 🔔 Notify Slack of Production Build if: ${{ inputs.profile == 'production' }} - uses: slackapi/slack-github-action@v2.1.1 + uses: slackapi/slack-github-action@v3.0.3 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index 22ea2d5cba..5f1f0a4e28 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -71,18 +71,18 @@ jobs: profile: ${{ inputs.channel || 'testflight' }} previous-commit-tag: ${{ inputs.runtimeVersion }} - - name: Lint check - run: pnpm lint - - - name: Prettier check - run: pnpm prettier --check . - - name: 🔤 Compile translations run: pnpm intl:build 2>&1 | tee i18n.log - name: Check for i18n compilation errors run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + - name: Lint check + run: pnpm lint + + - name: Prettier check + run: pnpm prettier --check . + - name: Type check run: pnpm typecheck @@ -386,7 +386,7 @@ jobs: path: build.apk - name: 🔔 Notify Slack - uses: slackapi/slack-github-action@v2.1.1 + uses: slackapi/slack-github-action@v3.0.3 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 93729874b6..aa6c92765d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - job: [lint, prettier] + job: [lint, prettier, typecheck] steps: - name: Check out Git repository uses: actions/checkout@v5 @@ -62,6 +62,8 @@ jobs: command: pnpm install --frozen-lockfile attempt_limit: 3 attempt_delay: 2000 + - name: Check & compile i18n + run: pnpm intl:build - name: Lint checks run: pnpm ${{ matrix.job }} # Aggregates the matrix results into a single stable check name so branch @@ -80,28 +82,6 @@ jobs: run: | echo "linting result: $RESULT" test "$RESULT" = "success" - typechecking: - name: Run typecheck - runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@v5 - - uses: pnpm/action-setup@v6 - - name: Install node - uses: actions/setup-node@v6 - with: - node-version-file: package.json - cache: pnpm - - name: pnpm install - uses: Wandalen/wretry.action@master - with: - command: pnpm install --frozen-lockfile - attempt_limit: 3 - attempt_delay: 2000 - - name: Check & compile i18n - run: pnpm intl:build - - name: Type check - run: pnpm typecheck testing: name: Run tests runs-on: ubuntu-latest diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml index c0d4e45da7..8ede462392 100644 --- a/.github/workflows/pull-request-comment.yml +++ b/.github/workflows/pull-request-comment.yml @@ -94,7 +94,7 @@ jobs: core.setOutput('head-ref', pr.data.head.ref); - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 with: header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }} number: ${{ github.event.issue.number }} @@ -128,15 +128,15 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Lint check - run: pnpm lint - - name: 🔤 Compile translations run: pnpm intl:build 2>&1 | tee i18n.log - name: Check for i18n compilation errors run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + - name: Lint check + run: pnpm lint + - name: Type check run: pnpm typecheck @@ -181,7 +181,7 @@ jobs: RUNTIME_VERSION: - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 env: ISSUE_NUMBER: ${{ github.event.issue.number }} with: @@ -198,7 +198,7 @@ jobs: *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 if: failure() with: header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }} diff --git a/.github/workflows/pull-request-commit.yml b/.github/workflows/pull-request-commit.yml index 722af1f000..976fa35662 100644 --- a/.github/workflows/pull-request-commit.yml +++ b/.github/workflows/pull-request-commit.yml @@ -96,7 +96,7 @@ jobs: excluded_assets: "(.+).chunk.js|(.+).js.map|(.+).json|(.+).png|(.+).svg|(.+).webp|(.+).jpg|(.+).ico" - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 with: header: bundle-diff message: | @@ -133,7 +133,7 @@ jobs: profile: pull-request - name: 💬 Drop a comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 if: ${{ steps.fingerprint.outputs.includes-changes }} with: header: fingerprint-diff @@ -151,7 +151,7 @@ jobs: *Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖* - name: 💬 Delete comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 if: ${{ !steps.fingerprint.outputs.includes-changes }} with: header: fingerprint-diff diff --git a/.github/workflows/sync-internal.yaml b/.github/workflows/sync-internal.yaml index 562ce96bea..03120eaebc 100644 --- a/.github/workflows/sync-internal.yaml +++ b/.github/workflows/sync-internal.yaml @@ -15,7 +15,7 @@ jobs: fetch-depth: 0 - name: Generate GitHub App Token id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.SYNC_INTERNAL_APP_ID }} private-key: ${{ secrets.SYNC_INTERNAL_PK }} diff --git a/CLAUDE.md b/CLAUDE.md index c747973c77..37e6a1c2c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -206,7 +206,7 @@ function MyComponent() { return ( - Hello + Hello ) } @@ -414,12 +414,16 @@ import {Text, H1, H2, P} from '#/components/Typography'

Heading

Paragraph text with default styling.

-Custom text +Custom text -// For text with emoji, add the emoji prop +// For text with emoji, add the emoji prop. User-generated text (e.g. display names) +// will almost certainly contain emoji, so only omit it when the text is static and +// does not contain an emoji Hello! 👋 ``` +The `Text` component's default style is `[a.text_sm, a.leading_snug, t.atoms.text]`. + ### TextField ```tsx diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts index 0b1b750281..23ffaa2875 100644 --- a/__tests__/lib/strings/url-helpers.test.ts +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -1,6 +1,7 @@ import {describe, expect, it} from '@jest/globals' import { + getChatInviteCodeFromUrl, isPossiblyAUrl, isTrustedUrl, linkRequiresWarning, @@ -178,3 +179,47 @@ describe('isTrustedUrl', () => { expect(output).toEqual(expected) }) }) + +describe('getChatInviteCodeFromUrl', () => { + type Case = [string, string | undefined] + + const cases: Case[] = [ + ['https://bsky.app/c/abcdefg', 'abcdefg'], + ['https://bsky.app/c/abcdefghij', 'abcdefghij'], + // http is not recognized as a bsky.app url + ['http://bsky.app/c/abcdefg', undefined], + ['https://bsky.app/c/abcdefg?utm=foo', 'abcdefg'], + ['https://bsky.app/c/abcdefg#section', 'abcdefg'], + ['/c/abcdefg', 'abcdefg'], + ['/c/abcdefg?utm=foo', 'abcdefg'], + ['/c/abcdefg#section', 'abcdefg'], + + // too short + ['https://bsky.app/c/abcdef', undefined], + ['/c/abcdef', undefined], + // too long + ['https://bsky.app/c/abcdefghijk', undefined], + ['/c/abcdefghijk', undefined], + // invalid characters + ['https://bsky.app/c/abc-def', undefined], + ['/c/abc def', undefined], + // trailing path + ['https://bsky.app/c/abcdefg/extra', undefined], + ['/c/abcdefg/extra', undefined], + // wrong path + ['https://bsky.app/profile/abcdefg', undefined], + ['https://bsky.app/c', undefined], + // wrong host + ['https://example.com/c/abcdefg', undefined], + // not a url, not a path + ['c/abcdefg', undefined], + ['abcdefg', undefined], + ['', undefined], + // malformed url + ['https://[invalid/c/abcdefg', undefined], + ] + + it.each(cases)('given input %p, returns %p', (input, expected) => { + expect(getChatInviteCodeFromUrl(input)).toEqual(expected) + }) +}) diff --git a/assets/images/chat-desktop-bg-dark.webp b/assets/images/chat-desktop-bg-dark.webp new file mode 100644 index 0000000000..9a3929f2f2 Binary files /dev/null and b/assets/images/chat-desktop-bg-dark.webp differ diff --git a/assets/images/chat-desktop-bg-light.webp b/assets/images/chat-desktop-bg-light.webp new file mode 100644 index 0000000000..099a1045a3 Binary files /dev/null and b/assets/images/chat-desktop-bg-light.webp differ diff --git a/assets/images/chat-mobile-bg-dark.webp b/assets/images/chat-mobile-bg-dark.webp new file mode 100644 index 0000000000..1ed3c11051 Binary files /dev/null and b/assets/images/chat-mobile-bg-dark.webp differ diff --git a/assets/images/chat-mobile-bg-light.webp b/assets/images/chat-mobile-bg-light.webp new file mode 100644 index 0000000000..f14a816c70 Binary files /dev/null and b/assets/images/chat-mobile-bg-light.webp differ diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 4a4ba2fde3..cc5119e8d4 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -339,6 +339,7 @@ func serve(cctx *cli.Context) error { e.GET("/messages/inbox", server.WebGeneric) e.GET("/messages/:conversation", server.WebGeneric) e.GET("/messages/:conversation/settings", server.WebGeneric) + e.GET("/messages/:conversation/requests", server.WebGeneric) // profile endpoints; only first populates info e.GET("/profile/:handleOrDID", server.WebProfile) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 40b918e771..d8555ace5b 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1,12 +1,72 @@ { + "src/Navigation.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/Splash.android.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/Splash.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/ageAssurance/components/RedirectOverlay.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/ageAssurance/util.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/alf/util/flatten.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "src/alf/util/systemUI.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/alf/util/useGutters.ts": { + "react-hooks/immutability": { + "count": 5 } }, "src/analytics/PassiveAnalytics.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/analytics/features/index.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/analytics/identifiers/session.test.ts": { + "@typescript-eslint/no-unsafe-call": { + "count": 9 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 } }, "src/analytics/metadata.ts": { @@ -17,16 +77,84 @@ "src/analytics/metrics/client.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 8 + }, + "@typescript-eslint/require-await": { + "count": 5 } }, "src/analytics/metrics/client.ts": { "@typescript-eslint/no-explicit-any": { "count": 5 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/analytics/utils.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/AppLanguageDropdown.tsx": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 1 + } + }, + "src/components/Autocomplete/Autocomplete.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/Autocomplete/useAutocomplete/index.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/BotAccountAlert.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/Button.tsx": { + "react-hooks/immutability": { + "count": 1 } }, "src/components/Composer/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 7 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/DebugFieldDisplay.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/components/Dialog/index.tsx": { @@ -37,6 +165,18 @@ "src/components/Dialog/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/components/DraggableList/index.web.tsx": { @@ -44,6 +184,11 @@ "count": 2 } }, + "src/components/EmojiPicker/preload.web.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, "src/components/FeedCard.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -52,16 +197,53 @@ "src/components/FocusScope/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/components/InternationalPhoneCodeSelect.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/components/KnownFollowers.tsx": { + "react-hooks/refs": { + "count": 6 + } + }, + "src/components/Lightbox/Lightbox.web.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/LikedByList.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/components/Lists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/components/Menu/types.ts": { @@ -74,34 +256,129 @@ "count": 1 } }, + "src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/components/Post/Embed/ExternalEmbed/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/components/Post/Embed/ImageEmbed.tsx": { "@typescript-eslint/no-explicit-any": { + "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/Post/Embed/StandardSiteEmbed/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/immutability": { "count": 1 } }, "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/components/PostControls/BookmarkButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/components/PostControls/DiscoverDebug.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/PostControls/ShareMenu/ShareMenuItems.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/components/PostControls/ShareMenu/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/components/ProfileHoverCard/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/refs": { + "count": 2 + } + }, + "src/components/ProgressGuide/FollowDialog.tsx": { + "react-hooks/refs": { + "count": 2 + } + }, + "src/components/RichTextTag.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/components/Select/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/components/Select/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/components/Select/types.ts": { @@ -109,9 +386,44 @@ "count": 3 } }, + "src/components/SendErrorReportDialog.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/components/StarterPack/Main/ProfilesList.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/components/StarterPack/ProfileStarterPacks.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/components/StarterPack/QrCodeDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 5 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 4 + } + }, + "src/components/StarterPack/ShareDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/components/StarterPack/Wizard/WizardEditListDialog.tsx": { @@ -119,9 +431,109 @@ "count": 1 } }, + "src/components/Tooltip/index.tsx": { + "@typescript-eslint/no-base-to-string": { + "count": 1 + } + }, + "src/components/Tooltip/index.web.tsx": { + "@typescript-eslint/no-base-to-string": { + "count": 1 + } + }, + "src/components/WhoCanReply.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/ageAssurance/AgeAssuranceErrors.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/ageAssurance/AgeAssuranceInitDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/components/contacts/components/OTPInput.tsx": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/components/contacts/screens/GetContacts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/contacts/screens/VerifyNumber.tsx": { + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/contacts/screens/ViewMatches.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/components/dialogs/DeviceLocationRequestDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/dialogs/EmailDialog/components/ResendEmailText.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/data/useAccountEmailState.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/EmailDialog/screens/Update.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/EmailDialog/screens/Verify.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/components/dialogs/InAppBrowserConsent.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/components/dialogs/LanguageSelectDialog.tsx": { @@ -129,27 +541,105 @@ "count": 1 } }, + "src/components/dialogs/LinkWarning.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, "src/components/dialogs/MutedWords.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/components/dialogs/PostInteractionSettingsDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, "react-hooks/preserve-manual-memoization": { "count": 1 } }, + "src/components/dialogs/ServerInput.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/StarterPackDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/SwitchAccount.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, "src/components/dialogs/lists/CreateOrEditListDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/dialogs/nuxs/index.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/dms/MessageItem.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/components/forms/DateField/index.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/forms/SearchInput.tsx": { + "react-hooks/refs": { + "count": 1 } }, "src/components/forms/SegmentedControl.tsx": { @@ -157,6 +647,14 @@ "count": 1 } }, + "src/components/forms/TextField.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "react-hooks/refs": { + "count": 1 + } + }, "src/components/forms/ToggleButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -165,16 +663,56 @@ "src/components/hooks/useFollowMethods.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/components/hooks/useFullscreen.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useLandingEntry.native.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useLandingEntry.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/components/hooks/useRefreshOnFocus.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/hooks/useRichText.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/components/images/AutoSizedImage.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 1 + "count": 2 } }, "src/components/images/Gallery/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 5 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/components/images/Gallery/useKeyboardHandlers.ts": { @@ -197,6 +735,51 @@ "count": 2 } }, + "src/components/intents/VerifyEmailIntentDialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/interstitials/TrendingVideos.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/components/moderation/ContentHider.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/components/moderation/LabelsOnMeDialog.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/moderation/ModerationDetailsDialog.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/components/verification/VerificationCreatePrompt.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/components/verification/VerificationRemovePrompt.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/features/liveEvents/preferences.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/features/liveNow/components/LiveStatusDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/features/liveNow/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 @@ -205,6 +788,12 @@ "src/geolocation/service.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/lib/ScrollContext.tsx": { @@ -212,9 +801,14 @@ "count": 3 } }, - "src/lib/api/index.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 + "src/lib/api/feed/demo.ts": { + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/lib/api/feed/posts.ts": { + "@typescript-eslint/require-await": { + "count": 1 } }, "src/lib/async/retry.ts": { @@ -232,9 +826,20 @@ "count": 1 } }, + "src/lib/custom-animations/CountWheel.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/lib/functions.ts": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 25 } }, "src/lib/getUserDisplayName.ts": { @@ -242,19 +847,80 @@ "count": 1 } }, + "src/lib/haptics.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/lib/hooks/useAccountSwitcher.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/lib/hooks/useAnimatedValue.ts": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/hooks/useDraggableScrollView.ts": { + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/hooks/useIntentHandler.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/lib/hooks/useNonReactiveCallback.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + } + }, + "src/lib/hooks/useNotificationHandler.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 16 + }, + "@typescript-eslint/require-await": { + "count": 1 } }, "src/lib/hooks/useOTAUpdates.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "src/lib/hooks/useOpenLink.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/lib/hooks/usePermissions.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 5 + } + }, + "src/lib/hooks/usePermissions.web.ts": { + "@typescript-eslint/require-await": { + "count": 3 } }, "src/lib/hooks/useRequireEmailVerification.tsx": { @@ -262,14 +928,33 @@ "count": 2 } }, + "src/lib/hooks/useTLDs.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/lib/hooks/useTabFocusEffect.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/lib/hooks/useToggleMutationQueue.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 } }, "src/lib/hooks/useWebScrollRestoration.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "react-hooks/immutability": { + "count": 1 } }, "src/lib/international-telephone-codes.ts": { @@ -277,9 +962,30 @@ "count": 1 } }, + "src/lib/link-meta/link-meta.ts": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, "src/lib/media/manip.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 5 + } + }, + "src/lib/media/manip.web.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 3 + } + }, + "src/lib/media/picker.web.tsx": { + "@typescript-eslint/require-await": { + "count": 2 } }, "src/lib/media/save-image.ios.ts": { @@ -297,9 +1003,34 @@ "count": 1 } }, + "src/lib/notifications/notifications.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 5 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, "src/lib/react-query.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/lib/routes/links.ts": { + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 } }, "src/lib/routes/router.ts": { @@ -312,82 +1043,298 @@ "count": 1 } }, + "src/lib/sharing.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, "src/lib/strings/errors.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 14 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 14 + } + }, + "src/lib/translation/index.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/lib/useGetEmojis/getEmojis.ts": { + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/logger/transports/sentry.ts": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 3 + } + }, + "src/logger/types.ts": { + "@typescript-eslint/no-redundant-type-constituents": { + "count": 1 } }, "src/screens/Bookmarks/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/screens/Deactivated.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/E2E/SharedPreferencesTesterScreen.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 6 + }, + "@typescript-eslint/require-await": { + "count": 6 + } + }, + "src/screens/Feeds/NoFollowingFeed.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Feeds/NoSavedFeedsOfAnyType.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Hashtag.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Home/NoFeedsPinned.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/List/ListHiddenScreen.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Log.tsx": { + "@typescript-eslint/no-unsafe-enum-comparison": { + "count": 2 } }, "src/screens/Login/ChooseAccountForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Login/ForgotPasswordForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/screens/Login/LoginForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/screens/Login/SetNewPasswordForm.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Login/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/screens/Messages/ChatList.tsx": { + "react-hooks/immutability": { + "count": 1 } }, "src/screens/Moderation/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/screens/ModerationInteractionSettings/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx": { + "react-hooks/refs": { + "count": 4 } }, "src/screens/Onboarding/StepFinished/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Onboarding/StepInterests/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Onboarding/StepProfile/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 } }, "src/screens/PostThread/index.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, "react-hooks/preserve-manual-memoization": { "count": 1 + }, + "react-hooks/refs": { + "count": 4 } }, "src/screens/Profile/Header/EditProfileDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Profile/Header/ProfileHeaderLabeler.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/screens/Profile/Header/Shell.tsx": { @@ -395,29 +1342,164 @@ "count": 1 } }, + "src/screens/Profile/KnownFollowers.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, "src/screens/Profile/Sections/Feed.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/screens/Profile/components/GermButton.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/Profile/components/ProfileFeedHeader.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 5 + } + }, + "src/screens/ProfileList/FeedSection.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/ProfileList/components/Header.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/screens/ProfileList/components/MoreOptionsMenu.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 5 + } + }, + "src/screens/ProfileList/components/SubscribeMenu.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/screens/ProfileList/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/SavedFeeds.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/screens/Search/Shell.tsx": { + "react-hooks/refs": { + "count": 4 + } + }, + "src/screens/Search/modules/ExploreSuggestedAccounts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Search/modules/ExploreTrendingTopics.tsx": { + "react-hooks/purity": { + "count": 2 + } + }, + "src/screens/Search/modules/ExploreTrendingVideos.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Settings/AboutSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Settings/AppPasswords.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/Settings/AutomationLabelSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/screens/Settings/FindContactsSettings.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Settings/InterestsSettings.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Settings/components/ChangeHandleDialog.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/screens/Settings/components/ChangePasswordDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/screens/Settings/components/CopyButton.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/screens/Settings/components/DeactivateAccountDialog.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/screens/Settings/components/DeleteAccountDialog.tsx": { @@ -425,14 +1507,114 @@ "count": 2 } }, + "src/screens/Settings/components/DisableEmail2FADialog.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 4 + } + }, + "src/screens/Settings/components/OTAInfo.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/screens/Signup/StepCaptcha/CaptchaWebView.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/screens/Signup/StepHandle/index.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/screens/Signup/StepInfo/Policies.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 } }, + "src/screens/Signup/StepInfo/index.tsx": { + "react-hooks/refs": { + "count": 3 + } + }, "src/screens/SignupQueued.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/screens/StarterPack/StarterPackLandingScreen.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/screens/StarterPack/StarterPackScreen.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/StepFeeds.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/StepProfiles.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/screens/StarterPack/Wizard/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/screens/Topic.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/state/a11y.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/cache/profile-shadow.ts": { + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/state/cache/thread-mutes.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/state/cache/types.ts": { @@ -443,31 +1625,213 @@ "src/state/feed-feedback.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "react-hooks/refs": { + "count": 2 + } + }, + "src/state/gallery.ts": { + "@typescript-eslint/prefer-promise-reject-errors": { + "count": 1 } }, "src/state/messages/events/agent.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/messages/message-drafts.tsx": { + "react-hooks/refs": { + "count": 1 } }, "src/state/persisted/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/state/persisted/index.web.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 } }, "src/state/persisted/util.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/state/preferences/alt-text-required.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/autoplay.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/disable-haptics.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/external-embeds-prefs.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/hidden-posts.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/in-app-browser.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/kawaii.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/state/preferences/languages.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/large-alt-badge.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/subtitles.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/trending.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/preferences/used-starter-packs.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/activity-subscriptions.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/actor-autocomplete.ts": { + "react-hooks/immutability": { + "count": 2 + } + }, + "src/state/queries/app-passwords.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/bookmarks/useBookmarkMutation.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/bookmarks/useBookmarksQuery.ts": { + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/state/queries/explore-feed-previews.tsx": { + "react-hooks/refs": { + "count": 4 + } + }, + "src/state/queries/feed.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/handle.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/queries/list-memberships.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/list.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 7 + } + }, + "src/state/queries/messages/accept-conversation.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/queries/messages/update-all-read.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/state/queries/my-lists.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/state/queries/notifications/feed.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/queries/notifications/unread.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/state/queries/nuxs/definitions.ts": { @@ -478,109 +1842,386 @@ "src/state/queries/pinned-post.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/state/queries/post-feed.ts": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/state/queries/postgate/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "@typescript-eslint/require-await": { + "count": 2 + } + }, + "src/state/queries/preferences/index.ts": { + "react-hooks/immutability": { + "count": 1 + } + }, + "src/state/queries/preferences/useThreadPreferences.ts": { + "react-hooks/refs": { + "count": 2 } }, "src/state/queries/search-posts.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/state/queries/starter-packs.ts": { + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/state/queries/suggested-follows.ts": { + "@typescript-eslint/no-unused-vars": { + "count": 1 } }, "src/state/queries/threadgate/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "@typescript-eslint/require-await": { + "count": 3 } }, "src/state/queries/util.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/state/session/agent.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + } + }, + "src/state/shell/color-mode.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/state/shell/composer/index.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/shell/onboarding.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 5 } }, "src/state/shell/progress-guide.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + } + }, + "src/state/shell/reminders.ts": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/state/shell/tick-every-minute.tsx": { + "react-hooks/purity": { + "count": 1 + } + }, + "src/storage/archive/index.ts": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/storage/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 8 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/auth/SplashScreen.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 } }, "src/view/com/composer/Composer.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 4 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "react-hooks/immutability": { + "count": 2 + }, "react-hooks/preserve-manual-memoization": { "count": 3 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/view/com/composer/SelectMediaButton.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/drafts/DraftsButton.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/view/com/composer/drafts/state/queries.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/com/composer/drafts/state/storage.ts": { + "@typescript-eslint/require-await": { + "count": 3 + } + }, + "src/view/com/composer/photos/EditImageDialog.web.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/photos/Gallery.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 } }, "src/view/com/composer/photos/OpenCameraBtn.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/select-language/SuggestedLanguage.tsx": { + "react-hooks/refs": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/view/com/composer/text-input/TextInput.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/com/composer/text-input/TextInput.web.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 + } + }, + "src/view/com/composer/text-input/web/Autocomplete.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, + "src/view/com/composer/videos/pickVideo.web.ts": { + "@typescript-eslint/no-misused-promises": { + "count": 1 } }, "src/view/com/feeds/ComposerPrompt.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + } + }, + "src/view/com/feeds/FeedPage.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/view/com/feeds/ProfileFeedgens.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/lists/ListMembers.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/lists/MyLists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 6 } }, "src/view/com/lists/ProfileLists.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-floating-promises": { + "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/com/modals/Modal.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/require-await": { + "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/notifications/NotificationFeed.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-floating-promises": { + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 6 + }, + "react-hooks/set-state-in-effect": { + "count": 1 } }, "src/view/com/notifications/NotificationFeedItem.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 } }, "src/view/com/pager/Pager.tsx": { "@typescript-eslint/no-explicit-any": { "count": 4 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/pager/Pager.web.tsx": { + "react-hooks/immutability": { + "count": 1 + }, "react-hooks/preserve-manual-memoization": { "count": 1 + }, + "react-hooks/refs": { + "count": 1 } }, "src/view/com/pager/PagerWithHeader.tsx": { @@ -593,14 +2234,46 @@ "count": 2 } }, + "src/view/com/pager/TabBar.tsx": { + "react-hooks/immutability": { + "count": 1 + } + }, "src/view/com/pager/TabBar.web.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/view/com/post-thread/PostLikedBy.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/post-thread/PostQuotes.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/post-thread/PostRepostedBy.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/posts/FeedShutdownMsg.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/posts/PostFeed.tsx": { @@ -611,16 +2284,47 @@ "src/view/com/posts/PostFeedErrorMessage.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 8 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 9 + } + }, + "src/view/com/profile/ProfileFollowers.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/profile/ProfileFollows.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 2 } }, "src/view/com/profile/ProfileMenu.tsx": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-floating-promises": { + "count": 4 + }, + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 6 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 12 } }, "src/view/com/testing/TestCtrls.e2e.tsx": { "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 + }, + "@typescript-eslint/no-misused-promises": { + "count": 9 } }, "src/view/com/util/EmptyState.tsx": { @@ -631,16 +2335,36 @@ "src/view/com/util/ErrorBoundary.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/EventStopper.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/Link.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 + }, + "@typescript-eslint/no-misused-promises": { + "count": 2 + } + }, + "src/view/com/util/LoadingPlaceholder.tsx": { + "react-hooks/purity": { + "count": 1 } }, "src/view/com/util/MainScrollProvider.tsx": { @@ -648,9 +2372,22 @@ "count": 2 } }, + "src/view/com/util/UserAvatar.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/view/com/util/UserBanner.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, "src/view/com/util/ViewSelector.tsx": { "@typescript-eslint/no-explicit-any": { "count": 6 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 } }, "src/view/com/util/Views.tsx": { @@ -658,14 +2395,101 @@ "count": 1 } }, + "src/view/com/util/Views.web.tsx": { + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/com/util/forms/Button.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, "src/view/screens/Debug.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 4 + } + }, + "src/view/screens/Feeds.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + }, + "@typescript-eslint/no-misused-promises": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/screens/Home.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 1 + } + }, + "src/view/screens/ModerationBlockedAccounts.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + } + }, + "src/view/screens/ModerationMutedAccounts.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 1 + } + }, + "src/view/screens/Notifications.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 3 + } + }, + "src/view/screens/Storybook/ListContained.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 2 + } + }, + "src/view/screens/Storybook/Storybook.tsx": { + "@typescript-eslint/no-misused-promises": { + "count": 1 + } + }, + "src/view/shell/Drawer.tsx": { + "@typescript-eslint/no-floating-promises": { + "count": 2 } }, "src/view/shell/createNativeStackNavigatorWithAuth.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 + }, + "@typescript-eslint/no-unsafe-call": { + "count": 1 + }, + "react-hooks/refs": { + "count": 13 + } + }, + "src/view/shell/desktop/Search.tsx": { + "@typescript-eslint/no-unsafe-call": { + "count": 3 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 + }, + "react-hooks/refs": { + "count": 3 + } + }, + "src/view/shell/index.web.tsx": { + "react-hooks/set-state-in-effect": { + "count": 1 } } } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 50ab8e5da4..c8addf66ae 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -134,11 +134,10 @@ export default defineConfig( 'react-native/no-inline-styles': 'off', ...reactNativeA11y.configs.all.rules, 'react-compiler/react-compiler': 'warn', - // TODO: Fix these and set to error - 'react-hooks/set-state-in-effect': 'warn', - 'react-hooks/purity': 'warn', - 'react-hooks/refs': 'warn', - 'react-hooks/immutability': 'warn', + 'react-hooks/set-state-in-effect': 'error', + 'react-hooks/purity': 'error', + 'react-hooks/refs': 'error', + 'react-hooks/immutability': 'error', /** * Import sorting @@ -235,9 +234,8 @@ export default defineConfig( }, ], /** - * Maintain previous behavior - these are stricter in typescript-eslint - * v8 `warn` ones are probably worth fixing. `off` ones are a bit too - * nit-picky + * Maintain previous behavior via eslint-suppressions.json - these are + * stricter in typescript-eslint v8. `off` ones are a bit too nit-picky. */ '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/ban-ts-comment': 'off', @@ -247,18 +245,18 @@ export default defineConfig( '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-member-access': 'warn', - '@typescript-eslint/no-unsafe-call': 'warn', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-misused-promises': 'warn', - '@typescript-eslint/require-await': 'warn', - '@typescript-eslint/no-unsafe-enum-comparison': 'warn', - '@typescript-eslint/no-unnecessary-type-assertion': 'warn', - '@typescript-eslint/no-redundant-type-constituents': 'warn', - '@typescript-eslint/no-duplicate-type-constituents': 'warn', - '@typescript-eslint/no-base-to-string': 'warn', - '@typescript-eslint/prefer-promise-reject-errors': 'warn', - '@typescript-eslint/await-thenable': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/require-await': 'error', + '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'error', + '@typescript-eslint/no-duplicate-type-constituents': 'error', + '@typescript-eslint/no-base-to-string': 'error', + '@typescript-eslint/prefer-promise-reject-errors': 'error', + '@typescript-eslint/await-thenable': 'error', 'no-restricted-imports': [ 'error', diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt index b7e81a61f6..1b9224c6e7 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -41,11 +41,14 @@ class BottomSheetView( private val screenHeight: Float = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) { // API 35+: edge-to-edge is mandatory, heightPixels is the full display - context.resources.displayMetrics.heightPixels.toFloat() + context.resources.displayMetrics.heightPixels + .toFloat() } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { // API 30-34: heightPixels may exclude nav bar, use currentWindowMetrics val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager - wm.currentWindowMetrics.bounds.height().toFloat() + wm.currentWindowMetrics.bounds + .height() + .toFloat() } else { // API < 30: currentWindowMetrics not available, use getRealSize // which includes system bars (heightPixels may exclude them) @@ -166,6 +169,7 @@ class BottomSheetView( when { // Full height sheets contentHeight >= screenHeight -> 0.99f + else -> this.clampRatio(this.getTargetHeight() / screenHeight) } @@ -271,8 +275,9 @@ class BottomSheetView( } // Apply deferred layout update after gesture completes if (newState != BottomSheetBehavior.STATE_DRAGGING && - newState != BottomSheetBehavior.STATE_SETTLING && - pendingLayoutUpdate) { + newState != BottomSheetBehavior.STATE_SETTLING && + pendingLayoutUpdate + ) { pendingLayoutUpdate = false updateLayout() } @@ -292,7 +297,6 @@ class BottomSheetView( if (!fullHeight) { this.startObservingContentHeight() } - } fun updateLayout() { @@ -365,17 +369,18 @@ class BottomSheetView( val innerViewGroup = this.innerView as? ViewGroup ?: return - val listener = OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, oldBottom -> - val newHeight = bottom - top - val oldHeight = oldBottom - oldTop - if (newHeight != oldHeight) { - val contentHeight = getContentHeight() - if (contentHeight != lastObservedContentHeight && contentHeight > 0 && (isOpen || isOpening) && !isClosing) { - lastObservedContentHeight = contentHeight - updateLayout() + val listener = + OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, oldBottom -> + val newHeight = bottom - top + val oldHeight = oldBottom - oldTop + if (newHeight != oldHeight) { + val contentHeight = getContentHeight() + if (contentHeight != lastObservedContentHeight && contentHeight > 0 && (isOpen || isOpening) && !isClosing) { + lastObservedContentHeight = contentHeight + updateLayout() + } } } - } val children = mutableListOf() for (i in 0 until innerViewGroup.childCount) { diff --git a/package.json b/package.json index 0716bd085b..b8bd26e241 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@bsky.app/expo-image-crop-tool": "^0.5.1", "@bsky.app/expo-scroll-edge-effect": "^0.1.4", "@bsky.app/expo-translate-text": "^0.2.9", + "@bsky.app/peek-menu": "^0.2.4", "@bsky.app/react-native-mmkv": "2.12.5", "@bsky.app/sift": "^0.3.8", "@bsky.app/tapper": "^0.5.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d9210cc2d..b3d0003107 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,6 +268,9 @@ importers: '@bsky.app/expo-translate-text': specifier: ^0.2.9 version: 0.2.9(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@bsky.app/peek-menu': + specifier: ^0.2.4 + version: 0.2.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@bsky.app/react-native-mmkv': specifier: 2.12.5 version: 2.12.5(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -1649,6 +1652,13 @@ packages: react: '*' react-native: '*' + '@bsky.app/peek-menu@0.2.4': + resolution: {integrity: sha512-3E5FwgCXMU6baye3NWoBKih3SCh6s8AgAtxN523YBHZGC5TxpjGaBsjFi765y31nKmTll3QH7x4wtDgYqVvCQg==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + '@bsky.app/react-native-mmkv@2.12.5': resolution: {integrity: sha512-3vUz1nQY1DiKIPAWRkpp5ZGxH5f2G6Ui0UuQuEYjYv81xx1qFcSzS9KQ2sHcOKYdkOM9amWV2Q8TQCxt1lrAHg==} peerDependencies: @@ -10461,6 +10471,12 @@ snapshots: react: 19.1.0 react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + '@bsky.app/peek-menu@0.2.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + expo: 54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + '@bsky.app/react-native-mmkv@2.12.5(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 diff --git a/src/App.native.tsx b/src/App.native.tsx index 2654c6ed83..22dd39f9d7 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -47,18 +47,18 @@ import { import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' +import {Provider as LandingProvider} from '#/state/shell/landing' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as OnboardingProvider} from '#/state/shell/onboarding' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' -import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {TestCtrls} from '#/view/com/testing/TestCtrls' import {Shell} from '#/view/shell' import {atoms as a, ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {Provider as ContextMenuProvider} from '#/components/ContextMenu' -import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' +import {useLandingEntry} from '#/components/hooks/useLandingEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as LightboxStateProvider} from '#/components/Lightbox/state' import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay' @@ -113,7 +113,7 @@ function InnerApp() { const {resumeSession} = useSessionApi() const theme = useColorModeTheme() const {t: l} = useLingui() - const hasCheckedReferrer = useStarterPackEntry() + const hasCheckedLanding = useLandingEntry() // init useEffect(() => { @@ -146,7 +146,7 @@ function InnerApp() { - + - + - + diff --git a/src/App.web.tsx b/src/App.web.tsx index 5c22b3c921..6faf6f2218 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -40,17 +40,17 @@ import { import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' +import {Provider as LandingProvider} from '#/state/shell/landing' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as OnboardingProvider} from '#/state/shell/onboarding' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' -import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Shell} from '#/view/shell/index' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {Provider as ContextMenuProvider} from '#/components/ContextMenu' -import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' +import {useLandingEntry} from '#/components/hooks/useLandingEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as LightboxStateProvider} from '#/components/Lightbox/state' import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay' @@ -92,7 +92,7 @@ function InnerApp() { const {resumeSession} = useSessionApi() const theme = useColorModeTheme() const {t: l} = useLingui() - const hasCheckedReferrer = useStarterPackEntry() + const hasCheckedLanding = useLandingEntry() // init useEffect(() => { @@ -125,7 +125,7 @@ function InnerApp() { - + - + - + diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 6284aff34b..efd1125295 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -44,6 +44,7 @@ import { type State, } from '#/lib/routes/types' import {bskyTitle} from '#/lib/strings/headings' +import {CHAT_INVITE_CODE_REGEX} from '#/lib/strings/url-helpers' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' @@ -81,6 +82,7 @@ import {MessagesScreen} from '#/screens/Messages/ChatList' import {MessagesConversationScreen} from '#/screens/Messages/Conversation' import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings' import {MessagesInboxScreen} from '#/screens/Messages/Inbox' +import {MessagesJoinRequestsScreen} from '#/screens/Messages/JoinRequests' import {MessagesSettingsScreen} from '#/screens/Messages/Settings' import {ModerationScreen} from '#/screens/Moderation' import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings' @@ -487,6 +489,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => MessagesConversationSettingsScreen} options={{title: title(msg`Group chat settings`), requireAuth: true}} /> + MessagesJoinRequestsScreen} + options={{title: title(msg`Requests to join`), requireAuth: true}} + /> MessagesSettingsScreen} @@ -797,6 +804,19 @@ const LINKING = { return buildStateObject('Flat', 'Home', params) } + // Chat invite URLs (`/c/:code`) are handled by `useIntentHandler`, which + // opens the GroupChatJoinDialog (or the logged-out join flow). Route the + // path to Home so the dialog overlays Home instead of NotFound. On native, + // react-navigation strips the `bluesky://` prefix and passes the path + // without a leading slash, so normalize before matching. + const normalizedPath = path.startsWith('/') ? path : `/${path}` + if (CHAT_INVITE_CODE_REGEX.test(normalizedPath.split('?')[0])) { + if (IS_NATIVE) { + return buildStateObject('HomeTab', 'Home', params) + } + return buildStateObject('Flat', 'Home', params) + } + if (IS_NATIVE) { if (name === 'Search') { return buildStateObject('SearchTab', 'Search', params) diff --git a/src/components/AvatarBubbles.tsx b/src/components/AvatarBubbles.tsx index 880a5284f0..c49dada16f 100644 --- a/src/components/AvatarBubbles.tsx +++ b/src/components/AvatarBubbles.tsx @@ -28,23 +28,29 @@ type Layout = { border?: boolean } -type Props = { - animate?: boolean - profiles: bsky.profile.AnyProfileView[] - size?: number - moderationOpts?: ModerationOpts -} - export function AvatarBubbles({ animate = false, profiles: allProfiles, + self = false, size = 120, moderationOpts, -}: Props) { +}: { + animate?: boolean + profiles: bsky.profile.AnyProfileView[] + /** + * By default, when there are more than 2 profiles, the current user is + * filtered out (so you don't see yourself among your own group's members). + * Set this to `true` for cases where every passed profile should appear, + * e.g. an invite preview where the owner is meaningful regardless of viewer. + */ + self?: boolean + size?: number + moderationOpts?: ModerationOpts +}) { const {currentAccount} = useSession() const profiles = - allProfiles.length > 2 - ? allProfiles.filter(p => p.did !== currentAccount?.did) + !self && allProfiles.length > 2 + ? allProfiles.filter(p => p?.did != null && p.did !== currentAccount?.did) : allProfiles const moderations = useMemo(() => { if (!moderationOpts) return [] diff --git a/src/components/Button.tsx b/src/components/Button.tsx index aa705b1897..5f74595029 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -757,11 +757,11 @@ export function useSharedButtonTextStyles() { } if (size === 'large') { - baseStyles.push(a.text_md, a.leading_snug, a.font_medium) + baseStyles.push(a.text_md, a.font_medium) } else if (size === 'small') { - baseStyles.push(a.text_sm, a.leading_snug, a.font_medium) + baseStyles.push(a.text_sm, a.font_medium) } else if (size === 'tiny') { - baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold) + baseStyles.push(a.text_xs, a.font_semi_bold) } return flatten(baseStyles) diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index 8d3d39a749..51bf83309f 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -499,6 +499,7 @@ function TriggerClone({ accessibilityLabel={label} accessibilityHint={_(msg`The subject of the context menu`)} accessibilityIgnoresInvertColors={false} + cachePolicy="none" /> ) diff --git a/src/components/Lightbox/Lightbox.web.tsx b/src/components/Lightbox/Lightbox.web.tsx index 641b2c3402..8883bfb5e5 100644 --- a/src/components/Lightbox/Lightbox.web.tsx +++ b/src/components/Lightbox/Lightbox.web.tsx @@ -230,7 +230,11 @@ function LightboxGallery({ style={[ a.px_4xl, a.py_2xl, - {backgroundColor: 'rgba(0, 0, 0, 0.45)'}, + { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + // @ts-expect-error web only + backdropFilter: 'blur(16px)', + }, delayedFadeInAnim, ]}> - { - isMomentumScrolling.current = true - }} - onMomentumScrollEnd={() => { - isMomentumScrolling.current = false - }} - contentContainerStyle={[a.px_md, a.py_sm]}> - { - if (isMomentumScrolling.current) return - LayoutAnimation.configureNext({ - duration: 450, - update: {type: 'spring', springDamping: 1}, - }) - onToggleAltExpanded() - }}> - - {altText} - - - + + { + isMomentumScrolling.current = true + }} + onMomentumScrollEnd={() => { + isMomentumScrolling.current = false + }} + contentContainerStyle={[a.px_md, a.py_sm]}> + + {/* + * The press handlers must live on the Text itself, not on a + * wrapping Pressable. Text selection is driven by the platform's + * native text view long-press (RN Text on Android, UITextView on + * iOS). A parent touchable consumes that long-press before the + * selectable Text can begin a selection - on Android this prevents + * selection entirely. Keeping onPress/onLongPress on the Text lets + * the same native node own both the tap-to-expand and the + * long-press-to-select. The empty onLongPress is intentional: it + * reserves the long-press for the OS selection gesture instead of + * firing the expand toggle. RN exposes no API to arbitrate tap vs + * native selection on a single node, so this is the supported + * workaround, and it behaves consistently on both platforms. + */} + { + if (isMomentumScrolling.current) return + LayoutAnimation.configureNext({ + duration: 450, + update: {type: 'spring', springDamping: 1}, + }) + onToggleAltExpanded() + }} + onLongPress={() => {}}> + {altText} + + + + ) @@ -74,7 +98,6 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) { const styles = StyleSheet.create({ altWrap: { - backgroundColor: 'rgba(0, 0, 0, 0.45)', borderRadius: 12, overflow: 'hidden', }, diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx index c0bb99c89e..19373cbe1a 100644 --- a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx +++ b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx @@ -246,6 +246,7 @@ const ImageItem = ({ } } cachePolicy="memory" + useAppleWebpCodec /> diff --git a/src/components/Lightbox/pager/ImagePager.tsx b/src/components/Lightbox/pager/ImagePager.tsx index 1c8836a0fd..7b8815f276 100644 --- a/src/components/Lightbox/pager/ImagePager.tsx +++ b/src/components/Lightbox/pager/ImagePager.tsx @@ -32,6 +32,7 @@ import Animated, { withSpring, type WithSpringConfig, } from 'react-native-reanimated' +import {Image} from 'expo-image' import * as ScreenOrientation from 'expo-screen-orientation' import {type Dimensions} from '#/lib/media/types' @@ -136,6 +137,9 @@ export default function ImageViewRoot({ 'worklet' thumbRects.set({}) })() + requestIdleCallback(() => { + void Image.clearMemoryCache() + }) }, [thumbRects]) useAnimatedReaction( diff --git a/src/components/Link.tsx b/src/components/Link.tsx index fcb2e8a87f..8795c8e899 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -12,6 +12,7 @@ import { } from '@react-navigation/native' import {BSKY_DOWNLOAD_URL} from '#/lib/constants' +import {useGroupChatJoinIntent} from '#/lib/hooks/useIntentHandler' import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' import {useOpenLink} from '#/lib/hooks/useOpenLink' import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types' @@ -19,6 +20,7 @@ import {shareUrl} from '#/lib/sharing' import { convertBskyAppUrlIfNeeded, createProxiedUrl, + getChatInviteCodeFromUrl, isBskyDownloadUrl, isExternalUrl, linkRequiresWarning, @@ -130,6 +132,7 @@ export function useLink({ const {closeModal} = useModalControls() const {linkWarningDialogControl} = useGlobalDialogsControlContext() const openLink = useOpenLink() + const groupChatJoinIntent = useGroupChatJoinIntent() const onPress = useCallback( (e: GestureResponderEvent) => { @@ -148,6 +151,12 @@ export function useLink({ e.preventDefault() } + const chatInviteCode = getChatInviteCodeFromUrl(href) + if (chatInviteCode) { + groupChatJoinIntent(chatInviteCode, href) + return + } + if (requiresWarning) { linkWarningDialogControl.open({ displayText, @@ -228,6 +237,7 @@ export function useLink({ overridePresentation, shouldProxy, linkWarningDialogControl, + groupChatJoinIntent, ], ) diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index 530c85ae66..b36705d840 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -1,11 +1,16 @@ import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' import {Image} from 'expo-image' -import {type AppBskyFeedDefs} from '@atproto/api' -import {Trans} from '@lingui/react/macro' +import {type AppBskyEmbedImages, type AppBskyFeedDefs} from '@atproto/api' +import {Trans, useLingui} from '@lingui/react/macro' +import {shareImageModal} from '#/lib/media/manip' +import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' import {isGifEmbed} from '#/lib/strings/embed-player' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' +import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import * as PeekMenu from '#/components/PeekMenu' import {Text} from '#/components/Typography' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import * as bsky from '#/types/bsky' @@ -16,9 +21,11 @@ import * as bsky from '#/types/bsky' export function Embed({ embed, style, + peekable = false, }: { embed: AppBskyFeedDefs.PostView['embed'] style?: StyleProp + peekable?: boolean }) { const e = bsky.post.parseEmbed(embed) @@ -27,13 +34,17 @@ export function Embed({ if (e.type === 'images') { return ( - {e.view.images.map(image => ( - - ))} + {e.view.images.map(image => + peekable ? ( + + ) : ( + + ), + )} ) } else if (e.type === 'link') { @@ -118,6 +129,7 @@ export function ImageItem({ contentFit="cover" accessible={true} accessibilityIgnoresInvertColors + useAppleWebpCodec /> {children} @@ -156,6 +168,45 @@ export function VideoItem({ ) } +function PeekableImageItem({image}: {image: AppBskyEmbedImages.ViewImage}) { + const {t: l} = useLingui() + const saveImage = useSaveImageToMediaLibrary() + + const aspect = + image.aspectRatio && image.aspectRatio.height > 0 + ? image.aspectRatio.width / image.aspectRatio.height + : undefined + + return ( + + 0 ? aspect : 1, + }} + borderRadius={tokens.borderRadius.xs}> + + + + void saveImage(image.fullsize)}> + + {l`Save image`} + + void shareImageModal({uri: image.fullsize})}> + + {l`Share`} + + + + ) +} + const styles = StyleSheet.create({ altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', diff --git a/src/components/PeekMenu.tsx b/src/components/PeekMenu.tsx new file mode 100644 index 0000000000..53732ec3e8 --- /dev/null +++ b/src/components/PeekMenu.tsx @@ -0,0 +1 @@ +export * from '@bsky.app/peek-menu' diff --git a/src/components/Post/Embed/ChatInviteEmbed.tsx b/src/components/Post/Embed/ChatInviteEmbed.tsx new file mode 100644 index 0000000000..744fcfb7fa --- /dev/null +++ b/src/components/Post/Embed/ChatInviteEmbed.tsx @@ -0,0 +1,41 @@ +import {type StyleProp, type ViewStyle} from 'react-native' +import {type AppBskyEmbedExternal} from '@atproto/api' + +import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links' +import {useSession} from '#/state/session' +import {atoms as a} from '#/alf' +import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' +import {JoinRequestEmbed} from '#/components/Post/Embed/JoinRequestEmbed' + +export function ChatInviteEmbed({ + code, + link, + onOpen, + style, +}: { + code: string + link: AppBskyEmbedExternal.ViewExternal + onOpen?: () => void + style?: StyleProp +}) { + const {hasSession} = useSession() + const {data, error, isPending} = useJoinLinkPreviewsQuery({ + codes: [code], + hasSession, + }) + + const preview = data?.joinLinkPreviews[0] + + if (error) { + return + } + + return ( + + ) +} diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index 43bfbb4747..fe0be2bd25 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -108,6 +108,7 @@ export const ExternalEmbed = ({ source={{uri: imageUri}} accessibilityIgnoresInvertColors loading="lazy" + useAppleWebpCodec /> ) : undefined} diff --git a/src/components/Post/Embed/ImageContextMenu.tsx b/src/components/Post/Embed/ImageContextMenu.tsx new file mode 100644 index 0000000000..3aadb56901 --- /dev/null +++ b/src/components/Post/Embed/ImageContextMenu.tsx @@ -0,0 +1,79 @@ +import {type ReactNode} from 'react' +import {type StyleProp, type ViewStyle} from 'react-native' +import {useLingui} from '@lingui/react/macro' + +import {shareImageModal} from '#/lib/media/manip' +import {useSaveImageToMediaLibrary} from '#/lib/media/save-image' +import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' +import * as PeekMenu from '#/components/PeekMenu' +import {IS_IOS} from '#/env' + +/** + * Wraps an image embed with the iOS peek-and-menu interaction. On non-iOS + * platforms this renders children unchanged. + * + * The aspect ratio is consumed by the native side to size the preview + * viewController correctly — which is what makes the lift animation clean + * for portrait/panorama images. + */ +export function ImageContextMenu({ + fullsizeUri, + thumbUri, + aspectRatio, + borderRadius, + onPreviewPress, + style, + children, +}: { + fullsizeUri: string + /** Thumbnail URL. Used as an instant placeholder in the native preview + * while the fullsize loads, so there's no black flash on first peek. */ + thumbUri?: string + /** width / height; defaults to 1 if missing. */ + aspectRatio: number | undefined + borderRadius?: number + onPreviewPress?: () => void + style?: StyleProp + children: ReactNode +}) { + const {t: l} = useLingui() + const saveImage = useSaveImageToMediaLibrary() + + if (!IS_IOS) { + return children + } + + const handleSave = () => { + void saveImage(fullsizeUri) + } + const handleShare = () => { + void shareImageModal({uri: fullsizeUri}) + } + + return ( + + 0 ? aspectRatio : 1, + }} + borderRadius={borderRadius} + onPreviewPress={onPreviewPress}> + {children} + + + + + {l`Save image`} + + + + {l`Share`} + + + + ) +} diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index d779d04876..2c6bb6c5de 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -1,3 +1,4 @@ +import {useRef} from 'react' import {InteractionManager, View} from 'react-native' import {type AnimatedRef} from 'react-native-reanimated' import {Image} from 'expo-image' @@ -8,6 +9,7 @@ import {Gallery} from '#/components/images/Gallery' import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid' import {useLightboxControls} from '#/components/Lightbox/state' import {type Dimensions} from '#/components/Lightbox/types' +import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {useAnalytics} from '#/analytics' import {type EmbedType} from '#/types/bsky/post' @@ -24,6 +26,11 @@ export function ImageEmbed({ const {images} = embed.view const galleryEnabled = ax.features.enabled(ax.features.PostGalleryEmbedEnable) + // Captured from AutoSizedImage so the peek-commit handler can reuse the same + // ref + dims that a tap would — keeps the lightbox's return animation intact. + const singleContainerRef = useRef | null>(null) + const singleDimsRef = useRef(null) + if (images.length > 0) { const items = images.map(img => ({ uri: img.fullsize, @@ -59,24 +66,45 @@ export function ImageEmbed({ if (images.length === 1) { const image = images[0] + const aspect = + image.aspectRatio && image.aspectRatio.height > 0 + ? image.aspectRatio.width / image.aspectRatio.height + : undefined + const openFromSingle = () => { + if (singleContainerRef.current) { + onPress(0, [singleContainerRef.current], [singleDimsRef.current]) + } + } return ( - onPress(0, [containerRef], [dims])} - onPressIn={() => onPressIn(0)} - hideBadge={ - rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - } - /> + + { + singleContainerRef.current = ref + }} + onDimsChange={dims => { + singleDimsRef.current = dims + }} + onPress={(containerRef, dims) => + onPress(0, [containerRef], [dims]) + } + onPressIn={() => onPressIn(0)} + hideBadge={rest.isWithinQuote} + /> + ) } @@ -89,6 +117,7 @@ export function ImageEmbed({ onPress={onPress} onPressIn={onPressIn} viewContext={rest.viewContext} + isWithinQuote={rest.isWithinQuote} /> ) diff --git a/src/components/Post/Embed/JoinRequestEmbed.tsx b/src/components/Post/Embed/JoinRequestEmbed.tsx new file mode 100644 index 0000000000..77a0223bec --- /dev/null +++ b/src/components/Post/Embed/JoinRequestEmbed.tsx @@ -0,0 +1,271 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' +import {type ChatBskyGroupDefs} from '@atproto/api' +import {Plural, Trans, useLingui} from '@lingui/react/macro' +import {useNavigation} from '@react-navigation/native' + +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import {type NavigationProp} from '#/lib/routes/types' +import {sanitizeHandle} from '#/lib/strings/handles' +import {atoms as a, useTheme} from '#/alf' +import {AvatarBubbles} from '#/components/AvatarBubbles' +import { + Button, + type ButtonColor, + ButtonIcon, + ButtonText, +} from '#/components/Button' +import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' +import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' +import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' +import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {useIntentDialogs} from '#/components/intents/IntentDialogs' +import {Loader} from '#/components/Loader' +import {ProfileBadges} from '#/components/ProfileBadges' +import {Text} from '#/components/Typography' + +const JOIN_REQUEST_EMBED_HEIGHT = 152 + +export function JoinRequestEmbed({ + loading = false, + preview, + style, + onOpen, +}: { + loading?: boolean + preview?: ChatBskyGroupDefs.JoinLinkPreviewView + style?: StyleProp + onOpen?: () => void +}) { + const t = useTheme() + + if (loading) { + return ( + + + + ) + } + + if (!preview) { + return ( + + + + Chat invite link no longer available + + + ) + } + + return ( + + ) +} + +function JoinRequestEmbedInner({ + preview, + style, + onOpen, +}: { + preview: ChatBskyGroupDefs.JoinLinkPreviewView + style?: StyleProp + onOpen?: () => void +}) { + const t = useTheme() + const {t: l} = useLingui() + const navigation = useNavigation() + + const {groupChatJoinDialogControl, setGroupChatJoinState} = useIntentDialogs() + + const ownerDisplayName = createSanitizedDisplayName(preview.owner) + const ownerHandle = sanitizeHandle(preview.owner.handle, '@') + + const avatarProfiles = preview.convo?.members ?? [preview.owner] + + const convoId = preview.convo?.id + const isFollowing = preview.owner.viewer?.following ?? false + const hasRequested = !convoId && preview.viewer?.requestedAt != null + + let canJoin = true + let ButtonIconImage = JoinIcon + let buttonText = preview.requireApproval ? l`Request to join` : l`Join` + let buttonColor: ButtonColor = 'primary' + if (preview.enabledStatus !== 'enabled') { + canJoin = false + ButtonIconImage = WarningIcon + buttonText = l`Chat invite link no longer available` + buttonColor = 'secondary' + } else if (preview.memberCount >= preview.memberLimit) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`This chat is full` + buttonColor = 'secondary' + } else if (preview.joinRule === 'followedByOwner' && !isFollowing) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`Only people the chat owner follows can join` + buttonColor = 'secondary' + } else if (hasRequested) { + ButtonIconImage = CheckIcon + buttonText = l`Requested` + buttonColor = 'secondary' + } + + return ( + + + + + + {preview.name} + + + + Group chat + + + + {preview.memberCount}/{preview.memberLimit}{' '} + + + + + + + + By{' '} + + {ownerDisplayName} + + + + + + {ownerHandle} + + + + + {convoId ? ( + + ) : ( + + )} + + ) +} diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 70fce8ad77..5f7fc1f5e7 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -166,6 +166,7 @@ export const StandardSiteEmbed = ({ source={{uri: imageUri}} accessibilityIgnoresInvertColors loading="lazy" + useAppleWebpCodec /> ) : undefined} diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index 054558ad83..ab6f5dc439 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -12,6 +12,7 @@ import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' +import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {unstableCacheProfileView} from '#/state/queries/profile' import {useSession} from '#/state/session' @@ -33,6 +34,7 @@ import { type EmbedType, parseEmbed, } from '#/types/bsky/post' +import {ChatInviteEmbed} from './ChatInviteEmbed' import {ExternalEmbed} from './ExternalEmbed' import {ModeratedFeedEmbed} from './FeedEmbed' import {ImageEmbed} from './ImageEmbed' @@ -110,6 +112,21 @@ function MediaEmbed({ ) } + const chatInviteCode = getChatInviteCodeFromUrl(embed.view.external.uri) + if (chatInviteCode) { + return ( + + + + ) + } return ( void + onClose?: () => void confirmButtonColor?: ButtonColor showCancel?: boolean }>) { return ( - + {title} {description && {description}} diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 322fb9ebd9..25af851723 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -66,7 +66,7 @@ export function RichText({ } }, [value]) - const plainStyles = [a.leading_snug, style] + const plainStyles = style const interactiveStyles = [plainStyles, interactiveStyle] const {text, facets} = richText diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index ac5bc4889a..3e11cf051e 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -8,8 +8,10 @@ import { type UninheritableButtonProps, } from '#/components/Button' import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' +import { + CircleInfo_Stroke2_Corner0_Rounded as CircleInfo, + CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon, +} from '#/components/icons/CircleInfo' import {type Props as SVGIconProps} from '#/components/icons/common' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {dismiss} from '#/components/Toast/sonner' diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index 04b5e5499d..a65889f67e 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -31,6 +31,7 @@ export function Text({ [ a.text_sm, t.atoms.text, + a.leading_snug, web(numberOfLines === 1 && numberOfLinesClippingFix), style, ], diff --git a/src/components/contacts/FindContactsBannerNUX.tsx b/src/components/contacts/FindContactsBannerNUX.tsx index 2997e00d4e..417379fa2b 100644 --- a/src/components/contacts/FindContactsBannerNUX.tsx +++ b/src/components/contacts/FindContactsBannerNUX.tsx @@ -60,6 +60,7 @@ export function FindContactsBannerNUX() { a.self_end, a.mt_sm, ]} + useAppleWebpCodec /> ) diff --git a/src/components/dialogs/nuxs/ActivitySubscriptions.tsx b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx index ed4306459c..318b130414 100644 --- a/src/components/dialogs/nuxs/ActivitySubscriptions.tsx +++ b/src/components/dialogs/nuxs/ActivitySubscriptions.tsx @@ -113,6 +113,7 @@ export function ActivitySubscriptionsNUX() { alt={_( msg`A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature.`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx index f7e4bf04c6..153130f8ec 100644 --- a/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx +++ b/src/components/dialogs/nuxs/BookmarksAnnouncement.tsx @@ -124,6 +124,7 @@ export function BookmarksAnnouncement() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/DraftsAnnouncement.tsx b/src/components/dialogs/nuxs/DraftsAnnouncement.tsx index 79488fe4d9..37b93bda70 100644 --- a/src/components/dialogs/nuxs/DraftsAnnouncement.tsx +++ b/src/components/dialogs/nuxs/DraftsAnnouncement.tsx @@ -101,6 +101,7 @@ export function DraftsAnnouncement() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx index 8fd7d2d2c5..70f29ef157 100644 --- a/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx +++ b/src/components/dialogs/nuxs/FindContactsAnnouncement.tsx @@ -78,6 +78,7 @@ export function FindContactsAnnouncement() { alt={_( msg`An illustration depicting user avatars flowing from a contact book into the Bluesky app`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx index 85bc29a48f..cbe2555aac 100644 --- a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx +++ b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx @@ -85,6 +85,7 @@ export function InitialVerificationAnnouncement() { alt={_( msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, )} + useAppleWebpCodec /> @@ -119,6 +120,7 @@ export function InitialVerificationAnnouncement() { alt={_( msg`An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name.`, )} + useAppleWebpCodec /> diff --git a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx index 49c43a1e41..e389e11175 100644 --- a/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx +++ b/src/components/dialogs/nuxs/LiveNowBetaDialog.tsx @@ -150,6 +150,7 @@ export function LiveNowBetaDialog() { 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', }), )} + useAppleWebpCodec /> diff --git a/src/components/dms/AddMembersFlow.tsx b/src/components/dms/AddMembersFlow.tsx index 1601594dab..749e6976ed 100644 --- a/src/components/dms/AddMembersFlow.tsx +++ b/src/components/dms/AddMembersFlow.tsx @@ -219,7 +219,7 @@ export function AddMembersFlow({ } } - if (searchText === '') { + if (searchText === '' && _items.length > 0) { _items.unshift({ type: 'label', key: 'suggested', diff --git a/src/components/dms/AfterReportDialog.tsx b/src/components/dms/AfterReportDialog.tsx index 3df194fc8b..060ea1fcfb 100644 --- a/src/components/dms/AfterReportDialog.tsx +++ b/src/components/dms/AfterReportDialog.tsx @@ -33,14 +33,19 @@ export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({ control, params, currentScreen, + onClose, }: { control: Dialog.DialogControlProps params: ReportDialogParams currentScreen: 'list' | 'conversation' + onClose?: () => void }): React.ReactNode { const {t: l} = useLingui() return ( - + 0 && (chatState === ChatState.NEW_CHAT || chatState === ChatState.NEW_GROUP_CHAT) ) { diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx index d3376d797b..bc77c5bd79 100644 --- a/src/components/dms/MessageContextMenu.tsx +++ b/src/components/dms/MessageContextMenu.tsx @@ -1,5 +1,5 @@ import {memo, useCallback} from 'react' -import {LayoutAnimation, Platform} from 'react-native' +import {Platform} from 'react-native' import * as Clipboard from 'expo-clipboard' import { type ChatBskyConvoDefs, @@ -7,26 +7,21 @@ import { RichText, } from '@atproto/api' import {useLingui} from '@lingui/react/macro' -import {useQueryClient} from '@tanstack/react-query' import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useConvoActive} from '#/state/messages/convo' import {useLanguagePrefs} from '#/state/preferences' -import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {useSession} from '#/state/session' import {atoms as a} from '#/alf' import * as ContextMenu from '#/components/ContextMenu' import {type TriggerProps} from '#/components/ContextMenu/types' -import {AfterReportDialog} from '#/components/dms/AfterReportDialog' +import {useMessageDialogs} from '#/components/dms/MessageOverlays' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag' import {Language_Stroke2_Corner2_Rounded as LanguageIcon} from '#/components/icons/Language' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' -import {ReportDialog} from '#/components/moderation/ReportDialog' -import * as Prompt from '#/components/Prompt' -import {usePromptControl} from '#/components/Prompt' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' @@ -48,11 +43,8 @@ export let MessageContextMenu = ({ const {t: l, i18n} = useLingui() const ax = useAnalytics() const {currentAccount} = useSession() - const queryClient = useQueryClient() const convo = useConvoActive() - const deleteControl = usePromptControl() - const reportControl = usePromptControl() - const blockOrDeleteControl = usePromptControl() + const {openDeleteMessage, openReportMessage} = useMessageDialogs() const langPrefs = useLanguagePrefs() const translate = useGoogleTranslate() @@ -93,14 +85,6 @@ export let MessageContextMenu = ({ }) }, [ax, langPrefs.primaryLanguage, message.text, translate]) - const onDelete = useCallback(() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - convo - .deleteMessage(message.id) - .then(() => Toast.show(l({message: 'Message deleted', context: 'toast'}))) - .catch(() => Toast.show(l`Failed to delete message`)) - }, [l, convo, message.id]) - const onEmojiSelect = useCallback( (emoji: string) => { if ( @@ -128,104 +112,72 @@ export let MessageContextMenu = ({ const sender = senderProfile return ( - <> - - {IS_NATIVE && reactionsAvailable && ( - - - - )} - - - {children} - - - + {IS_NATIVE && reactionsAvailable && ( + - {message.text.length > 0 && ( - <> - - - {l`Translate`} - - - - - {l`Copy message text`} - - - - )} + + + )} + + + {children} + + + + {message.text.length > 0 && ( + <> + + + {l`Translate`} + + + + + {l`Copy message text`} + + + + )} + openDeleteMessage(message)}> + + {l`Delete for me`} + + {!isFromSelf && ( deleteControl.open()}> - - {l`Delete for me`} + testID="messageDropdownReportBtn" + label={l`Report message`} + onPress={() => openReportMessage(message, senderProfile)}> + + {l`Report`} - {!isFromSelf && ( - reportControl.open()}> - - {l`Report`} - - )} - - - { - if (sender) { - unstableCacheProfileView(queryClient, sender) - } - blockOrDeleteControl.open() - }} - /> - - - + )} + + ) } MessageContextMenu = memo(MessageContextMenu) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 6f1819ada2..ac6acb5891 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -40,8 +40,8 @@ import {useSession} from '#/state/session' import {atoms as a, native, platform, useTheme} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' import {Button} from '#/components/Button' -import {useDialogControl} from '#/components/Dialog' import {ActionsWrapper} from '#/components/dms/ActionsWrapper' +import {useMessageDialogs} from '#/components/dms/MessageOverlays' import {InlineLinkText, Link} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' @@ -49,7 +49,7 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' -import {ReactionsDialog} from './ReactionsDialog' +import {groupReactions} from './ReactionsDialog' import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util' const AVATAR_SIZE = 28 @@ -118,7 +118,7 @@ let MessageItem = ({ const {message} = item const profile = useMaybeProfileShadow(relatedProfiles.get(message.sender.did)) - const reactionsControl = useDialogControl() + const {openReactions} = useMessageDialogs() const isPending = item.type === 'pending-message' @@ -243,34 +243,10 @@ let MessageItem = ({ ) - const groupedReactions = useMemo(() => { - const reactions = message.reactions ?? [] - const grouped = new Map< - string, - { - key: string - value: string - senders: ChatBskyConvoDefs.ReactionViewSender[] - count: number - } - >() - for (const reaction of reactions) { - if (!reaction) continue - const existing = grouped.get(reaction.value) - if (existing) { - existing.senders.push(reaction.sender) - existing.count++ - } else { - grouped.set(reaction.value, { - key: reaction.value, - value: reaction.value, - senders: [reaction.sender], - count: 1, - }) - } - } - return Array.from(grouped.values()) - }, [message.reactions]) + const groupedReactions = useMemo( + () => groupReactions(message.reactions), + [message.reactions], + ) const reactions = useMemo(() => message.reactions ?? [], [message.reactions]) @@ -336,7 +312,7 @@ let MessageItem = ({ transform: [{translateY: -8}], }, ]} - onPress={isGroupChat ? reactionsControl.open : undefined}> + onPress={isGroupChat ? () => openReactions(message) : undefined}> {groupedReactions.map(group => ( ) : null} - ) diff --git a/src/components/dms/MessageOverlays.tsx b/src/components/dms/MessageOverlays.tsx new file mode 100644 index 0000000000..41228f2899 --- /dev/null +++ b/src/components/dms/MessageOverlays.tsx @@ -0,0 +1,175 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' +import {LayoutAnimation} from 'react-native' +import {type ChatBskyConvoDefs} from '@atproto/api' +import {useLingui} from '@lingui/react/macro' +import {useQueryClient} from '@tanstack/react-query' + +import {useConvoActive} from '#/state/messages/convo' +import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' +import {useDialogControl} from '#/components/Dialog' +import {AfterReportDialog} from '#/components/dms/AfterReportDialog' +import {ReactionsDialog} from '#/components/dms/ReactionsDialog' +import {ReportDialog} from '#/components/moderation/ReportDialog' +import * as Prompt from '#/components/Prompt' +import {usePromptControl} from '#/components/Prompt' +import * as Toast from '#/components/Toast' +import type * as bsky from '#/types/bsky' + +type MessageDialogsContextType = { + openDeleteMessage: (message: ChatBskyConvoDefs.MessageView) => void + openReportMessage: ( + message: ChatBskyConvoDefs.MessageView, + senderProfile: bsky.profile.AnyProfileView | undefined, + ) => void + openReactions: (message: ChatBskyConvoDefs.MessageView) => void +} + +const Context = createContext(null) + +export function useMessageDialogs() { + const ctx = useContext(Context) + if (!ctx) { + throw new Error('useMessageDialogs must be used within a MessageOverlays') + } + return ctx +} + +export function MessageOverlays({children}: {children: React.ReactNode}) { + const {t: l} = useLingui() + const queryClient = useQueryClient() + const convo = useConvoActive() + + const deleteControl = usePromptControl() + const reportControl = usePromptControl() + const afterReportControl = usePromptControl() + const reactionsControl = useDialogControl() + + const [deleteTarget, setDeleteTarget] = + useState(null) + const [reportTarget, setReportTarget] = useState<{ + message: ChatBskyConvoDefs.MessageView + senderProfile: bsky.profile.AnyProfileView | undefined + } | null>(null) + const [afterReportTarget, setAfterReportTarget] = + useState(null) + const [reactionsTarget, setReactionsTarget] = + useState(null) + + const openDeleteMessage = useCallback( + (message: ChatBskyConvoDefs.MessageView) => { + setDeleteTarget(message) + deleteControl.open() + }, + [deleteControl], + ) + + const openReportMessage = useCallback( + ( + message: ChatBskyConvoDefs.MessageView, + senderProfile: bsky.profile.AnyProfileView | undefined, + ) => { + setReportTarget({message, senderProfile}) + reportControl.open() + }, + [reportControl], + ) + + const openReactions = useCallback( + (message: ChatBskyConvoDefs.MessageView) => { + setReactionsTarget(message) + }, + [], + ) + + // These dialogs are conditionally mounted, so we can't open them in the same + // tick that we set their targets - the control refs aren't attached yet. Open + // in an effect after the dialog has mounted. + useEffect(() => { + if (reactionsTarget) { + reactionsControl.open() + } + }, [reactionsTarget, reactionsControl]) + + useEffect(() => { + if (afterReportTarget) { + afterReportControl.open() + } + }, [afterReportTarget, afterReportControl]) + + const onConfirmDelete = useCallback(() => { + if (!deleteTarget) return + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) + convo + .deleteMessage(deleteTarget.id) + .then(() => Toast.show(l({message: 'Message deleted', context: 'toast'}))) + .catch(() => Toast.show(l`Failed to delete message`)) + }, [l, convo, deleteTarget]) + + const onAfterReportSubmit = useCallback(() => { + if (!reportTarget) return + if (reportTarget.senderProfile) { + unstableCacheProfileView(queryClient, reportTarget.senderProfile) + } + setAfterReportTarget(reportTarget.message) + }, [queryClient, reportTarget]) + + const ctx = useMemo( + () => ({openDeleteMessage, openReportMessage, openReactions}), + [openDeleteMessage, openReportMessage, openReactions], + ) + + const reportSubject = reportTarget + ? ({ + view: 'message', + convoId: convo.convo.view.id, + message: reportTarget.message, + } as const) + : undefined + + return ( + + {children} + setReportTarget(null)} + /> + {afterReportTarget && ( + setAfterReportTarget(null)} + /> + )} + {reactionsTarget && ( + setReactionsTarget(null)} + /> + )} + setDeleteTarget(null)} + /> + + ) +} diff --git a/src/components/dms/ReactionsDialog.tsx b/src/components/dms/ReactionsDialog.tsx index 576ef4cead..8119cc1b7c 100644 --- a/src/components/dms/ReactionsDialog.tsx +++ b/src/components/dms/ReactionsDialog.tsx @@ -1,4 +1,4 @@ -import {useRef, useState} from 'react' +import {useMemo, useRef, useState} from 'react' import { LayoutAnimation, Pressable, @@ -37,14 +37,12 @@ export function ReactionsDialog({ control, relatedProfiles, message, - reactions, - groupedReactions, + onClose, }: { control: Dialog.DialogControlProps relatedProfiles: Map message: ChatBskyConvoDefs.MessageView - reactions?: ChatBskyConvoDefs.ReactionView[] - groupedReactions?: Reaction[] + onClose?: () => void }) { const {t: l} = useLingui() @@ -54,6 +52,9 @@ export function ReactionsDialog({ const [selected, setSelected] = useState('all') + const reactions = message.reactions + const groupedReactions = useMemo(() => groupReactions(reactions), [reactions]) + const filteredReactions = reactions?.filter( r => selected === 'all' || r.value === selected, ) @@ -78,7 +79,10 @@ export function ReactionsDialog({ return ( setSelected('all')} + onClose={() => { + setSelected('all') + onClose?.() + }} nativeOptions={{ preventExpansion: true, minHeight: screenHeight / 2, @@ -388,3 +392,25 @@ function ReactionTab({ ) } + +export function groupReactions( + reactions: ChatBskyConvoDefs.ReactionView[] | undefined, +): Reaction[] { + const grouped = new Map() + for (const reaction of reactions ?? []) { + if (!reaction) continue + const existing = grouped.get(reaction.value) + if (existing) { + existing.senders.push(reaction.sender) + existing.count++ + } else { + grouped.set(reaction.value, { + key: reaction.value, + value: reaction.value, + senders: [reaction.sender], + count: 1, + }) + } + } + return Array.from(grouped.values()) +} diff --git a/src/components/hooks/useStarterPackEntry.native.ts b/src/components/hooks/useLandingEntry.native.ts similarity index 64% rename from src/components/hooks/useStarterPackEntry.native.ts rename to src/components/hooks/useLandingEntry.native.ts index 31221d599d..c5950a6469 100644 --- a/src/components/hooks/useStarterPackEntry.native.ts +++ b/src/components/hooks/useLandingEntry.native.ts @@ -1,22 +1,45 @@ import {useEffect, useState} from 'react' +import * as Linking from 'expo-linking' +import {parseLinkingUrl} from '#/lib/parseLinkingUrl' import { createStarterPackLinkFromAndroidReferrer, httpStarterPackUriToAtUri, } from '#/lib/strings/starter-pack' +import {CHAT_INVITE_CODE_REGEX} from '#/lib/strings/url-helpers' import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' -import {useSetActiveStarterPack} from '#/state/shell/starter-pack' +import { + useSetActiveLanding, + useSetActiveStarterPack, +} from '#/state/shell/landing' import {IS_ANDROID} from '#/env' import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' -export function useStarterPackEntry() { +export function useLandingEntry() { const [ready, setReady] = useState(false) const setActiveStarterPack = useSetActiveStarterPack() + const setActiveLanding = useSetActiveLanding() const hasCheckedForStarterPack = useHasCheckedForStarterPack() useEffect(() => { if (ready) return + // Check for group chat invite link from the initial deep link URL + const linkingUrl = Linking.getLinkingURL() + if (linkingUrl) { + const urlp = parseLinkingUrl(linkingUrl) + const chatInviteMatch = urlp.pathname.match(CHAT_INVITE_CODE_REGEX) + if (chatInviteMatch) { + setActiveLanding({ + type: 'groupchat', + uri: linkingUrl, + code: chatInviteMatch[1], + }) + setReady(true) + return + } + } + // On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So, // let's just ensure we never check again after the first time. if (hasCheckedForStarterPack) { @@ -29,7 +52,8 @@ export function useStarterPackEntry() { setReady(true) }, 500) - ;(async () => { + void (async () => { + // Check for starter pack let uri: string | null | undefined if (IS_ANDROID) { @@ -58,7 +82,7 @@ export function useStarterPackEntry() { return () => { clearTimeout(timeout) } - }, [ready, setActiveStarterPack, hasCheckedForStarterPack]) + }, [ready, setActiveStarterPack, setActiveLanding, hasCheckedForStarterPack]) return ready } diff --git a/src/components/hooks/useStarterPackEntry.ts b/src/components/hooks/useLandingEntry.ts similarity index 76% rename from src/components/hooks/useStarterPackEntry.ts rename to src/components/hooks/useLandingEntry.ts index ccbd991574..e3b728a5a0 100644 --- a/src/components/hooks/useStarterPackEntry.ts +++ b/src/components/hooks/useLandingEntry.ts @@ -1,25 +1,27 @@ import {useEffect, useState} from 'react' import {httpStarterPackUriToAtUri} from '#/lib/strings/starter-pack' -import {useSetActiveStarterPack} from '#/state/shell/starter-pack' +import {useSetActiveStarterPack} from '#/state/shell/landing' -export function useStarterPackEntry() { +export function useLandingEntry() { const [ready, setReady] = useState(false) - const setActiveStarterPack = useSetActiveStarterPack() useEffect(() => { const href = window.location.href - const atUri = httpStarterPackUriToAtUri(href) + const url = new URL(href) + // Check for starter pack + const atUri = httpStarterPackUriToAtUri(href) if (atUri) { - const url = new URL(href) // Determines if an App Clip is loading this landing page const isClip = url.searchParams.get('clip') === 'true' setActiveStarterPack({ uri: atUri, isClip, }) + setReady(true) + return } setReady(true) diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx index 4feaaef359..a7ef371cd1 100644 --- a/src/components/icons/TEMPLATE.tsx +++ b/src/components/icons/TEMPLATE.tsx @@ -1,8 +1,20 @@ -import {forwardRef} from 'react' +import { + forwardRef, + type ForwardRefExoticComponent, + type RefAttributes, +} from 'react' import Svg, {Path} from 'react-native-svg' import {type Props, useCommonSVGProps} from '#/components/icons/common' +export type IconWithSvgMeta = ForwardRefExoticComponent< + Props & RefAttributes +> & { + svgPaths: string[] + svgViewBox: string + svgStrokeWidth: number +} + export const IconTemplate_Stroke2_Corner0_Rounded = forwardRef( function LogoImpl(props: Props, ref) { const {fill, size, style, ...rest} = useCommonSVGProps(props) @@ -41,7 +53,7 @@ export function createSinglePathSVG({ strokeLinecap?: 'butt' | 'round' | 'square' strokeLinejoin?: 'miter' | 'round' | 'bevel' }) { - return forwardRef(function LogoImpl(props, ref) { + const Icon = forwardRef(function LogoImpl(props, ref) { const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) const hasStroke = strokeWidth > 0 @@ -68,7 +80,11 @@ export function createSinglePathSVG({ /> ) - }) + }) as IconWithSvgMeta + Icon.svgPaths = [path] + Icon.svgViewBox = viewBox || '0 0 24 24' + Icon.svgStrokeWidth = strokeWidth + return Icon } export function createMultiPathSVG({ @@ -78,7 +94,7 @@ export function createMultiPathSVG({ paths: string[] viewBox?: string }) { - return forwardRef(function LogoImpl(props, ref) { + const Icon = forwardRef(function LogoImpl(props, ref) { const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) return ( @@ -102,5 +118,9 @@ export function createMultiPathSVG({ ))} ) - }) + }) as IconWithSvgMeta + Icon.svgPaths = paths + Icon.svgViewBox = viewBox || '0 0 24 24' + Icon.svgStrokeWidth = 0 + return Icon } diff --git a/src/components/images/AutoSizedImage.tsx b/src/components/images/AutoSizedImage.tsx index 2df8d056d6..182bfe661b 100644 --- a/src/components/images/AutoSizedImage.tsx +++ b/src/components/images/AutoSizedImage.tsx @@ -1,4 +1,4 @@ -import {useMemo, useRef} from 'react' +import {useEffect, useMemo, useRef} from 'react' import {type DimensionValue, Pressable, View} from 'react-native' import Animated, { type AnimatedRef, @@ -69,6 +69,8 @@ export function AutoSizedImage({ onPress, onLongPress, onPressIn, + onContainerRef, + onDimsChange, }: { image: AppBskyEmbedImages.ViewImage crop?: 'none' | 'square' | 'constrained' @@ -79,6 +81,11 @@ export function AutoSizedImage({ ) => void onLongPress?: () => void onPressIn?: () => void + /** Fires once with the internal container ref so a parent can drive its + * own lightbox-return animation without waiting for an `onPress`. */ + onContainerRef?: (ref: AnimatedRef) => void + /** Fires when the underlying image reports its natural dimensions. */ + onDimsChange?: (dims: Dimensions) => void }) { const t = useTheme() const {_} = useLingui() @@ -86,6 +93,10 @@ export function AutoSizedImage({ const containerRef = useAnimatedRef() const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) + useEffect(() => { + onContainerRef?.(containerRef) + }, [containerRef, onContainerRef]) + let aspectRatio: number | undefined const dims = image.aspectRatio if (dims) { @@ -122,13 +133,16 @@ export function AutoSizedImage({ accessibilityHint="" onLoad={e => { if (!isContain) { - fetchedDimsRef.current = { + const dims = { width: e.source.width, height: e.source.height, } + fetchedDimsRef.current = dims + onDimsChange?.(dims) } }} loading="lazy" + useAppleWebpCodec /> diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 1566f09736..fd18cb8a0b 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -24,7 +24,7 @@ import {mergeRefs} from '#/lib/merge-refs' import {useA11y} from '#/state/a11y' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' -import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf' import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' import {AutoSizedImage} from '#/components/images/AutoSizedImage' import { @@ -36,10 +36,11 @@ import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandle import {usePointerHandlers} from '#/components/images/Gallery/usePointerHandlers' import {getAspectRatio} from '#/components/images/Gallery/utils' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' -import {IS_WEB} from '#/env' +import {IS_ANDROID, IS_WEB} from '#/env' export * from './const' export * from './maybeApplyGalleryOffsetStyles' @@ -53,6 +54,7 @@ interface GalleryProps { ) => void onPressIn?: (index: number) => void viewContext?: PostEmbedViewContext + isWithinQuote?: boolean } const Context = createContext<{ @@ -96,6 +98,7 @@ export function Gallery({ onPress, onPressIn, viewContext, + isWithinQuote, }: GalleryProps) { const {t: l} = useLingui() const ax = useAnalytics() @@ -103,14 +106,21 @@ export function Gallery({ const largeAltBadge = useLargeAltBadgeEnabled() const bps = useBreakpoints() const window = useWindowDimensions() - const isWithinQuote = - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia const isWithinChat = viewContext === PostEmbedViewContext.ChatMessage const hideBadges = isWithinQuote const contentHeight = useMemo(() => { if (isWithinChat) { return 120 } + if (isWithinQuote) { + if (bps.gtMobile) { + return 220 + } else if (bps.gtPhone) { + return 190 + } else { + return 150 + } + } if (bps.gtMobile) { return 300 } else if (bps.gtPhone) { @@ -118,7 +128,7 @@ export function Gallery({ } else { return 200 } - }, [bps, isWithinChat]) + }, [bps, isWithinChat, isWithinQuote]) /* * Container overflow styles @@ -219,7 +229,7 @@ export function Gallery({ crop={ viewContext === PostEmbedViewContext.ThreadHighlighted ? 'none' - : viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + : isWithinQuote ? 'square' : 'constrained' } @@ -228,9 +238,7 @@ export function Gallery({ onPress?.(index, [containerRef], [dims]) } onPressIn={() => onPressIn?.(index)} - hideBadge={ - viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - } + hideBadge={isWithinQuote} /> ))} @@ -256,6 +264,9 @@ export function Gallery({ aria-label={l`Image gallery, ${images.length} images`} horizontal pagingEnabled={false} + // Disable Android's stretch overscroll, which can leave the carousel + // settled just off the left edge instead of aligned to x = 0 + overScrollMode={IS_ANDROID ? 'never' : 'auto'} showsHorizontalScrollIndicator={false} directionalLockEnabled nestedScrollEnabled @@ -264,6 +275,21 @@ export function Gallery({ data={images} keyExtractor={(item, index) => item.thumb + index} renderItem={({item, index}) => { + const openLightboxAtIndex = onPress + ? () => { + ax.metric('post:gallery:openLightbox', { + fromImage: index + 1, // convert to 1-based index for easier analysis + totalImages: images.length, + }) + const refs: AnimatedRef[] = [] + const dims: (Dimensions | null)[] = [] + for (let i = 0; i < images.length; i++) { + refs.push(containerRefsRef.current.get(i)!) + dims.push(thumbDimsRef.current.get(i) ?? null) + } + onPress(index, refs, dims) + } + : undefined return ( { thumbDimsRef.current.set(i, dims) }} - onPress={ - onPress - ? () => { - ax.metric('post:gallery:openLightbox', { - fromImage: index + 1, // convert to 1-based index for easier analysis - totalImages: images.length, - }) - const refs: AnimatedRef[] = [] - const dims: (Dimensions | null)[] = [] - for (let i = 0; i < images.length; i++) { - refs.push(containerRefsRef.current.get(i)!) - dims.push(thumbDimsRef.current.get(i) ?? null) - } - onPress(index, refs, dims) - } - : undefined - } + onPress={openLightboxAtIndex} onPressIn={onPressIn ? () => onPressIn(index) : undefined} + onPreviewPress={openLightboxAtIndex} /> ) }} @@ -378,6 +389,7 @@ function GalleryImage({ onThumbDims, onPress, onPressIn, + onPreviewPress, }: { contentHeight: number image: AppBskyEmbedImages.ViewImage @@ -391,6 +403,7 @@ function GalleryImage({ onThumbDims: (index: number, dims: Dimensions) => void onPress?: () => void onPressIn?: () => void + onPreviewPress?: () => void }) { const t = useTheme() const {t: l} = useLingui() @@ -416,124 +429,132 @@ function GalleryImage({ collapsable={false} aria-roledescription={l`slide`} aria-label={image.alt || l`Image ${index + 1} of ${imageCount}`}> - setFocused(true)} - onBlur={() => setFocused(false)} - accessibilityRole="button" - accessibilityLabel={image.alt || l`Image ${index + 1}`} - accessibilityHint={l`Opens full image`} - android_ripple={{ - color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), - foreground: true, - }} - style={({pressed}) => [ - a.rounded_md, - a.overflow_hidden, - t.atoms.bg_contrast_25, - web([ - { - cursor: 'inherit', - outline: 0, - border: 0, - }, - a.transition_transform, - {transitionDuration: '200ms'}, - pressed && {transform: [{scale: 0.99}]}, - ]), - ]}> - { - const ar = getAspectRatio(e.source) - if (ar && ar !== aspectRatio) { - setAspectRatio(ar) - } - onThumbDims(index, { - width: e.source.width, - height: e.source.height, - }) + + setFocused(true)} + onBlur={() => setFocused(false)} + accessibilityRole="button" + accessibilityLabel={image.alt || l`Image ${index + 1}`} + accessibilityHint={l`Opens full image`} + android_ripple={{ + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, }} - /> - - {(hasAlt || isCropped) && !hideBadges ? ( - [ + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + web([ { - bottom: a.p_xs.padding, - right: a.p_xs.padding, - gap: 3, + cursor: 'inherit', + outline: 0, + border: 0, }, - largeAltBadge && { - gap: 4, - }, - ]}> - {isCropped && ( - - - - )} - {hasAlt && ( - - - ALT - - - )} - - ) : null} + a.transition_transform, + {transitionDuration: '200ms'}, + pressed && {transform: [{scale: 0.99}]}, + ]), + ]}> + { + const ar = getAspectRatio(e.source) + if (ar && ar !== aspectRatio) { + setAspectRatio(ar) + } + onThumbDims(index, { + width: e.source.width, + height: e.source.height, + }) + }} + useAppleWebpCodec + /> - + {isCropped && ( + + + + )} + {hasAlt && ( + + + ALT + + + )} + + ) : null} + + - + /> + + ) } diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index b809a783fa..d6cbc0aded 100644 --- a/src/components/images/ImageLayoutGridItem.tsx +++ b/src/components/images/ImageLayoutGridItem.tsx @@ -9,8 +9,9 @@ import {Trans} from '@lingui/react/macro' import {type Dimensions} from '#/lib/media/types' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' @@ -52,46 +53,64 @@ export function GalleryItem({ const hasAlt = !!image.alt const hideBadges = viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + + const aspect = + image.aspectRatio && image.aspectRatio.height > 0 + ? image.aspectRatio.width / image.aspectRatio.height + : undefined + + // The tap handler and the peek-commit handler do the same thing: open the + // lightbox with this cell's ref + dims so the lightbox's return animation + // can target the original thumbnail. + const openLightboxAtIndex = onPress + ? () => onPress(index, containerRefs, thumbDimsRef.current.slice()) + : undefined + return ( - onPress(index, containerRefs, thumbDimsRef.current.slice()) - : undefined - } - onPressIn={onPressIn ? () => onPressIn(index) : undefined} - onLongPress={onLongPress ? () => onLongPress(index) : undefined} - android_ripple={{ - color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), - foreground: true, - }} - style={[ - a.flex_1, - a.overflow_hidden, - t.atoms.bg_contrast_25, - imageStyle, - ]} - accessibilityRole="button" - accessibilityLabel={image.alt || _(msg`Image`)} - accessibilityHint=""> - { - thumbDimsRef.current[index] = { - width: e.source.width, - height: e.source.height, - } + + onPressIn(index) : undefined} + onLongPress={onLongPress ? () => onLongPress(index) : undefined} + android_ripple={{ + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, }} - loading="lazy" - /> - - + style={[ + a.flex_1, + a.overflow_hidden, + t.atoms.bg_contrast_25, + imageStyle, + ]} + accessibilityRole="button" + accessibilityLabel={image.alt || _(msg`Image`)} + accessibilityHint=""> + { + thumbDimsRef.current[index] = { + width: e.source.width, + height: e.source.height, + } + }} + loading="lazy" + useAppleWebpCodec + /> + + + {hasAlt && !hideBadges ? ( + + + + ) +} + +function GroupChatJoinDialogInner({code}: {code?: string}) { + const {t: l} = useLingui() + + return ( + + + + + + + ) +} + +function GroupChatJoinDialogContent({code}: {code?: string}) { + const t = useTheme() + const {t: l} = useLingui() + const {groupChatJoinDialogControl: control} = useIntentDialogs() + const {hasSession} = useSession() + const moderationOpts = useModerationOpts() + const navigation = useNavigation() + + const {data, error, isLoading} = useJoinLinkPreviewsQuery({ + codes: code ? [code] : undefined, + hasSession, + staleTime: 0, + }) + + const {mutate: joinGroupChat, isPending: isJoinPending} = + useRequestJoinGroupChat({ + onSuccess: data => { + switch (data.status) { + case 'pending': + control.close(() => { + Toast.show( + l`Access requested! The group owner will review your request.`, + ) + }) + break + case 'joined': { + if (data.convo && data.convo.id) { + control.close(() => { + Toast.show(l`Successfully joined the group chat!`) + navigation.navigate('MessagesConversation', { + conversation: data.convo!.id, + }) + }) + } else { + logger.warn('Request to join group chat returned no convo ID', { + status: data.status, + convoId: data.convo?.id, + }) + } + break + } + } + }, + onError: error => { + let errorMessage = l`Failed to join the group chat. Please try again.` + if (isNetworkError(error)) { + errorMessage = l`There was a problem with your internet connection, please try again` + } else if (error instanceof ChatBskyGroupRequestJoin.ConvoLockedError) { + errorMessage = l`This conversation is locked.` + } else if ( + error instanceof ChatBskyGroupRequestJoin.FollowRequiredError + ) { + errorMessage = l`Only followers can join this group chat.` + } else if (error instanceof ChatBskyGroupRequestJoin.InvalidCodeError) { + errorMessage = l`Invalid group chat code.` + } else if ( + error instanceof ChatBskyGroupRequestJoin.LinkDisabledError + ) { + errorMessage = l`This invite link has been disabled.` + } else if ( + error instanceof ChatBskyGroupRequestJoin.MemberLimitReachedError + ) { + errorMessage = l`The member limit has been reached.` + } else if (error instanceof ChatBskyGroupRequestJoin.UserKickedError) { + errorMessage = l`You have been removed from this group.` + } + Toast.show(errorMessage) + }, + }) + + const {mutate: withdrawRequest, isPending: isWithdrawPending} = + useWithdrawJoinGroupChatRequest({ + onSuccess: () => { + control.close(() => { + Toast.show(l`Join request rescinded.`) + }) + }, + onError: error => { + let errorMessage = l`Failed to rescind your request. Please try again.` + if (isNetworkError(error)) { + errorMessage = l`There was a problem with your internet connection, please try again` + } else if ( + error instanceof + ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError + ) { + errorMessage = l`Invalid rescind request.` + } + Toast.show(errorMessage) + }, + }) + + const { + state: interacted, + onIn: onInteract, + onOut: onInteractOut, + } = useInteractionState() + + const handleJoin = () => { + if (!code) return + joinGroupChat({code}) + } + + const handleWithdraw = () => { + if (!convoId) return + withdrawRequest({convoId}) + } + + // Fallback if the prefetch exceeds the timeout + if (isLoading || !data || !moderationOpts) { + return ( + + + + ) + } + + if (error) { + return ( + <> + + + This invite link is invalid + + + + ) + } + + const joinLinkPreview = data.joinLinkPreviews[0] + + if (!joinLinkPreview) { + return ( + <> + + + + + Chat invite link no longer available + + + + + + ) + } + + const convoId = joinLinkPreview.convo?.id + const isFollowing = joinLinkPreview.owner.viewer?.following ?? false + const hasRequested = !convoId && joinLinkPreview.viewer?.requestedAt != null + + let canJoin = true + let ButtonIconImage = isJoinPending || isWithdrawPending ? Loader : JoinIcon + let buttonText = joinLinkPreview.requireApproval + ? l`Request to join` + : l`Join` + let buttonColor: ButtonColor = 'primary' + if (joinLinkPreview.enabledStatus !== 'enabled') { + canJoin = false + ButtonIconImage = WarningIcon + buttonText = l`Chat invite link no longer available` + buttonColor = 'secondary' + } else if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`This chat is full` + buttonColor = 'secondary' + } else if (joinLinkPreview.joinRule === 'followedByOwner' && !isFollowing) { + canJoin = false + ButtonIconImage = HandIcon + buttonText = l`Only people the chat owner follows can join` + buttonColor = 'secondary' + } else if (hasRequested) { + ButtonIconImage = XIcon + buttonText = l`Rescind request` + buttonColor = 'secondary' + } + + return ( + <> + + + + + + Group chat + + + {joinLinkPreview.name} + + + + + + {joinLinkPreview.memberCount}/{joinLinkPreview.memberLimit}{' '} + members + + + + + + + {joinLinkPreview.joinRule === 'followedByOwner' + ? l`Followers can join` + : l`Anyone can join`} + + + + + + By{' '} + { + onInteract() + }, + onMouseLeave: () => { + onInteractOut() + }, + })}> + {createSanitizedDisplayName( + joinLinkPreview.owner, + true, + moderateProfile(joinLinkPreview.owner, moderationOpts).ui( + 'displayName', + ), + )} + + + + + + {sanitizeHandle(joinLinkPreview.owner.handle, '@')} + + + + + {convoId ? ( + + ) : ( + + )} + + ) +} diff --git a/src/components/intents/IntentDialogs.tsx b/src/components/intents/IntentDialogs.tsx index ea72f8c099..76809b366a 100644 --- a/src/components/intents/IntentDialogs.tsx +++ b/src/components/intents/IntentDialogs.tsx @@ -1,13 +1,30 @@ -import {createContext, useContext, useMemo, useState} from 'react' +import { + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import {usePrefetchJoinLinkPreviews} from '#/state/queries/join-links' +import {useSession} from '#/state/session' +import { + useActiveGroupChatJoinRequest, + useSetActiveLanding, +} from '#/state/shell/landing' import * as Dialog from '#/components/Dialog' import {type DialogControlProps} from '#/components/Dialog' +import {GroupChatJoinDialog} from '#/components/intents/GroupChatJoinDialog' import {VerifyEmailIntentDialog} from '#/components/intents/VerifyEmailIntentDialog' interface Context { verifyEmailDialogControl: DialogControlProps verifyEmailState: {code: string} | undefined setVerifyEmailState: (state: {code: string} | undefined) => void + groupChatJoinDialogControl: DialogControlProps + groupChatJoinState: {code: string} | undefined + setGroupChatJoinState: (state: {code: string} | undefined) => void } const Context = createContext({} as Context) @@ -19,20 +36,70 @@ export function Provider({children}: {children: React.ReactNode}) { const [verifyEmailState, setVerifyEmailState] = useState< {code: string} | undefined >() + const groupChatJoinDialogControl = Dialog.useDialogControl() + const [groupChatJoinState, setGroupChatJoinState] = useState< + {code: string} | undefined + >() + + const {hasSession} = useSession() + const groupChatLanding = useActiveGroupChatJoinRequest() + const setActiveLanding = useSetActiveLanding() + const prefetchJoinLinkPreviews = usePrefetchJoinLinkPreviews() + const landingHandledRef = useRef(false) + + useEffect(() => { + if (hasSession && groupChatLanding && !landingHandledRef.current) { + landingHandledRef.current = true + const code = groupChatLanding.code + setActiveLanding(undefined) + const prefetch = prefetchJoinLinkPreviews({ + codes: [code], + hasSession: true, + }) + void Promise.race([ + prefetch, + new Promise(res => setTimeout(res, 200)), + ]).finally(() => { + setGroupChatJoinState({code}) + groupChatJoinDialogControl.open() + }) + } + if (!groupChatLanding) { + landingHandledRef.current = false + } + }, [ + hasSession, + groupChatLanding, + setActiveLanding, + setGroupChatJoinState, + prefetchJoinLinkPreviews, + groupChatJoinDialogControl, + ]) const value = useMemo( () => ({ verifyEmailDialogControl, verifyEmailState, setVerifyEmailState, + groupChatJoinDialogControl, + groupChatJoinState, + setGroupChatJoinState, }), - [verifyEmailDialogControl, verifyEmailState, setVerifyEmailState], + [ + verifyEmailDialogControl, + verifyEmailState, + setVerifyEmailState, + groupChatJoinDialogControl, + groupChatJoinState, + setGroupChatJoinState, + ], ) return ( {children} + ) } diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index c24c6feae0..d1d3aa5584 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -75,9 +75,11 @@ export function ReportDialog( () => (props.subject ? parseReportSubject(props.subject) : undefined), [props.subject], ) + const propsOnClose = props.onClose const onClose = useCallback(() => { ax.metric('reportDialog:close', {}) - }, [ax]) + propsOnClose?.() + }, [ax, propsOnClose]) return ( diff --git a/src/components/moderation/ReportDialog/types.ts b/src/components/moderation/ReportDialog/types.ts index 89089d138b..57ed3cd279 100644 --- a/src/components/moderation/ReportDialog/types.ts +++ b/src/components/moderation/ReportDialog/types.ts @@ -88,4 +88,8 @@ export type ReportDialogProps = { * Called if the report was successfully submitted. */ onAfterSubmit?: () => void + /** + * Called after the dialog finishes closing. + */ + onClose?: () => void } diff --git a/src/components/verification/VerifierDialog.tsx b/src/components/verification/VerifierDialog.tsx index 570441ecd0..e8944024ca 100644 --- a/src/components/verification/VerifierDialog.tsx +++ b/src/components/verification/VerifierDialog.tsx @@ -87,6 +87,7 @@ function Inner({ alt={_( msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, )} + useAppleWebpCodec /> diff --git a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx index df2bb17f83..9d2f948eb6 100644 --- a/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx +++ b/src/features/liveEvents/components/LiveEventFeedCardCompact.tsx @@ -69,6 +69,7 @@ export function LiveEventFeedCardCompact({ style={[a.absolute, a.inset_0, a.w_full, a.h_full]} contentFit="cover" placeholderContentFit="cover" + useAppleWebpCodec /> setImageLoadError(false)} onError={() => setImageLoadError(true)} + useAppleWebpCodec /> )} {linkMeta && (!linkMeta.image || imageLoadError) && ( diff --git a/src/features/liveNow/components/LiveStatusDialog.tsx b/src/features/liveNow/components/LiveStatusDialog.tsx index 545e8ab943..9e916678f2 100644 --- a/src/features/liveNow/components/LiveStatusDialog.tsx +++ b/src/features/liveNow/components/LiveStatusDialog.tsx @@ -147,6 +147,7 @@ export function LiveStatus({ contentFit="cover" style={[a.absolute, a.inset_0]} accessibilityIgnoresInvertColors + useAppleWebpCodec /> { } // Returns a transformed version of the object for use in DAG-CBOR. +// eslint-disable-next-line @typescript-eslint/no-explicit-any function prepareForHashing(v: any): any { // IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing, // the API client will convert this for you but we're hashing in the client, @@ -496,9 +508,10 @@ function prepareForHashing(v: any): any { // Walk through plain objects if (isPlainObject(v)) { - const obj: any = {} + const obj: Record = {} let pure = true for (const key in v) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access let value = v[key] // `value` is undefined if (value === undefined) { @@ -517,6 +530,7 @@ function prepareForHashing(v: any): any { return v } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function isPlainObject(v: any): boolean { if (typeof v !== 'object' || v === null) { return false diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index ec466b3551..ca0b342045 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -2,11 +2,12 @@ import { type AppBskyFeedDefs, type AppBskyGraphDefs, type BskyAgent, + type ChatBskyGroupDefs, type ComAtprotoRepoStrongRef, } from '@atproto/api' import {AtUri} from '@atproto/api' -import {IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants' +import {DM_SERVICE_HEADERS, IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' import {downloadAndResize} from '#/lib/media/manip' @@ -16,6 +17,7 @@ import { } from '#/lib/strings/starter-pack' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, isBskyCustomFeedUrl, isBskyListUrl, isBskyPostUrl, @@ -71,12 +73,20 @@ type ResolvedStarterPackRecord = { view: AppBskyGraphDefs.StarterPackView } +type ResolvedChatInvite = { + type: 'chat-invite' + uri: string + code: string + view?: ChatBskyGroupDefs.JoinLinkPreviewView +} + export type ResolvedLink = | ResolvedExternalLink | ResolvedPostRecord | ResolvedFeedRecord | ResolvedListRecord | ResolvedStarterPackRecord + | ResolvedChatInvite export class EmbeddingDisabledError extends Error { constructor() { @@ -141,6 +151,19 @@ export async function resolveLink( view: res.data.list, } } + const chatInviteCode = getChatInviteCodeFromUrl(uri) + if (chatInviteCode) { + const res = await agent.chat.bsky.group.getJoinLinkPreviews( + {codes: [chatInviteCode]}, + {headers: DM_SERVICE_HEADERS}, + ) + return { + type: 'chat-invite', + uri, + code: chatInviteCode, + view: res.data.joinLinkPreviews[0], + } + } if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { const parsed = parseStarterPackUri(uri) if (!parsed) { @@ -246,6 +269,10 @@ async function resolveExternal( title: result.title ?? '', description: result.description ?? '', thumb: result.image ? await imageToThumb(result.image) : undefined, + /* + * New fields from Standard Site integration. Other fields are derived from + * opengraph/oembed as before. + */ associatedRefs: result.associatedRefs, view: result.view, } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c77ce0cc94..13ae162a85 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -13,6 +13,7 @@ export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' export const DEFAULT_SERVICE = BSKY_SERVICE const HELP_DESK_LANG = 'en-us' export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}` +export const CHAT_SERVICE = 'https://api.bsky.chat' export const EMBED_SERVICE = 'https://embed.bsky.app' export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download' diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index c35ca49b16..5a9d0b39fe 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -5,7 +5,11 @@ import * as WebBrowser from 'expo-web-browser' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {parseLinkingUrl} from '#/lib/parseLinkingUrl' +import {CHAT_INVITE_CODE_REGEX} from '#/lib/strings/url-helpers' +import {usePrefetchJoinLinkPreviews} from '#/state/queries/join-links' import {useSession} from '#/state/session' +import {useSetActiveLanding} from '#/state/shell/landing' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {useIntentDialogs} from '#/components/intents/IntentDialogs' import {useAnalytics} from '#/analytics' @@ -25,6 +29,7 @@ export function useIntentHandler() { const ax = useAnalytics() const composeIntent = useComposeIntent() const verifyEmailIntent = useVerifyEmailIntent() + const groupChatJoinIntent = useGroupChatJoinIntent() const {currentAccount} = useSession() const {tryApplyUpdate} = useApplyPullRequestOTAUpdate() @@ -44,6 +49,11 @@ export function useIntentHandler() { }) } const urlp = parseLinkingUrl(url) + const chatInviteMatch = urlp.pathname.match(CHAT_INVITE_CODE_REGEX) + if (chatInviteMatch) { + groupChatJoinIntent(chatInviteMatch[1], url) + return + } const [, intent, intentType] = urlp.pathname.split('/') // On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the @@ -99,6 +109,7 @@ export function useIntentHandler() { ax, composeIntent, verifyEmailIntent, + groupChatJoinIntent, currentAccount, tryApplyUpdate, ]) @@ -162,6 +173,46 @@ export function useComposeIntent() { ) } +export function useGroupChatJoinIntent() { + const closeAllActiveElements = useCloseAllActiveElements() + const {hasSession} = useSession() + const {groupChatJoinDialogControl: control, setGroupChatJoinState: setState} = + useIntentDialogs() + const {requestSwitchToAccount} = useLoggedOutViewControls() + const setActiveLanding = useSetActiveLanding() + const prefetchJoinLinkPreviews = usePrefetchJoinLinkPreviews() + return useCallback( + (code: string, uri?: string) => { + closeAllActiveElements() + if (hasSession) { + setState({code}) + const prefetch = prefetchJoinLinkPreviews({ + codes: [code], + hasSession: true, + }) + void Promise.race([ + prefetch, + new Promise(res => setTimeout(res, 200)), + ]).finally(() => { + control.open() + }) + } else { + setActiveLanding({type: 'groupchat', uri: uri ?? '', code}) + requestSwitchToAccount({requestedAccount: 'groupchat'}) + } + }, + [ + closeAllActiveElements, + hasSession, + control, + setState, + prefetchJoinLinkPreviews, + requestSwitchToAccount, + setActiveLanding, + ], + ) +} + function useVerifyEmailIntent() { const closeAllActiveElements = useCloseAllActiveElements() const {verifyEmailDialogControl: control, setVerifyEmailState: setState} = diff --git a/src/lib/hooks/useTimeAgo.ts b/src/lib/hooks/useTimeAgo.ts index 67398ec547..ffc808a91f 100644 --- a/src/lib/hooks/useTimeAgo.ts +++ b/src/lib/hooks/useTimeAgo.ts @@ -58,12 +58,12 @@ export function dateDiff( if (diffSeconds < NOW) { diff = { value: 0, - unit: 'now' as DateDiff['unit'], + unit: 'now', } } else if (diffSeconds < MINUTE) { diff = { value: diffSeconds, - unit: 'second' as DateDiff['unit'], + unit: 'second', } } else if (diffSeconds < HOUR) { const value = @@ -72,7 +72,7 @@ export function dateDiff( : Math.floor(diffSeconds / MINUTE) diff = { value, - unit: 'minute' as DateDiff['unit'], + unit: 'minute', } } else if (diffSeconds < DAY) { const value = @@ -81,7 +81,7 @@ export function dateDiff( : Math.floor(diffSeconds / HOUR) diff = { value, - unit: 'hour' as DateDiff['unit'], + unit: 'hour', } } else if (diffSeconds < MONTH_30) { const value = @@ -90,7 +90,7 @@ export function dateDiff( : Math.floor(diffSeconds / DAY) diff = { value, - unit: 'day' as DateDiff['unit'], + unit: 'day', } } else { const value = @@ -99,7 +99,7 @@ export function dateDiff( : Math.floor(diffSeconds / MONTH_30) diff = { value, - unit: 'month' as DateDiff['unit'], + unit: 'month', } } diff --git a/src/lib/routes/links.ts b/src/lib/routes/links.ts index 908db7cba7..c87ccb5a39 100644 --- a/src/lib/routes/links.ts +++ b/src/lib/routes/links.ts @@ -19,8 +19,8 @@ export function makeProfileLink( export function makeCustomFeedLink( did: string, rkey: string, - segment?: string | undefined, - feedCacheKey?: 'discover' | 'explore' | undefined, + segment?: string, + feedCacheKey?: 'discover' | 'explore', ) { return ( [`/profile`, did, 'feed', rkey, ...(segment ? [segment] : [])].join('/') + diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 3c30b740da..22bb50572a 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -64,6 +64,7 @@ export type CommonNavigatorParams = { Topic: {topic: string} MessagesConversation: {conversation: string; embed?: string; accept?: true} MessagesConversationSettings: {conversation: string} + MessagesJoinRequests: {conversation: string} MessagesSettings: undefined MessagesInbox: undefined NotificationsActivityList: {posts: string} diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index cd2ae29afe..fd1a5ef7d9 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,5 +1,5 @@ import {AtUri} from '@atproto/api' -import psl from 'psl' +import {parse} from 'psl' import TLDs from 'tlds' import {BSKY_SERVICE} from '#/lib/constants' @@ -178,6 +178,29 @@ export function isBskyStarterPackUrl(url: string): boolean { return false } +// Invite codes are 7 alphanumeric characters long, supporting up to 10 here to future-proof. +export const CHAT_INVITE_CODE_REGEX = /^\/c\/([a-zA-Z0-9]{7,10})$/ + +export function getChatInviteCodeFromUrl(url: string): string | undefined { + let pathname: string + if (isBskyAppUrl(url)) { + try { + pathname = new URL(url).pathname + } catch { + return undefined + } + } else if (url.startsWith('/')) { + pathname = url.split('?')[0].split('#')[0] + } else { + return undefined + } + return pathname.match(CHAT_INVITE_CODE_REGEX)?.[1] +} + +export function isBskyChatInviteUrl(url: string): boolean { + return getChatInviteCodeFromUrl(url) !== undefined +} + export function isBskyDownloadUrl(url: string): boolean { if (isExternalUrl(url)) { return false @@ -306,7 +329,7 @@ export function isPossiblyAUrl(str: string): boolean { } export function splitApexDomain(hostname: string): [string, string] { - const hostnamep = psl.parse(hostname) + const hostnamep = parse(hostname) if (hostnamep.error || !hostnamep.listed || !hostnamep.domain) { return ['', hostname] } diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 4de882a998..5830797c52 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -86,9 +86,14 @@ msgstr "" msgid "{0, plural, one {# new join request} other {# new join requests}}" msgstr "{0, plural, one {# new join request} other {# new join requests}}" +#. placeholder {0}: convo.details.memberLimit +#: src/screens/Messages/components/InviteLinkDialog.tsx:181 +msgid "{0, plural, one {# person} other {# people}}" +msgstr "{0, plural, one {# person} other {# people}}" + #. placeholder {0}: reactions.length #. placeholder {1}: groupedReactions.map(g => g.value).join(' ') -#: src/components/dms/MessageItem.tsx:297 +#: src/components/dms/MessageItem.tsx:273 msgid "{0, plural, one {# person} other {# people}} reacted – {1}" msgstr "{0, plural, one {# person} other {# people}} reacted – {1}" @@ -117,7 +122,7 @@ msgstr "" #. How long it takes to read an article, in minutes. Displayed in a short form, e.g. "5m" for 5 minutes. #. placeholder {0}: view.readingTime -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:232 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:233 msgid "{0, plural, one {#m} other {#m}}" msgstr "{0, plural, one {#m} other {#m}}" @@ -206,7 +211,7 @@ msgid "{0} following" msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:640 +#: src/components/dms/MessageItem.tsx:609 msgid "{0} is blocking you" msgstr "{0} is blocking you" @@ -264,7 +269,7 @@ msgstr "" #. placeholder {0}: createSanitizedDisplayName(memberSender) #. placeholder {1}: reaction.value -#: src/components/dms/MessageItem.tsx:292 +#: src/components/dms/MessageItem.tsx:268 msgid "{0} reacted {1}" msgstr "" @@ -309,8 +314,8 @@ msgstr "" #. placeholder {0}: sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), ) #. placeholder {0}: sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), ) -#: src/view/com/util/UserAvatar.tsx:595 -#: src/view/com/util/UserAvatar.tsx:613 +#: src/view/com/util/UserAvatar.tsx:596 +#: src/view/com/util/UserAvatar.tsx:614 msgid "{0}'s avatar" msgstr "" @@ -319,6 +324,20 @@ msgstr "" msgid "{0}’s avatar" msgstr "{0}’s avatar" +#. The number of active group chat members out of the total number allowed. +#. placeholder {0}: data.joinLinkPreviews[0].memberCount +#. placeholder {1}: data.joinLinkPreviews[0].memberLimit +#: src/screens/Messages/JoinRequest.tsx:130 +msgid "{0}/{1}" +msgstr "{0}/{1}" + +#. The number of active group chat members out of the total number allowed. +#. placeholder {0}: joinLinkPreview.memberCount +#. placeholder {1}: joinLinkPreview.memberLimit +#: src/components/intents/GroupChatJoinDialog.tsx:307 +msgid "{0}/{1} members" +msgstr "{0}/{1} members" + #. How many days have passed, displayed in a narrow form #. placeholder {0}: diff.value #: src/lib/hooks/useTimeAgo.ts:171 @@ -355,6 +374,10 @@ msgstr "{count, plural, one {# request} other {# requests}}" msgid "{count, plural, one {# unread item} other {# unread items}}" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:429 +msgid "{count, plural, zero {No requests to join} one {# request to join} other {# requests to join}}" +msgstr "{count, plural, zero {No requests to join} one {# request to join} other {# requests to join}}" + #. Displayed when the number of requests is greater than 20 #: src/screens/Messages/components/InboxRequests.tsx:53 msgid "{count}+" @@ -365,6 +388,11 @@ msgstr "{count}+" msgid "{count}+ requests" msgstr "{count}+ requests" +#. Displayed when there are more requests to join a group chat than have been loaded +#: src/screens/Messages/JoinRequests.tsx:423 +msgid "{count}+ requests to join" +msgstr "{count}+ requests to join" + #: src/components/dms/DateDivider.tsx:60 msgid "{date} at {time}" msgstr "{date} at {time}" @@ -539,7 +567,7 @@ msgid "{following} following" msgstr "" #: src/components/dms/components/GroupChatProfileCard.tsx:56 -#: src/components/dms/InitiateChatFlow.tsx:888 +#: src/components/dms/InitiateChatFlow.tsx:889 msgid "{handle} can’t be added" msgstr "{handle} can’t be added" @@ -547,7 +575,7 @@ msgstr "{handle} can’t be added" msgid "{handle} can't be messaged" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:849 +#: src/components/dms/InitiateChatFlow.tsx:850 msgid "{handle} can’t be messaged" msgstr "{handle} can’t be messaged" @@ -581,7 +609,7 @@ msgid "{MAX_HIDDEN_REPLIES, plural, other {You can hide a maximum of # replies.} msgstr "" #. The number of group chat members out of the total number of permitted users. -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:34 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:39 msgid "{memberCount}/{memberLimit}" msgstr "{memberCount}/{memberLimit}" @@ -629,12 +657,12 @@ msgid "{rank}." msgstr "" #. The number of requests to join a group chat. -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:53 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:61 msgid "{requestCount, plural, one {# request} other {# requests}}" msgstr "{requestCount, plural, one {# request} other {# requests}}" #. Displayed when there are more than 20 requests to join a group chat -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:48 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:56 msgid "{requestCount}+ requests" msgstr "{requestCount}+ requests" @@ -802,6 +830,8 @@ msgstr "" #: src/components/contacts/screens/VerifyNumber.tsx:155 #: src/components/dms/dialogs/NewChatDialog.tsx:52 #: src/components/dms/dialogs/NewChatDialog.tsx:90 +#: src/screens/Messages/JoinRequests.tsx:188 +#: src/screens/Messages/JoinRequests.tsx:220 msgid "A network error occurred. Please check your internet connection." msgstr "" @@ -809,7 +839,7 @@ msgstr "" msgid "A new code has been sent" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:93 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:94 msgid "A new form of verification" msgstr "" @@ -836,7 +866,7 @@ msgstr "" msgid "A selected recipient is not followed by the sender." msgstr "A selected recipient is not followed by the sender." -#: src/Navigation.tsx:457 +#: src/Navigation.tsx:459 #: src/screens/Settings/AboutSettings.tsx:78 #: src/screens/Settings/Settings.tsx:255 #: src/screens/Settings/Settings.tsx:258 @@ -852,12 +882,22 @@ msgstr "" msgid "Accept" msgstr "" +#. Accept a request to join a chat +#: src/screens/Messages/JoinRequests.tsx:460 +msgctxt "button" +msgid "Accept" +msgstr "Accept" + #: src/screens/Messages/components/RequestButtons.tsx:280 msgid "Accept chat request" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:454 +msgid "Accept join request" +msgstr "Accept join request" + #. Accept a chat request -#: src/screens/Messages/components/RequestListItem.tsx:48 +#: src/screens/Messages/components/RequestListItem.tsx:51 msgid "Accept Request" msgstr "" @@ -870,17 +910,21 @@ msgstr "" msgid "Accepted conversations" msgstr "Accepted conversations" +#: src/components/intents/GroupChatJoinDialog.tsx:93 +msgid "Access requested! The group owner will review your request." +msgstr "Access requested! The group owner will review your request." + #: src/screens/Settings/AccessibilitySettings.tsx:45 #: src/screens/Settings/Settings.tsx:233 #: src/screens/Settings/Settings.tsx:236 msgid "Accessibility" msgstr "" -#: src/Navigation.tsx:380 +#: src/Navigation.tsx:382 msgid "Accessibility Settings" msgstr "" -#: src/Navigation.tsx:396 +#: src/Navigation.tsx:398 #: src/screens/Login/LoginForm.tsx:192 #: src/screens/Settings/AccountSettings.tsx:56 #: src/screens/Settings/Settings.tsx:173 @@ -955,7 +999,7 @@ msgctxt "toast" msgid "Account unmuted" msgstr "" -#: src/components/verification/VerifierDialog.tsx:99 +#: src/components/verification/VerifierDialog.tsx:100 msgid "Accounts with a scalloped blue check mark <0><1/> can verify others. These trusted verifiers are selected by Bluesky." msgstr "" @@ -1023,11 +1067,11 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:1401 +#: src/view/com/composer/Composer.tsx:1383 msgid "Add another post" msgstr "" -#: src/view/com/composer/Composer.tsx:2067 +#: src/view/com/composer/Composer.tsx:2049 msgid "Add another post to thread" msgstr "" @@ -1041,7 +1085,7 @@ msgstr "" msgid "Add App Password" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:166 +#: src/screens/Settings/AutomationLabelSettings.tsx:170 msgid "Add automation label to account" msgstr "Add automation label to account" @@ -1058,7 +1102,7 @@ msgid "Add image" msgstr "" #. Accessibility label for button in composer to add images, a video, or a GIF to a post -#: src/view/com/composer/SelectMediaButton.tsx:499 +#: src/view/com/composer/SelectMediaButton.tsx:503 msgid "Add media to post" msgstr "" @@ -1068,8 +1112,8 @@ msgstr "" msgid "Add members" msgstr "Add members" -#: src/components/moderation/ReportDialog/index.tsx:526 -#: src/components/moderation/ReportDialog/index.tsx:530 +#: src/components/moderation/ReportDialog/index.tsx:528 +#: src/components/moderation/ReportDialog/index.tsx:532 msgid "Add more details (optional)" msgstr "" @@ -1086,8 +1130,8 @@ msgstr "" msgid "Add muted words and tags" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:135 -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:174 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:132 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:171 #: src/screens/ProfileList/AboutSection.tsx:72 #: src/screens/ProfileList/AboutSection.tsx:90 msgid "Add people" @@ -1171,7 +1215,7 @@ msgstr "" msgid "Additional details (limit 1000 characters)" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:544 +#: src/components/moderation/ReportDialog/index.tsx:546 msgid "Additional details (limit 300 characters)" msgstr "" @@ -1239,7 +1283,7 @@ msgid "alice@example.com" msgstr "" #. the default tab in the interests tab bar -#: src/components/dms/ReactionsDialog.tsx:284 +#: src/components/dms/ReactionsDialog.tsx:288 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:201 #: src/view/screens/Notifications.tsx:86 msgid "All" @@ -1247,7 +1291,7 @@ msgstr "" #. Tab label showing the total count of reactions on a chat message. #. placeholder {0}: tab.count -#: src/components/dms/ReactionsDialog.tsx:381 +#: src/components/dms/ReactionsDialog.tsx:385 msgid "All {0}" msgstr "All {0}" @@ -1344,9 +1388,9 @@ msgstr "" msgid "Already signed in as @{0}" msgstr "" -#: src/components/images/AutoSizedImage.tsx:190 -#: src/components/images/Gallery/index.tsx:522 -#: src/components/images/ImageLayoutGridItem.tsx:120 +#: src/components/images/AutoSizedImage.tsx:204 +#: src/components/images/Gallery/index.tsx:539 +#: src/components/images/ImageLayoutGridItem.tsx:139 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94 #: src/view/com/composer/GifAltText.tsx:100 #: src/view/com/composer/photos/Gallery.tsx:209 @@ -1393,7 +1437,7 @@ msgstr "" msgid "An error occurred" msgstr "" -#: src/view/com/composer/state/video.ts:411 +#: src/view/com/composer/state/video.ts:401 msgid "An error occurred while compressing the video." msgstr "" @@ -1438,11 +1482,11 @@ msgstr "" msgid "An error occurred while trying to follow all" msgstr "" -#: src/view/com/composer/state/video.ts:468 +#: src/view/com/composer/state/video.ts:453 msgid "An error occurred while uploading the video. {message}" msgstr "" -#: src/view/com/composer/state/video.ts:460 +#: src/view/com/composer/state/video.ts:445 msgid "An error occurred while uploading the video. Please check your internet connection and try again." msgstr "" @@ -1468,8 +1512,8 @@ msgid "An illustration showing that Bluesky selects trusted verifiers, and trust msgstr "" #: src/screens/Messages/components/InviteLinkDialog.tsx:172 -msgid "An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time." -msgstr "An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time." +msgid "An invite link lets people join this group chat without being added directly. You control who can join the chat. You can disable the link at any time." +msgstr "An invite link lets people join this group chat without being added directly. You control who can join the chat. You can disable the link at any time." #: src/components/moderation/ReportDialog/utils/useReportOptions.ts:239 msgid "An issue not included in these options" @@ -1496,7 +1540,7 @@ msgstr "" msgid "An issue occurred, please try again." msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:120 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:121 msgid "An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name." msgstr "" @@ -1545,6 +1589,10 @@ msgstr "" msgid "Anyone can interact" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:322 +msgid "Anyone can join" +msgstr "Anyone can join" + #: src/screens/Messages/components/InviteLinkDialog.tsx:143 #: src/screens/Messages/components/InviteLinkDialog.tsx:144 msgid "Anyone can join instantly" @@ -1561,11 +1609,11 @@ msgstr "Anyone can request to join" msgid "Anyone who follows me" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:436 +#: src/screens/Messages/components/InviteLinkDialog.tsx:446 msgid "Anyone who has it will no longer be able to join or request to join. You can always create a new one." msgstr "Anyone who has it will no longer be able to join or request to join. You can always create a new one." -#: src/Navigation.tsx:465 +#: src/Navigation.tsx:467 #: src/screens/Settings/AppIconSettings/index.tsx:65 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:19 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:24 @@ -1603,7 +1651,7 @@ msgstr "" msgid "App passwords" msgstr "" -#: src/Navigation.tsx:348 +#: src/Navigation.tsx:350 #: src/screens/Settings/AppPasswords.tsx:51 msgid "App Passwords" msgstr "" @@ -1645,7 +1693,7 @@ msgstr "" msgid "Appeal this decision" msgstr "" -#: src/Navigation.tsx:388 +#: src/Navigation.tsx:390 #: src/screens/Settings/AppearanceSettings.tsx:73 #: src/screens/Settings/Settings.tsx:225 #: src/screens/Settings/Settings.tsx:228 @@ -1681,7 +1729,7 @@ msgstr "" msgid "Are you sure you want to delete the app password \"{0}\"?" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:223 +#: src/components/dms/MessageOverlays.tsx:167 msgid "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants." msgstr "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants." @@ -1710,7 +1758,7 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:1541 +#: src/view/com/composer/Composer.tsx:1523 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1758,7 +1806,7 @@ msgstr "Automated account" msgid "Automation label" msgstr "Automation label" -#: src/Navigation.tsx:404 +#: src/Navigation.tsx:406 #: src/screens/Settings/AutomationLabelSettings.tsx:103 msgid "Automation Label" msgstr "Automation Label" @@ -1776,9 +1824,9 @@ msgstr "" #: src/components/dms/AddMembersFlow.tsx:342 #: src/components/dms/AddMembersFlow.tsx:509 #: src/components/dms/AddMembersFlow.tsx:515 -#: src/components/dms/InitiateChatFlow.tsx:508 -#: src/components/dms/InitiateChatFlow.tsx:701 -#: src/components/dms/InitiateChatFlow.tsx:708 +#: src/components/dms/InitiateChatFlow.tsx:509 +#: src/components/dms/InitiateChatFlow.tsx:702 +#: src/components/dms/InitiateChatFlow.tsx:709 #: src/components/moderation/LabelsOnMeDialog.tsx:334 #: src/components/moderation/LabelsOnMeDialog.tsx:335 #: src/screens/Login/ChooseAccountForm.tsx:98 @@ -1791,6 +1839,8 @@ msgstr "" #: src/screens/Login/SetNewPasswordForm.tsx:178 #: src/screens/Messages/components/ChatDisabled.tsx:164 #: src/screens/Messages/components/ChatDisabled.tsx:166 +#: src/screens/Messages/components/InviteLinkDialog.tsx:256 +#: src/screens/Messages/components/InviteLinkDialog.tsx:284 #: src/screens/Messages/Inbox.tsx:242 #: src/screens/Profile/Header/Shell.tsx:174 #: src/screens/Settings/components/ChangePasswordDialog.tsx:273 @@ -1918,7 +1968,7 @@ msgstr "" msgid "Block accounts" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:143 +#: src/components/dms/AfterReportDialog.tsx:148 msgid "Block and delete" msgstr "Block and delete" @@ -1942,9 +1992,9 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:221 #: src/components/dms/AfterReportConversationDialog.tsx:224 -#: src/components/dms/AfterReportDialog.tsx:149 -#: src/components/dms/AfterReportDialog.tsx:184 -#: src/components/dms/AfterReportDialog.tsx:187 +#: src/components/dms/AfterReportDialog.tsx:154 +#: src/components/dms/AfterReportDialog.tsx:189 +#: src/components/dms/AfterReportDialog.tsx:192 msgid "Block user" msgstr "" @@ -1954,7 +2004,7 @@ msgctxt "button" msgid "Block user" msgstr "Block user" -#: src/components/dms/AfterReportDialog.tsx:180 +#: src/components/dms/AfterReportDialog.tsx:185 msgid "Block user and/or delete this conversation" msgstr "" @@ -1970,7 +2020,7 @@ msgstr "" msgid "Blocked accounts" msgstr "" -#: src/Navigation.tsx:189 +#: src/Navigation.tsx:191 #: src/view/screens/ModerationBlockedAccounts.tsx:95 msgid "Blocked Accounts" msgstr "" @@ -2035,7 +2085,7 @@ msgstr "" msgid "Bluesky is more fun with friends" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:107 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:108 msgid "Bluesky is more fun with friends! Import your contacts to see who’s already here." msgstr "" @@ -2059,7 +2109,7 @@ msgstr "" msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:134 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:136 msgid "Bluesky will proactively verify notable and authentic accounts." msgstr "" @@ -2143,11 +2193,13 @@ msgstr "" msgid "Button to go back to the home timeline" msgstr "" +#. The owner (creator) of a group chat. +#. placeholder {0}: createSanitizedDisplayName( data.joinLinkPreviews[0].owner, true, moderateProfile( data.joinLinkPreviews[0].owner, moderationOpts, ).ui('displayName'), ) #. placeholder {0}: sanitizeHandle(handle, '@') #. placeholder {0}: sanitizeHandle(item.feed.creator.handle, '@') -#. placeholder {0}: sanitizeHandle(labeler.creator.handle, '@') #: src/components/LabelingServiceCard/index.tsx:62 -#: src/components/moderation/ReportDialog/index.tsx:845 +#: src/components/moderation/ReportDialog/index.tsx:847 +#: src/screens/Messages/JoinRequest.tsx:166 #: src/screens/Search/components/StarterPackCard.tsx:107 #: src/screens/Search/Explore.tsx:971 msgid "By {0}" @@ -2218,8 +2270,8 @@ msgstr "" #: src/features/liveNow/components/GoLiveDialog.tsx:254 #: src/lib/media/picker.tsx:38 #: src/screens/Deactivated.tsx:150 -#: src/screens/Messages/components/InviteLinkDialog.tsx:458 -#: src/screens/Messages/components/InviteLinkDialog.tsx:462 +#: src/screens/Messages/components/InviteLinkDialog.tsx:468 +#: src/screens/Messages/components/InviteLinkDialog.tsx:472 #: src/screens/Messages/ConversationSettings/prompts.tsx:70 #: src/screens/Messages/ConversationSettings/prompts.tsx:94 #: src/screens/Profile/Header/EditProfileDialog.tsx:215 @@ -2234,8 +2286,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:300 #: src/screens/Takendown.tsx:102 #: src/screens/Takendown.tsx:105 -#: src/view/com/composer/Composer.tsx:1619 -#: src/view/com/composer/Composer.tsx:1629 +#: src/view/com/composer/Composer.tsx:1601 +#: src/view/com/composer/Composer.tsx:1611 #: src/view/com/composer/photos/EditImageDialog.web.tsx:44 #: src/view/com/composer/photos/EditImageDialog.web.tsx:53 #: src/view/shell/desktop/LeftNav.tsx:227 @@ -2270,7 +2322,7 @@ msgstr "" msgid "Captions & alt text" msgstr "" -#: src/components/images/Gallery/index.tsx:255 +#: src/components/images/Gallery/index.tsx:263 msgid "carousel" msgstr "carousel" @@ -2303,7 +2355,7 @@ msgstr "" msgid "Change Handle" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:443 +#: src/components/moderation/ReportDialog/index.tsx:445 msgid "Change moderation service" msgstr "" @@ -2316,11 +2368,11 @@ msgstr "" msgid "Change password dialog" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:310 +#: src/components/moderation/ReportDialog/index.tsx:312 msgid "Change report category" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:388 +#: src/components/moderation/ReportDialog/index.tsx:390 msgid "Change report reason" msgstr "" @@ -2350,7 +2402,7 @@ msgid "Changes to the starter pack will not be reflected in the list after creat msgstr "" #: src/lib/hooks/useNotificationHandler.ts:133 -#: src/Navigation.tsx:483 +#: src/Navigation.tsx:485 #: src/view/shell/bottom-bar/BottomBar.tsx:229 #: src/view/shell/desktop/LeftNav.tsx:697 #: src/view/shell/Drawer.tsx:469 @@ -2367,6 +2419,11 @@ msgstr "" msgid "Chat ended" msgstr "Chat ended" +#: src/components/intents/GroupChatJoinDialog.tsx:227 +#: src/components/intents/GroupChatJoinDialog.tsx:258 +msgid "Chat invite link no longer available" +msgstr "Chat invite link no longer available" + #: src/components/dms/getSystemMessageInfo.ts:104 msgid "Chat locked" msgstr "Chat locked" @@ -2388,7 +2445,7 @@ msgstr "" msgid "Chat recipient is not followed by the sender." msgstr "Chat recipient is not followed by the sender." -#: src/Navigation.tsx:498 +#: src/Navigation.tsx:505 msgid "Chat request inbox" msgstr "" @@ -2398,7 +2455,7 @@ msgid "Chat requests" msgstr "" #: src/components/dms/ConvoMenu.tsx:84 -#: src/Navigation.tsx:493 +#: src/Navigation.tsx:500 #: src/screens/Messages/ChatList.tsx:83 #: src/screens/Messages/ChatList.tsx:87 #: src/screens/Messages/ChatList.tsx:500 @@ -2485,7 +2542,7 @@ msgstr "" msgid "Choose this color as your avatar" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:211 +#: src/screens/Messages/components/InviteLinkDialog.tsx:222 msgid "Choose who can join this group chat and how." msgstr "Choose who can join this group chat and how." @@ -2539,7 +2596,7 @@ msgstr "" msgid "click here" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:131 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:128 msgid "Click here to add people to this group chat" msgstr "Click here to add people to this group chat" @@ -2547,7 +2604,7 @@ msgstr "Click here to add people to this group chat" msgid "Click here to contact our support team" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:145 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:142 msgid "Click here to create or manage an invite link for this group chat" msgstr "Click here to create or manage an invite link for this group chat" @@ -2577,7 +2634,7 @@ msgstr "" msgid "Click here to update your email" msgstr "" -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:146 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:143 msgid "Click here to view the invite link for this group chat" msgstr "Click here to view the invite link for this group chat" @@ -2586,7 +2643,7 @@ msgstr "Click here to view the invite link for this group chat" msgid "Click to open tag menu for {0}" msgstr "" -#: src/components/dms/MessageItem.tsx:559 +#: src/components/dms/MessageItem.tsx:528 msgid "Click to retry failed message" msgstr "" @@ -2600,15 +2657,15 @@ msgstr "" #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:233 #: src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx:239 #: src/components/dialogs/LanguageSelectDialog.tsx:359 -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:159 -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:168 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:164 -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:172 -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:139 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:176 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:185 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:191 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:199 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:160 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:169 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:165 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:173 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:140 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:178 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:187 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:192 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:200 #: src/components/dialogs/SearchablePeopleList.tsx:340 #: src/components/dialogs/StarterPackDialog.tsx:187 #: src/components/dms/AddMembersFlow.tsx:367 @@ -2616,12 +2673,14 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:96 #: src/components/dms/AfterReportConversationDialog.tsx:247 #: src/components/dms/AfterReportConversationDialog.tsx:252 -#: src/components/dms/AfterReportDialog.tsx:89 #: src/components/dms/AfterReportDialog.tsx:94 -#: src/components/dms/AfterReportDialog.tsx:207 +#: src/components/dms/AfterReportDialog.tsx:99 #: src/components/dms/AfterReportDialog.tsx:212 +#: src/components/dms/AfterReportDialog.tsx:217 #: src/components/dms/EmojiPopup.android.tsx:59 -#: src/components/dms/InitiateChatFlow.tsx:533 +#: src/components/dms/InitiateChatFlow.tsx:534 +#: src/components/intents/GroupChatJoinDialog.tsx:204 +#: src/components/intents/GroupChatJoinDialog.tsx:239 #: src/components/NewskieDialog.tsx:169 #: src/components/NewskieDialog.tsx:175 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:107 @@ -2629,14 +2688,14 @@ msgstr "" #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:122 #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:128 #: src/components/verification/VerificationsDialog.tsx:146 -#: src/components/verification/VerifierDialog.tsx:146 +#: src/components/verification/VerifierDialog.tsx:147 #: src/components/WhoCanReply.tsx:236 #: src/components/WhoCanReply.tsx:243 #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:45 #: src/features/liveNow/components/EditLiveDialog.tsx:217 #: src/features/liveNow/components/EditLiveDialog.tsx:223 -#: src/screens/Messages/components/InviteLinkDialog.tsx:481 -#: src/screens/Messages/components/InviteLinkDialog.tsx:486 +#: src/screens/Messages/components/InviteLinkDialog.tsx:492 +#: src/screens/Messages/components/InviteLinkDialog.tsx:497 #: src/screens/Settings/components/ChangePasswordDialog.tsx:288 #: src/screens/Settings/components/ChangePasswordDialog.tsx:293 #: src/view/com/feeds/MissingFeed.tsx:211 @@ -2665,7 +2724,7 @@ msgstr "" #: src/components/dialogs/LanguageSelectDialog.tsx:354 #: src/components/dialogs/NotificationSettingsDialog.tsx:94 #: src/components/verification/VerificationsDialog.tsx:138 -#: src/components/verification/VerifierDialog.tsx:139 +#: src/components/verification/VerifierDialog.tsx:140 #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:36 msgid "Close dialog" msgstr "" @@ -2679,7 +2738,7 @@ msgid "Close image" msgstr "" #: src/components/Lightbox/Lightbox.web.tsx:73 -#: src/components/Lightbox/Lightbox.web.tsx:326 +#: src/components/Lightbox/Lightbox.web.tsx:330 msgid "Close image viewer" msgstr "" @@ -2690,6 +2749,10 @@ msgstr "" msgid "Close menu" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:197 +#: src/components/intents/GroupChatJoinDialog.tsx:198 +#: src/components/intents/GroupChatJoinDialog.tsx:234 +#: src/components/intents/GroupChatJoinDialog.tsx:235 #: src/components/Menu/index.tsx:361 msgid "Close this dialog" msgstr "" @@ -2702,7 +2765,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:1627 +#: src/view/com/composer/Composer.tsx:1609 msgid "Closes post composer and discards post draft" msgstr "" @@ -2732,7 +2795,7 @@ msgid "Comics" msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:46 -#: src/Navigation.tsx:338 +#: src/Navigation.tsx:340 #: src/view/screens/CommunityGuidelines.tsx:28 msgid "Community Guidelines" msgstr "" @@ -2752,7 +2815,7 @@ msgid "Compose new post" msgstr "" #. placeholder {0}: MAX_GRAPHEME_LENGTH || 0 -#: src/view/com/composer/Composer.tsx:1503 +#: src/view/com/composer/Composer.tsx:1485 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -2760,11 +2823,11 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:2464 +#: src/view/com/composer/Composer.tsx:2446 msgid "Compressing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2466 +#: src/view/com/composer/Composer.tsx:2448 msgid "Compressing video..." msgstr "" @@ -2849,7 +2912,7 @@ msgstr "" msgid "Content and media" msgstr "" -#: src/Navigation.tsx:441 +#: src/Navigation.tsx:443 msgid "Content and Media" msgstr "" @@ -2916,7 +2979,7 @@ msgid "Continue thread..." msgstr "" #: src/components/dms/AddMembersFlow.tsx:307 -#: src/components/dms/InitiateChatFlow.tsx:460 +#: src/components/dms/InitiateChatFlow.tsx:461 msgid "Continue to group name" msgstr "Continue to group name" @@ -2936,8 +2999,8 @@ msgstr "" msgid "Conversation deleted" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:144 -#: src/components/dms/AfterReportDialog.tsx:147 +#: src/components/dms/AfterReportDialog.tsx:149 +#: src/components/dms/AfterReportDialog.tsx:152 msgctxt "toast" msgid "Conversation deleted" msgstr "" @@ -2948,11 +3011,16 @@ msgctxt "toast" msgid "Conversation left" msgstr "Conversation left" +#: src/screens/Messages/JoinRequests.tsx:192 +#: src/screens/Messages/JoinRequests.tsx:224 +msgid "Conversation not found." +msgstr "Conversation not found." + #: src/screens/Settings/AboutSettings.tsx:162 msgid "Copied build version to clipboard" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:79 +#: src/components/dms/MessageContextMenu.tsx:71 #: src/components/PostControls/DiscoverDebug.tsx:36 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:272 #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:77 @@ -3034,8 +3102,8 @@ msgstr "" msgid "Copy link to starter pack" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:169 -#: src/components/dms/MessageContextMenu.tsx:173 +#: src/components/dms/MessageContextMenu.tsx:152 +#: src/components/dms/MessageContextMenu.tsx:156 msgid "Copy message text" msgstr "" @@ -3059,7 +3127,7 @@ msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:41 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:108 -#: src/Navigation.tsx:343 +#: src/Navigation.tsx:345 #: src/view/screens/CopyrightPolicy.tsx:25 msgid "Copyright Policy" msgstr "" @@ -3092,7 +3160,7 @@ msgid "Could not follow all matches. {0}" msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:158 -#: src/components/dms/AfterReportDialog.tsx:134 +#: src/components/dms/AfterReportDialog.tsx:139 #: src/components/dms/LeaveConvoPrompt.tsx:34 msgid "Could not leave chat" msgstr "" @@ -3146,7 +3214,7 @@ msgstr "" #. Text on button to create a new starter pack #: src/components/dialogs/StarterPackDialog.tsx:113 #: src/components/dialogs/StarterPackDialog.tsx:210 -#: src/components/dms/InitiateChatFlow.tsx:469 +#: src/components/dms/InitiateChatFlow.tsx:470 #: src/components/StarterPack/ProfileStarterPacks.tsx:329 msgid "Create" msgstr "" @@ -3166,7 +3234,7 @@ msgstr "" #: src/components/StarterPack/ProfileStarterPacks.tsx:207 #: src/components/StarterPack/ProfileStarterPacks.tsx:316 -#: src/Navigation.tsx:529 +#: src/Navigation.tsx:536 msgid "Create a starter pack" msgstr "" @@ -3181,6 +3249,7 @@ msgstr "" #: src/components/WelcomeModal.tsx:158 #: src/components/WelcomeModal.tsx:166 +#: src/screens/Messages/JoinRequest.tsx:300 #: src/screens/Signup/index.tsx:135 #: src/view/com/auth/SplashScreen.tsx:107 #: src/view/com/auth/SplashScreen.web.tsx:116 @@ -3213,7 +3282,7 @@ msgstr "" msgid "Create another" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:468 +#: src/components/dms/InitiateChatFlow.tsx:469 msgid "Create group chat" msgstr "Create group chat" @@ -3235,19 +3304,20 @@ msgstr "" msgid "Create moderation list" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:294 #: src/view/com/auth/SplashScreen.tsx:100 #: src/view/com/auth/SplashScreen.web.tsx:108 msgid "Create new account" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:488 +#: src/screens/Messages/ConversationSettings/index.tsx:487 msgid "Create or modify an invite link for this group chat" msgstr "Create or modify an invite link for this group chat" #. Accessibility label for button to create a moderation report for the selected option #. placeholder {0}: option.title -#: src/components/moderation/ReportDialog/index.tsx:707 -#: src/components/moderation/ReportDialog/index.tsx:752 +#: src/components/moderation/ReportDialog/index.tsx:709 +#: src/components/moderation/ReportDialog/index.tsx:754 msgid "Create report for {0}" msgstr "" @@ -3263,8 +3333,8 @@ msgstr "" #. placeholder {0}: i18n.date(appPassword.createdAt, { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', }) #. placeholder {0}: i18n.date(createdAt, { dateStyle: 'long', timeStyle: 'short', }) #. placeholder {0}: i18n.date(createdAt, { month: 'long', day: 'numeric', year: 'numeric', }) -#: src/screens/Messages/components/InviteLinkDialog.tsx:312 -#: src/screens/Messages/ConversationSettings/index.tsx:441 +#: src/screens/Messages/components/InviteLinkDialog.tsx:329 +#: src/screens/Messages/ConversationSettings/index.tsx:440 #: src/screens/Settings/AppPasswords.tsx:174 msgid "Created {0}" msgstr "" @@ -3348,7 +3418,7 @@ msgstr "" msgid "Default icons" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:224 +#: src/components/dms/MessageOverlays.tsx:168 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:803 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:275 #: src/screens/Settings/AppPasswords.tsx:213 @@ -3389,15 +3459,15 @@ msgstr "" msgid "Delete contacts" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:146 -#: src/components/dms/AfterReportDialog.tsx:190 -#: src/components/dms/AfterReportDialog.tsx:193 +#: src/components/dms/AfterReportDialog.tsx:151 +#: src/components/dms/AfterReportDialog.tsx:195 +#: src/components/dms/AfterReportDialog.tsx:198 #: src/screens/Messages/components/RequestButtons.tsx:142 #: src/screens/Messages/components/RequestButtons.tsx:144 msgid "Delete conversation" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:184 +#: src/components/dms/MessageContextMenu.tsx:167 msgid "Delete for me" msgstr "" @@ -3406,11 +3476,11 @@ msgstr "" msgid "Delete list" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:222 +#: src/components/dms/MessageOverlays.tsx:166 msgid "Delete message" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:181 +#: src/components/dms/MessageContextMenu.tsx:164 msgid "Delete message for me" msgstr "" @@ -3420,7 +3490,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:787 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:789 -#: src/view/com/composer/Composer.tsx:1515 +#: src/view/com/composer/Composer.tsx:1497 msgid "Delete post" msgstr "" @@ -3514,8 +3584,8 @@ msgstr "" msgid "Dim" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:344 -#: src/screens/Messages/components/InviteLinkDialog.tsx:349 +#: src/screens/Messages/components/InviteLinkDialog.tsx:361 +#: src/screens/Messages/components/InviteLinkDialog.tsx:366 msgid "Disable" msgstr "Disable" @@ -3541,8 +3611,8 @@ msgstr "" msgid "Disable haptic feedback" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:446 -#: src/screens/Messages/components/InviteLinkDialog.tsx:452 +#: src/screens/Messages/components/InviteLinkDialog.tsx:456 +#: src/screens/Messages/components/InviteLinkDialog.tsx:462 msgid "Disable link" msgstr "Disable link" @@ -3554,7 +3624,7 @@ msgstr "" msgid "Disable replies entirely" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:426 +#: src/screens/Messages/components/InviteLinkDialog.tsx:443 msgid "Disable this invite link?" msgstr "Disable this invite link?" @@ -3567,9 +3637,9 @@ msgstr "" #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:101 #: src/screens/Profile/Header/EditProfileDialog.tsx:79 -#: src/view/com/composer/Composer.tsx:1294 -#: src/view/com/composer/Composer.tsx:1338 -#: src/view/com/composer/Composer.tsx:1548 +#: src/view/com/composer/Composer.tsx:1276 +#: src/view/com/composer/Composer.tsx:1320 +#: src/view/com/composer/Composer.tsx:1530 #: src/view/com/composer/drafts/DraftItem.tsx:242 #: src/view/com/composer/drafts/DraftsButton.tsx:131 msgid "Discard" @@ -3580,19 +3650,19 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1292 +#: src/view/com/composer/Composer.tsx:1274 #: src/view/com/composer/drafts/DraftItem.tsx:239 #: src/view/com/composer/drafts/DraftsButton.tsx:98 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:1309 -#: src/view/com/composer/Composer.tsx:1540 +#: src/view/com/composer/Composer.tsx:1291 +#: src/view/com/composer/Composer.tsx:1522 msgid "Discard post?" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:261 -#: src/screens/Profile/components/GermButton.tsx:268 +#: src/screens/Profile/components/GermButton.tsx:262 +#: src/screens/Profile/components/GermButton.tsx:269 msgid "Disconnect Germ DM" msgstr "" @@ -3620,11 +3690,11 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/components/contacts/FindContactsBannerNUX.tsx:77 +#: src/components/contacts/FindContactsBannerNUX.tsx:78 msgid "Dismiss banner" msgstr "" -#: src/view/com/composer/Composer.tsx:2385 +#: src/view/com/composer/Composer.tsx:2367 msgid "Dismiss error" msgstr "" @@ -3714,7 +3784,7 @@ msgstr "" #: src/components/dialogs/ServerInput.tsx:237 #: src/components/dialogs/ServerInput.tsx:239 #: src/components/dms/AfterReportConversationDialog.tsx:164 -#: src/components/dms/AfterReportDialog.tsx:140 +#: src/components/dms/AfterReportDialog.tsx:145 #: src/components/forms/DateField/index.tsx:104 #: src/components/forms/DateField/index.tsx:110 #: src/lib/media/picker.tsx:37 @@ -3735,7 +3805,7 @@ msgctxt "action" msgid "Done" msgstr "" -#: src/components/dms/MessageItem.tsx:463 +#: src/components/dms/MessageItem.tsx:432 msgid "Double tap or long press the message to add a reaction" msgstr "" @@ -3760,8 +3830,8 @@ msgctxt "button" msgid "Download chat data" msgstr "Download chat data" -#: src/components/Lightbox/Lightbox.web.tsx:304 -#: src/components/Lightbox/Lightbox.web.tsx:316 +#: src/components/Lightbox/Lightbox.web.tsx:308 +#: src/components/Lightbox/Lightbox.web.tsx:320 msgid "Download image" msgstr "Download image" @@ -3778,7 +3848,7 @@ msgstr "Download profile data" msgid "Doxxing" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:119 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:120 #: src/view/com/composer/drafts/DraftsButton.tsx:70 #: src/view/com/composer/drafts/DraftsButton.tsx:79 #: src/view/com/composer/drafts/DraftsListDialog.tsx:119 @@ -3833,7 +3903,7 @@ msgstr "" msgid "Eating disorders" msgstr "" -#: src/screens/Messages/components/EditTextButton.tsx:51 +#: src/screens/Messages/components/EditTextButton.tsx:52 #: src/screens/Settings/AccountSettings.tsx:150 #: src/screens/StarterPack/StarterPackScreen.tsx:604 #: src/screens/StarterPack/Wizard/index.tsx:331 @@ -3847,7 +3917,7 @@ msgctxt "action" msgid "Edit" msgstr "" -#: src/view/com/util/UserAvatar.tsx:460 +#: src/view/com/util/UserAvatar.tsx:461 #: src/view/com/util/UserBanner.tsx:122 msgid "Edit avatar" msgstr "" @@ -3877,7 +3947,16 @@ msgstr "" msgid "Edit interests" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:326 +#: src/screens/Messages/JoinRequests.tsx:294 +msgid "Edit invite link" +msgstr "Edit invite link" + +#: src/screens/Messages/JoinRequests.tsx:300 +msgctxt "button" +msgid "Edit invite link" +msgstr "Edit invite link" + +#: src/screens/Messages/components/InviteLinkDialog.tsx:343 msgid "Edit link settings" msgstr "Edit link settings" @@ -3895,12 +3974,12 @@ msgstr "" msgid "Edit moderation list" msgstr "" -#: src/Navigation.tsx:353 +#: src/Navigation.tsx:355 #: src/view/screens/Feeds.tsx:511 msgid "Edit My Feeds" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:475 +#: src/screens/Messages/ConversationSettings/index.tsx:474 msgid "Edit name" msgstr "Edit name" @@ -3929,7 +4008,7 @@ msgstr "" msgid "Edit starter pack" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:474 +#: src/screens/Messages/ConversationSettings/index.tsx:473 msgid "Edit this group chat’s name" msgstr "Edit this group chat’s name" @@ -3941,7 +4020,7 @@ msgstr "" msgid "Edit who can reply" msgstr "" -#: src/Navigation.tsx:534 +#: src/Navigation.tsx:541 msgid "Edit your starter pack" msgstr "" @@ -4146,7 +4225,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:2484 +#: src/view/com/composer/Composer.tsx:2466 #: src/view/com/util/error/ErrorScreen.tsx:40 msgid "Error" msgstr "" @@ -4220,8 +4299,8 @@ msgstr "" msgid "Exit fullscreen" msgstr "" -#: src/components/Lightbox/chrome/Footer.tsx:51 -#: src/components/Lightbox/Lightbox.web.tsx:237 +#: src/components/Lightbox/chrome/Footer.tsx:59 +#: src/components/Lightbox/Lightbox.web.tsx:241 msgid "Expand alt text" msgstr "" @@ -4241,7 +4320,7 @@ msgstr "" msgid "Expands or collapses post text" msgstr "" -#: src/lib/api/index.ts:460 +#: src/lib/api/index.ts:441 msgid "Expected uri to resolve to a record" msgstr "" @@ -4280,7 +4359,7 @@ msgstr "" msgid "Explicit sexual images." msgstr "" -#: src/Navigation.tsx:738 +#: src/Navigation.tsx:745 #: src/screens/Search/Shell.tsx:362 #: src/view/shell/desktop/LeftNav.tsx:676 #: src/view/shell/Drawer.tsx:417 @@ -4322,7 +4401,7 @@ msgstr "" msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." msgstr "" -#: src/Navigation.tsx:372 +#: src/Navigation.tsx:374 #: src/screens/Settings/ExternalMediaPreferences.tsx:35 msgid "External Media Preferences" msgstr "" @@ -4336,8 +4415,12 @@ msgctxt "toast" msgid "Failed to accept chat" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:186 +msgid "Failed to accept join request" +msgstr "Failed to accept join request" + #: src/components/dms/ActionsWrapper.web.tsx:77 -#: src/components/dms/MessageContextMenu.tsx:119 +#: src/components/dms/MessageContextMenu.tsx:103 msgid "Failed to add emoji reaction" msgstr "" @@ -4354,7 +4437,7 @@ msgstr "" msgid "Failed to change handle. Please try again." msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:294 +#: src/components/Lightbox/Lightbox.web.tsx:298 msgid "Failed to copy link" msgstr "Failed to copy link" @@ -4382,7 +4465,7 @@ msgctxt "toast" msgid "Failed to delete chat" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:101 +#: src/components/dms/MessageOverlays.tsx:112 msgid "Failed to delete message" msgstr "" @@ -4399,11 +4482,11 @@ msgid "Failed to disable invite link" msgstr "Failed to disable invite link" #. placeholder {0}: error?.message -#: src/screens/Profile/components/GermButton.tsx:195 +#: src/screens/Profile/components/GermButton.tsx:196 msgid "Failed to disconnect Germ DM. Error: {0}" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:345 +#: src/screens/Messages/ConversationSettings/index.tsx:344 msgid "Failed to edit group chat name" msgstr "Failed to edit group chat name" @@ -4427,13 +4510,21 @@ msgstr "" msgid "Failed to hide suggestion, please check your internet connection" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:218 +msgid "Failed to ignore join request" +msgstr "Failed to ignore join request" + +#: src/components/intents/GroupChatJoinDialog.tsx:116 +msgid "Failed to join the group chat. Please try again." +msgstr "Failed to join the group chat. Please try again." + #: src/components/contacts/screens/ViewMatches.tsx:582 msgid "Failed to launch SMS app" msgstr "" #: src/screens/Messages/components/ChatEnded.tsx:42 #: src/screens/Messages/components/ChatLocked.tsx:53 -#: src/screens/Messages/ConversationSettings/index.tsx:372 +#: src/screens/Messages/ConversationSettings/index.tsx:371 msgctxt "toast" msgid "Failed to leave group chat" msgstr "Failed to leave group chat" @@ -4484,7 +4575,7 @@ msgstr "" msgid "Failed to load suggested follows" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:393 +#: src/screens/Messages/ConversationSettings/index.tsx:392 msgid "Failed to lock group chat" msgstr "Failed to lock group chat" @@ -4493,7 +4584,7 @@ msgstr "Failed to lock group chat" msgid "Failed to mark all requests as read" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:359 +#: src/screens/Messages/ConversationSettings/index.tsx:358 msgid "Failed to mute group chat" msgstr "Failed to mute group chat" @@ -4502,7 +4593,7 @@ msgid "Failed to pin post" msgstr "" #. placeholder {0}: e?.message -#: src/screens/Profile/components/GermButton.tsx:163 +#: src/screens/Profile/components/GermButton.tsx:164 msgid "Failed to reconnect Germ DM. Error: {0}" msgstr "" @@ -4516,8 +4607,8 @@ msgid "Failed to remove data. {0}" msgstr "" #: src/components/dms/ActionsWrapper.web.tsx:73 -#: src/components/dms/MessageContextMenu.tsx:115 -#: src/components/dms/ReactionsDialog.tsx:171 +#: src/components/dms/MessageContextMenu.tsx:99 +#: src/components/dms/ReactionsDialog.tsx:175 msgid "Failed to remove emoji reaction" msgstr "" @@ -4533,15 +4624,19 @@ msgstr "Failed to remove group chat member" msgid "Failed to remove verification" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:150 +msgid "Failed to rescind your request. Please try again." +msgstr "Failed to rescind your request. Please try again." + #: src/components/dialogs/DeviceLocationRequestDialog.tsx:81 msgid "Failed to resolve location. Please try again." msgstr "" -#: src/view/com/composer/Composer.tsx:584 +#: src/view/com/composer/Composer.tsx:578 msgid "Failed to save draft" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:311 +#: src/components/Lightbox/Lightbox.web.tsx:315 msgid "Failed to save image" msgstr "Failed to save image" @@ -4580,7 +4675,7 @@ msgid "Failed to toggle thread mute, please try again" msgstr "" #: src/screens/Messages/components/ChatLocked.tsx:42 -#: src/screens/Messages/ConversationSettings/index.tsx:396 +#: src/screens/Messages/ConversationSettings/index.tsx:395 msgid "Failed to unlock group chat" msgstr "Failed to unlock group chat" @@ -4632,7 +4727,7 @@ msgstr "" msgid "False information about elections" msgstr "" -#: src/Navigation.tsx:288 +#: src/Navigation.tsx:290 msgid "Feed" msgstr "" @@ -4677,7 +4772,7 @@ msgctxt "toast" msgid "Feedback sent to feed operator" msgstr "" -#: src/Navigation.tsx:514 +#: src/Navigation.tsx:521 #: src/screens/SavedFeeds.tsx:112 #: src/screens/SavedFeeds.tsx:303 #: src/screens/Search/SearchResults.tsx:80 @@ -4738,11 +4833,11 @@ msgstr "" msgid "Finalizing" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:145 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:146 msgid "Finally!" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:155 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:156 msgid "Finally! Keep track of posts that matter to you. Save them to revisit anytime." msgstr "" @@ -4759,8 +4854,8 @@ msgstr "" msgid "Find accounts to follow" msgstr "" -#: src/Navigation.tsx:428 -#: src/Navigation.tsx:556 +#: src/Navigation.tsx:430 +#: src/Navigation.tsx:563 msgid "Find Contacts" msgstr "" @@ -4790,7 +4885,7 @@ msgstr "" msgid "Find posts, users, and feeds on Bluesky" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:97 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:98 msgid "Find your friends" msgstr "" @@ -4935,8 +5030,12 @@ msgstr "" msgid "Followed by <0>{0}, <1>{1}, and {2, plural, one {# other} other {# others}}" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:321 +msgid "Followers can join" +msgstr "Followers can join" + #. placeholder {0}: route.params.name -#: src/Navigation.tsx:242 +#: src/Navigation.tsx:244 msgid "Followers of @{0} that you know" msgstr "" @@ -4986,7 +5085,7 @@ msgstr "" msgid "Following feed preferences" msgstr "" -#: src/Navigation.tsx:359 +#: src/Navigation.tsx:361 #: src/screens/Settings/FollowingFeedPreferences.tsx:57 msgid "Following Feed Preferences" msgstr "" @@ -5077,35 +5176,35 @@ msgstr "" msgid "Generate a starter pack" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:206 -#: src/screens/Messages/components/InviteLinkDialog.tsx:244 -#: src/screens/Messages/components/InviteLinkDialog.tsx:268 +#: src/screens/Messages/components/InviteLinkDialog.tsx:217 +#: src/screens/Messages/components/InviteLinkDialog.tsx:257 +#: src/screens/Messages/components/InviteLinkDialog.tsx:285 msgid "Generate invite link" msgstr "Generate invite link" -#: src/screens/Messages/components/InviteLinkDialog.tsx:397 +#: src/screens/Messages/components/InviteLinkDialog.tsx:414 msgid "Generate new invite link" msgstr "Generate new invite link" -#: src/screens/Messages/components/InviteLinkDialog.tsx:402 +#: src/screens/Messages/components/InviteLinkDialog.tsx:419 msgid "Generate new link" msgstr "Generate new link" #: src/screens/Profile/components/GermButton.tsx:86 -#: src/screens/Profile/components/GermButton.tsx:224 +#: src/screens/Profile/components/GermButton.tsx:225 msgid "Germ DM" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:182 +#: src/screens/Profile/components/GermButton.tsx:183 msgid "Germ DM disconnected" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:233 -#: src/screens/Profile/components/GermButton.tsx:238 +#: src/screens/Profile/components/GermButton.tsx:234 +#: src/screens/Profile/components/GermButton.tsx:239 msgid "Germ DM Link" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:159 +#: src/screens/Profile/components/GermButton.tsx:160 msgid "Germ DM reconnected" msgstr "" @@ -5169,23 +5268,23 @@ msgstr "" msgid "Get notified when {name} posts" msgstr "" -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:138 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:139 msgid "Get notified when someone posts" msgstr "" #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:71 #: src/components/dialogs/EmailDialog/screens/VerificationReminder.tsx:81 -#: src/screens/Messages/components/InviteLinkDialog.tsx:187 -#: src/screens/Messages/components/InviteLinkDialog.tsx:194 +#: src/screens/Messages/components/InviteLinkDialog.tsx:197 +#: src/screens/Messages/components/InviteLinkDialog.tsx:204 msgid "Get started" msgstr "" -#: src/components/MediaPreview.tsx:136 +#: src/components/MediaPreview.tsx:148 #: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:69 msgid "GIF" msgstr "" -#: src/view/com/composer/Composer.tsx:2489 +#: src/view/com/composer/Composer.tsx:2471 msgid "GIF uploaded" msgstr "" @@ -5208,7 +5307,7 @@ msgstr "" #: src/screens/VideoFeed/components/Header.tsx:163 #: src/screens/VideoFeed/index.tsx:1168 #: src/screens/VideoFeed/index.tsx:1172 -#: src/view/com/auth/LoggedOut.tsx:89 +#: src/view/com/auth/LoggedOut.tsx:103 #: src/view/com/profile/ProfileFollowers.tsx:178 #: src/view/com/profile/ProfileFollowers.tsx:179 #: src/view/screens/NotFound.tsx:51 @@ -5314,8 +5413,8 @@ msgstr "Go to user’s profile" msgid "Going live is currently disabled for your account" msgstr "" -#: src/screens/Profile/components/GermButton.tsx:252 -#: src/screens/Profile/components/GermButton.tsx:257 +#: src/screens/Profile/components/GermButton.tsx:253 +#: src/screens/Profile/components/GermButton.tsx:258 msgid "Got it" msgstr "" @@ -5334,37 +5433,42 @@ msgstr "" msgid "Grooming or predatory behavior" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:513 +#: src/components/intents/GroupChatJoinDialog.tsx:297 +#: src/screens/Messages/JoinRequest.tsx:116 +msgid "Group chat" +msgstr "Group chat" + +#: src/screens/Messages/components/InviteLinkDialog.tsx:522 msgid "Group chat invite link dialog" msgstr "Group chat invite link dialog" -#: src/screens/Messages/ConversationSettings/index.tsx:385 +#: src/screens/Messages/ConversationSettings/index.tsx:384 msgctxt "toast" msgid "Group chat locked" msgstr "Group chat locked" -#: src/screens/Messages/ConversationSettings/index.tsx:352 +#: src/screens/Messages/ConversationSettings/index.tsx:351 msgctxt "toast" msgid "Group chat muted" msgstr "Group chat muted" -#: src/screens/Messages/ConversationSettings/index.tsx:340 +#: src/screens/Messages/ConversationSettings/index.tsx:339 msgctxt "toast" msgid "Group chat name updated" msgstr "Group chat name updated" -#: src/Navigation.tsx:488 +#: src/Navigation.tsx:490 #: src/screens/Messages/ConversationSettings/index.tsx:91 msgid "Group chat settings" msgstr "Group chat settings" #: src/screens/Messages/components/ChatLocked.tsx:38 -#: src/screens/Messages/ConversationSettings/index.tsx:387 +#: src/screens/Messages/ConversationSettings/index.tsx:386 msgctxt "toast" msgid "Group chat unlocked" msgstr "Group chat unlocked" -#: src/screens/Messages/ConversationSettings/index.tsx:354 +#: src/screens/Messages/ConversationSettings/index.tsx:353 msgctxt "toast" msgid "Group chat unmuted" msgstr "Group chat unmuted" @@ -5377,12 +5481,17 @@ msgstr "Group chats are not yet available" msgid "Group chats are now available" msgstr "Group chats are now available" +#. placeholder {0}: import {useState} from 'react' import {View} from 'react-native' import { type ChatBskyGroupDefs, moderateProfile, type ModerationOpts, } from '@atproto/api' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {shareUrl} from '#/lib/sharing' import {useCreateJoinLink} from '#/state/queries/messages/create-join-link' import {useDisableJoinLink} from '#/state/queries/messages/disable-join-link' import {useEditJoinLink} from '#/state/queries/messages/edit-join-link' import {useEnableJoinLink} from '#/state/queries/messages/enable-join-link' import {atoms as a, useTheme, web} from '#/alf' import { Button, ButtonIcon, ButtonText, StackedButton, } from '#/components/Button' import * as Dialog from '#/components/Dialog' import { type ConvoWithDetails, type GroupConvoMember, } from '#/components/dms/util' import * as Toggle from '#/components/forms/Toggle' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight' import {ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon} from '#/components/icons/ChainLink' import {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/EditBig' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' import {CopyTextButton} from './CopyTextButton' import {EditTextButton} from './EditTextButton' enum Step { INFO, GENERATE, MANAGE, CONFIRM_DISABLE, } export function InviteLinkDialog({ convo, control, owner, isOwner, moderationOpts, }: { convo: Extract control: Dialog.DialogOuterProps['control'] owner: GroupConvoMember isOwner: boolean moderationOpts: ModerationOpts }) { const t = useTheme() const {t: l, i18n} = useLingui() const ownerName = createSanitizedDisplayName( owner, false, moderateProfile(owner, moderationOpts).ui('displayName'), ) const {joinLink} = convo.details const defaultStep = joinLink ? Step.MANAGE : Step.INFO const defaultWhoCanJoin = joinLink ? joinLinkToKey(joinLink) : 'anyone' const [step, setStep] = useState(defaultStep) const [whoCanJoin, setWhoCanJoin] = useState(defaultWhoCanJoin) // Resync local state when the server-side join link rules change (mutation // success, refetch, or change from another client). Keyed on the rule string // so identity-only refetches don't bump a user mid-edit. const joinLinkRuleKey = joinLink ? joinLinkToKey(joinLink) : null const [prevKey, setPrevKey] = useState(joinLinkRuleKey) if (joinLinkRuleKey !== prevKey) { setStep(joinLinkRuleKey ? Step.MANAGE : Step.INFO) setWhoCanJoin(joinLinkRuleKey ?? 'anyone') setPrevKey(joinLinkRuleKey) } const {openComposer} = useOpenComposer() const {mutate: createJoinLink, isPending: isCreating} = useCreateJoinLink( convo.view.id, { onSuccess: () => { setStep(Step.MANAGE) }, onError: () => { Toast.show(l`Failed to create invite link`, { type: 'error', }) }, }, ) const {mutate: editJoinLink, isPending: isEditing} = useEditJoinLink( convo.view.id, { onSuccess: () => { setStep(Step.MANAGE) }, onError: () => { Toast.show(l`Failed to edit invite link`, { type: 'error', }) }, }, ) const {mutate: disableJoinLink, isPending: isDisabling} = useDisableJoinLink( convo.view.id, { onError: () => { Toast.show(l`Failed to disable invite link`, { type: 'error', }) }, }, ) const {mutate: enableJoinLink, isPending: isEnabling} = useEnableJoinLink( convo.view.id, { onError: () => { Toast.show(l`Failed to enable invite link`, { type: 'error', }) }, }, ) const isSaving = isCreating || isEditing const whoCanJoinOptions = [ { name: 'anyone', owner: l`Anyone can join instantly`, member: l`Anyone can join instantly`, }, { name: 'anyone:requireApproval', owner: l`Anyone can request to join`, member: l`Anyone can request to join`, }, { name: 'followedByOwner', owner: l`People I follow can join instantly`, member: l`People ${ownerName} follows can join instantly`, }, { name: 'followedByOwner:requireApproval', owner: l`People I follow can request to join`, member: l`People ${ownerName} follows can request to join`, }, ] let content: React.ReactNode = null let header: string | null = null switch (step) { case Step.INFO: { header = l`Invite link` content = ( <> An invite link lets people join this group chat without being added directly. You control who can join the chat. You can disable the link at any time. Group chats can only have a maximum of{' '} {plural(convo.details.memberLimit, { one: '# person', other: '# people', })} . Your name, avatar, the name of the group chat, and the number of members will be visible to everyone. ) break } case Step.GENERATE: { const linkEnabled = joinLink?.enabledStatus === 'enabled' const linkHasChanged = linkEnabled && joinLinkRuleKey !== whoCanJoin header = linkEnabled ? l`Update invite link` : l`Generate invite link` content = ( <> Choose who can join this group chat and how. setWhoCanJoin(value)}> {whoCanJoinOptions.map(option => ( {({selected}) => ( )} ))} ) break } case Step.MANAGE: { const linkEnabled = joinLink?.enabledStatus === 'enabled' const linkDisabled = joinLink?.enabledStatus === 'disabled' const joinLinkURI = joinLink?.code ? `https://bsky.app/c/${joinLink.code}` : 'https://bsky.app/' const createdAt = joinLink ? new Date(joinLink.createdAt) : null const currentOption = whoCanJoinOptions.find( o => o.name === (joinLink ? joinLinkToKey(joinLink) : null), ) ?? whoCanJoinOptions[0] const ownerValue = currentOption?.owner ?? whoCanJoinOptions[0].owner const memberValue = currentOption?.member ?? whoCanJoinOptions[0].member header = linkEnabled ? l`Invite link` : l`Invite link disabled` content = ( <> {joinLinkURI} {createdAt ? ( Created{' '} {i18n.date(createdAt, { dateStyle: 'long', timeStyle: 'short', })} ) : null} {linkEnabled ? ( {isOwner ? ( setStep(Step.GENERATE)}> {ownerValue} ) : ( {memberValue} )} ) : null} {linkEnabled ? ( {isOwner ? ( setStep(Step.CONFIRM_DISABLE)}> Disable ) : null} { control.close(() => { openComposer({ text: joinLinkURI, logContext: 'Other', }) }) }}> Post link { void shareUrl(joinLinkURI) }}> Share ) : ( )} ) break } case Step.CONFIRM_DISABLE: { content = ( <> Disable this invite link? Anyone who has it will no longer be able to join or request to join. You can always create a new one. ) break } } if (!isOwner && (!joinLink || joinLink.enabledStatus === 'disabled')) { header = l`Invite link` content = ( <> There is no invite link for this group chat. ) } return ( { setStep(defaultStep) setWhoCanJoin(defaultWhoCanJoin) }}> {header} } label={l`Group chat invite link dialog`} style={web({maxWidth: 400})}> {content} ) } function joinLinkToKey(joinLink: ChatBskyGroupDefs.JoinLinkView): string { return `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}` } function keyToJoinLink( key: string, ): Pick { const [joinRule, requireApproval] = key.split(':') return { joinRule, requireApproval: requireApproval === 'requireApproval', } } +#: src/screens/Messages/components/InviteLinkDialog.tsx:179 +msgid "Group chats can only have a maximum of {0}." +msgstr "Group chats can only have a maximum of {0}." + #: src/components/dialogs/SearchablePeopleList.tsx:569 msgid "Group is locked" msgstr "Group is locked" #: src/components/dms/InitiateChatFlow.tsx:237 -#: src/components/dms/InitiateChatFlow.tsx:568 +#: src/components/dms/InitiateChatFlow.tsx:569 #: src/screens/Messages/ConversationSettings/prompts.tsx:33 msgid "Group name" msgstr "Group name" @@ -5415,7 +5524,7 @@ msgid "Handle too long. Please try a shorter one." msgstr "" #: src/components/FeedCard.tsx:156 -#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:122 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:123 msgid "Happening now" msgstr "" @@ -5440,7 +5549,7 @@ msgstr "" msgid "Harming or endangering minors" msgstr "" -#: src/Navigation.tsx:472 +#: src/Navigation.tsx:474 msgid "Hashtag" msgstr "" @@ -5646,7 +5755,7 @@ msgstr "" msgid "Hmmmm, we couldn't load that moderation service." msgstr "" -#: src/view/com/composer/state/video.ts:430 +#: src/view/com/composer/state/video.ts:415 msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!" msgstr "" @@ -5654,8 +5763,8 @@ msgstr "" msgid "Hold your horses! This feature isn't available to you yet. Please check back later." msgstr "Hold your horses! This feature isn't available to you yet. Please check back later." -#: src/Navigation.tsx:733 -#: src/Navigation.tsx:754 +#: src/Navigation.tsx:740 +#: src/Navigation.tsx:761 #: src/view/shell/bottom-bar/BottomBar.tsx:184 #: src/view/shell/desktop/LeftNav.tsx:666 #: src/view/shell/Drawer.tsx:443 @@ -5715,7 +5824,7 @@ msgstr "" msgid "I'm on Bluesky as {0} - come find me! https://bsky.app/download" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:238 +#: src/components/Lightbox/Lightbox.web.tsx:242 msgid "If alt text is long, toggles alt text expanded state" msgstr "" @@ -5779,23 +5888,33 @@ msgstr "" msgid "If you're trying to change your handle or email, do so before you deactivate." msgstr "" -#: src/components/images/ImageLayoutGridItem.tsx:76 +#. Ignore a request to join a chat +#: src/screens/Messages/JoinRequests.tsx:485 +msgctxt "button" +msgid "Ignore" +msgstr "Ignore" + +#: src/screens/Messages/JoinRequests.tsx:479 +msgid "Ignore join request" +msgstr "Ignore join request" + +#: src/components/images/ImageLayoutGridItem.tsx:93 msgid "Image" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:427 +#: src/components/images/Gallery/index.tsx:443 msgid "Image {0}" msgstr "Image {0}" #. placeholder {0}: index + 1 #. placeholder {1}: imgs.length -#: src/components/Lightbox/Lightbox.web.tsx:253 +#: src/components/Lightbox/Lightbox.web.tsx:257 msgid "Image {0} of {1}" msgstr "" #. placeholder {0}: index + 1 -#: src/components/images/Gallery/index.tsx:418 +#: src/components/images/Gallery/index.tsx:428 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5810,27 +5929,27 @@ msgid "Image cache cleared, freed {0}" msgstr "" #. placeholder {0}: images.length -#: src/components/images/Gallery/index.tsx:256 +#: src/components/images/Gallery/index.tsx:264 msgid "Image gallery, {0} images" msgstr "Image gallery, {0} images" #. Image has been moderated and user has the option of showing it temporarily -#: src/features/liveNow/components/LiveStatusDialog.tsx:299 +#: src/features/liveNow/components/LiveStatusDialog.tsx:300 msgid "Image is hidden due to your moderation settings." msgstr "Image is hidden due to your moderation settings." #. Image has been moderated and is not visible to the user -#: src/features/liveNow/components/LiveStatusDialog.tsx:309 +#: src/features/liveNow/components/LiveStatusDialog.tsx:310 msgid "Image is unavailable." msgstr "Image is unavailable." #: src/components/Lightbox/chrome/ImageMenu.tsx:73 -#: src/components/Lightbox/Lightbox.web.tsx:257 -#: src/components/Lightbox/Lightbox.web.tsx:265 +#: src/components/Lightbox/Lightbox.web.tsx:261 +#: src/components/Lightbox/Lightbox.web.tsx:269 msgid "Image options" msgstr "Image options" -#: src/components/Lightbox/Lightbox.web.tsx:308 +#: src/components/Lightbox/Lightbox.web.tsx:312 #: src/lib/media/save-image.ios.ts:25 #: src/lib/media/save-image.ts:29 msgid "Image saved" @@ -5855,13 +5974,13 @@ msgstr "" msgid "Import contacts" msgstr "" -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:115 -#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:126 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:116 +#: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:127 msgid "Import Contacts" msgstr "" #: src/components/contacts/FindContactsBannerNUX.tsx:33 -#: src/components/contacts/FindContactsBannerNUX.tsx:71 +#: src/components/contacts/FindContactsBannerNUX.tsx:72 msgid "Import contacts to find your friends" msgstr "" @@ -5956,6 +6075,10 @@ msgstr "" msgid "Invalid 2FA confirmation code." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:126 +msgid "Invalid group chat code." +msgstr "Invalid group chat code." + #: src/screens/Settings/components/ChangeHandleDialog.tsx:615 msgid "Invalid handle. Please try a different one." msgstr "" @@ -5970,10 +6093,14 @@ msgstr "" msgid "Invalid phone number" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:98 +#: src/components/moderation/ReportDialog/index.tsx:100 msgid "Invalid report subject" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:157 +msgid "Invalid rescind request." +msgstr "Invalid rescind request." + #: src/components/intents/VerifyEmailIntentDialog.tsx:86 msgid "Invalid Verification Code" msgstr "" @@ -6004,11 +6131,11 @@ msgid "Invite friends <0/>" msgstr "" #: src/screens/Messages/components/InviteLinkDialog.tsx:167 -#: src/screens/Messages/components/InviteLinkDialog.tsx:290 -#: src/screens/Messages/components/InviteLinkDialog.tsx:296 -#: src/screens/Messages/components/InviteLinkDialog.tsx:471 -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:151 -#: src/screens/Messages/ConversationSettings/index.tsx:491 +#: src/screens/Messages/components/InviteLinkDialog.tsx:309 +#: src/screens/Messages/components/InviteLinkDialog.tsx:315 +#: src/screens/Messages/components/InviteLinkDialog.tsx:482 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:148 +#: src/screens/Messages/ConversationSettings/index.tsx:490 msgid "Invite link" msgstr "Invite link" @@ -6017,7 +6144,7 @@ msgid "Invite link created" msgstr "Invite link created" #: src/components/dms/getSystemMessageInfo.ts:138 -#: src/screens/Messages/components/InviteLinkDialog.tsx:290 +#: src/screens/Messages/components/InviteLinkDialog.tsx:309 msgid "Invite link disabled" msgstr "Invite link disabled" @@ -6067,7 +6194,7 @@ msgid "It's just you right now! Add more people to your starter pack by searchin msgstr "" #. placeholder {0}: videoState.jobId -#: src/view/com/composer/Composer.tsx:2404 +#: src/view/com/composer/Composer.tsx:2386 msgid "Job ID: {0}" msgstr "" @@ -6076,6 +6203,10 @@ msgstr "" msgid "Jobs" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:253 +msgid "Join" +msgstr "Join" + #: src/screens/StarterPack/StarterPackLandingScreen.tsx:210 #: src/screens/StarterPack/StarterPackLandingScreen.tsx:215 #: src/screens/StarterPack/StarterPackScreen.tsx:478 @@ -6083,17 +6214,30 @@ msgstr "" msgid "Join Bluesky" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:63 +#: src/components/intents/GroupChatJoinDialog.tsx:415 +msgid "Join group chat" +msgstr "Join group chat" + +#: src/components/intents/GroupChatJoinDialog.tsx:146 +msgid "Join request rescinded." +msgstr "Join request rescinded." + #: src/components/StarterPack/QrCode.tsx:72 #: src/view/shell/NavSignupCard.tsx:41 msgid "Join the conversation" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:420 +msgid "Join this group chat" +msgstr "Join this group chat" + #: src/lib/interests.ts:63 msgid "Journalism" msgstr "" -#: src/view/com/composer/Composer.tsx:1342 -#: src/view/com/composer/Composer.tsx:1352 +#: src/view/com/composer/Composer.tsx:1324 +#: src/view/com/composer/Composer.tsx:1334 #: src/view/com/composer/drafts/DraftsButton.tsx:135 msgid "Keep editing" msgstr "" @@ -6136,7 +6280,7 @@ msgstr "" msgid "Labels on your content" msgstr "" -#: src/Navigation.tsx:215 +#: src/Navigation.tsx:217 msgid "Language Settings" msgstr "" @@ -6167,7 +6311,7 @@ msgid "Latest" msgstr "" #: src/components/verification/VerificationsDialog.tsx:168 -#: src/components/verification/VerifierDialog.tsx:135 +#: src/components/verification/VerifierDialog.tsx:136 #: src/screens/Moderation/VerificationSettings.tsx:50 #: src/screens/Profile/Header/EditProfileDialog.tsx:346 #: src/screens/Settings/components/ChangeHandleDialog.tsx:215 @@ -6222,7 +6366,7 @@ msgid "Learn more about this warning" msgstr "" #: src/components/verification/VerificationsDialog.tsx:153 -#: src/components/verification/VerifierDialog.tsx:121 +#: src/components/verification/VerifierDialog.tsx:122 msgctxt "english-only-resource" msgid "Learn more about verification on Bluesky" msgstr "" @@ -6232,7 +6376,7 @@ msgstr "" msgid "Learn more about what is public on Bluesky." msgstr "" -#: src/screens/Profile/components/GermButton.tsx:211 +#: src/screens/Profile/components/GermButton.tsx:212 msgid "Learn more about your Germ DM link" msgstr "" @@ -6246,7 +6390,7 @@ msgid "Learn more." msgstr "" #: src/components/dms/LeaveConvoPrompt.tsx:49 -#: src/screens/Messages/ConversationSettings/index.tsx:527 +#: src/screens/Messages/ConversationSettings/index.tsx:526 msgid "Leave" msgstr "" @@ -6283,7 +6427,7 @@ msgstr "Leave conversation" msgid "Leave group chat" msgstr "Leave group chat" -#: src/screens/Messages/ConversationSettings/index.tsx:526 +#: src/screens/Messages/ConversationSettings/index.tsx:525 msgid "Leave this group chat" msgstr "Leave this group chat" @@ -6347,8 +6491,8 @@ msgstr "" msgid "Like this labeler" msgstr "" -#: src/Navigation.tsx:293 -#: src/Navigation.tsx:298 +#: src/Navigation.tsx:295 +#: src/Navigation.tsx:300 msgid "Liked by" msgstr "" @@ -6394,11 +6538,11 @@ msgstr "" msgid "Linear" msgstr "" -#: src/components/Lightbox/Lightbox.web.tsx:292 +#: src/components/Lightbox/Lightbox.web.tsx:296 msgid "Link copied to clipboard" msgstr "Link copied to clipboard" -#: src/Navigation.tsx:248 +#: src/Navigation.tsx:250 msgid "List" msgstr "" @@ -6484,7 +6628,7 @@ msgctxt "toast" msgid "List unmuted" msgstr "" -#: src/Navigation.tsx:169 +#: src/Navigation.tsx:171 #: src/view/screens/Lists.tsx:60 #: src/view/screens/Profile.tsx:233 #: src/view/screens/Profile.tsx:241 @@ -6530,7 +6674,7 @@ msgstr "" msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface." msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:244 +#: src/features/liveNow/components/LiveStatusDialog.tsx:245 msgid "Live feature is in beta" msgstr "" @@ -6580,7 +6724,7 @@ msgstr "" msgid "Loading..." msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:505 +#: src/screens/Messages/ConversationSettings/index.tsx:504 msgid "Lock" msgstr "Lock" @@ -6592,15 +6736,15 @@ msgstr "Lock group chat" msgid "Lock group chat?" msgstr "Lock group chat?" -#: src/screens/Messages/ConversationSettings/index.tsx:503 +#: src/screens/Messages/ConversationSettings/index.tsx:502 msgid "Lock this group chat" msgstr "Lock this group chat" -#: src/screens/Messages/ConversationSettings/index.tsx:505 +#: src/screens/Messages/ConversationSettings/index.tsx:504 msgid "Locked" msgstr "Locked" -#: src/Navigation.tsx:318 +#: src/Navigation.tsx:320 msgid "Log" msgstr "" @@ -6712,7 +6856,7 @@ msgid "Media that may be disturbing or inappropriate for some audiences." msgstr "" #. The heading above the list of chat members. -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:29 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:34 msgid "Members" msgstr "Members" @@ -6760,18 +6904,18 @@ msgstr "Message {displayName}" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:100 +#: src/components/dms/MessageOverlays.tsx:111 msgctxt "toast" msgid "Message deleted" msgstr "" -#: src/components/dms/MessageItem.tsx:553 +#: src/components/dms/MessageItem.tsx:522 msgid "Message failed to send." msgstr "Message failed to send." #. placeholder {0}: sender?.handle ?? 'unknown' #. placeholder {1}: message.text -#: src/components/dms/MessageContextMenu.tsx:146 +#: src/components/dms/MessageContextMenu.tsx:129 msgid "Message from @{0}: {1}" msgstr "" @@ -6794,19 +6938,19 @@ msgstr "" msgid "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" -#: src/components/dms/MessageContextMenu.tsx:145 +#: src/components/dms/MessageContextMenu.tsx:128 msgid "Message options" msgstr "" -#: src/Navigation.tsx:748 +#: src/Navigation.tsx:755 msgid "Messages" msgstr "" -#: src/components/dms/MessageItem.tsx:652 +#: src/components/dms/MessageItem.tsx:621 msgid "Messages from this person are hidden while they are blocking you." msgstr "Messages from this person are hidden while they are blocking you." -#: src/components/dms/MessageItem.tsx:647 +#: src/components/dms/MessageItem.tsx:616 msgid "Messages from this person are hidden while you are blocking them." msgstr "Messages from this person are hidden while you are blocking them." @@ -6827,7 +6971,7 @@ msgstr "" msgid "Missing media" msgstr "" -#: src/Navigation.tsx:174 +#: src/Navigation.tsx:176 #: src/screens/Moderation/index.tsx:100 msgid "Moderation" msgstr "" @@ -6871,7 +7015,7 @@ msgstr "" msgid "Moderation lists" msgstr "" -#: src/Navigation.tsx:179 +#: src/Navigation.tsx:181 #: src/view/screens/ModerationModlists.tsx:60 msgid "Moderation Lists" msgstr "" @@ -6880,7 +7024,7 @@ msgstr "" msgid "moderation settings" msgstr "" -#: src/Navigation.tsx:308 +#: src/Navigation.tsx:310 msgid "Moderation states" msgstr "" @@ -6926,7 +7070,7 @@ msgstr "" msgid "Music" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:467 +#: src/screens/Messages/ConversationSettings/index.tsx:466 msgid "Mute" msgstr "Mute" @@ -6971,7 +7115,7 @@ msgstr "" msgid "Mute these accounts?" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:465 +#: src/screens/Messages/ConversationSettings/index.tsx:464 msgid "Mute this group chat" msgstr "Mute this group chat" @@ -7009,7 +7153,7 @@ msgstr "" msgid "Mute words & tags" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:467 +#: src/screens/Messages/ConversationSettings/index.tsx:466 msgid "Muted" msgstr "Muted" @@ -7017,7 +7161,7 @@ msgstr "Muted" msgid "Muted accounts" msgstr "" -#: src/Navigation.tsx:184 +#: src/Navigation.tsx:186 #: src/view/screens/ModerationMutedAccounts.tsx:107 msgid "Muted Accounts" msgstr "" @@ -7085,8 +7229,8 @@ msgstr "" msgid "Navigates to your profile" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:340 -#: src/components/moderation/ReportDialog/index.tsx:356 +#: src/components/moderation/ReportDialog/index.tsx:342 +#: src/components/moderation/ReportDialog/index.tsx:358 msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?" msgstr "" @@ -7124,19 +7268,19 @@ msgid "New chat" msgstr "" #. placeholder {0}: createSanitizedDisplayName(members[0]) -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:61 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:58 msgid "New chat with {0}" msgstr "New chat with {0}" #. placeholder {0}: createSanitizedDisplayName(members[0]) #. placeholder {1}: createSanitizedDisplayName(members[1]) -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:65 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:62 msgid "New chat with {0} and {1}" msgstr "New chat with {0} and {1}" #. placeholder {0}: createSanitizedDisplayName(members[0]) #. placeholder {1}: createSanitizedDisplayName(members[1]) -#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:73 +#: src/screens/Messages/components/MessagesListGroupInfoPanel.tsx:70 msgid "New chat with {0}, {1}, and {memberCount, plural, one {{memberCount} more} other {{memberCount} more}}." msgstr "New chat with {0}, {1}, and {memberCount, plural, one {{memberCount} more} other {{memberCount} more}}." @@ -7163,8 +7307,8 @@ msgstr "New group chat" #. Button used to create a new group chat. #. Button used to create a new group chat. -#: src/components/dms/InitiateChatFlow.tsx:743 -#: src/components/dms/InitiateChatFlow.tsx:777 +#: src/components/dms/InitiateChatFlow.tsx:744 +#: src/components/dms/InitiateChatFlow.tsx:778 msgctxt "action" msgid "New group chat" msgstr "New group chat" @@ -7236,7 +7380,7 @@ msgstr "" #: src/components/contacts/screens/ViewMatches.tsx:395 #: src/components/contacts/screens/ViewMatches.tsx:410 #: src/components/dms/AddMembersFlow.tsx:308 -#: src/components/dms/InitiateChatFlow.tsx:461 +#: src/components/dms/InitiateChatFlow.tsx:462 #: src/screens/Login/ForgotPasswordForm.tsx:149 #: src/screens/Login/ForgotPasswordForm.tsx:156 #: src/screens/Login/SetNewPasswordForm.tsx:186 @@ -7308,7 +7452,7 @@ msgstr "No GIFs found for \"{query}\"." msgid "No GIFs to show right now. Try again in a moment." msgstr "No GIFs to show right now. Try again in a moment." -#: src/features/liveNow/components/LinkPreview.tsx:63 +#: src/features/liveNow/components/LinkPreview.tsx:64 msgid "No image" msgstr "" @@ -7409,7 +7553,7 @@ msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:250 #: src/components/dms/AddMembersFlow.tsx:240 -#: src/components/dms/InitiateChatFlow.tsx:346 +#: src/components/dms/InitiateChatFlow.tsx:347 #: src/components/ProgressGuide/FollowDialog.tsx:221 msgid "No results" msgstr "" @@ -7491,12 +7635,12 @@ msgstr "" msgid "Not followed by anyone you’re following" msgstr "Not followed by anyone you’re following" -#: src/Navigation.tsx:164 +#: src/Navigation.tsx:166 #: src/view/screens/Profile.tsx:132 msgid "Not Found" msgstr "" -#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:130 +#: src/components/dialogs/nuxs/DraftsAnnouncement.tsx:131 msgid "Not ready to hit post? Keep your best ideas in Drafts until the timing is just right." msgstr "" @@ -7518,8 +7662,8 @@ msgid "Nothing saved yet" msgstr "" #: src/components/dialogs/NotificationSettingsDialog.tsx:64 -#: src/Navigation.tsx:435 -#: src/Navigation.tsx:509 +#: src/Navigation.tsx:437 +#: src/Navigation.tsx:516 #: src/view/screens/Notifications.tsx:134 msgid "Notification settings" msgstr "" @@ -7529,8 +7673,8 @@ msgstr "" msgid "Notification sounds" msgstr "" -#: src/Navigation.tsx:504 -#: src/Navigation.tsx:743 +#: src/Navigation.tsx:511 +#: src/Navigation.tsx:750 #: src/screens/Notifications/ActivityList.tsx:31 #: src/screens/Settings/NotificationSettings/index.tsx:104 #: src/screens/Settings/Settings.tsx:197 @@ -7579,8 +7723,8 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 -#: src/components/dms/InitiateChatFlow.tsx:676 -#: src/components/dms/MessageItem.tsx:659 +#: src/components/dms/InitiateChatFlow.tsx:677 +#: src/components/dms/MessageItem.tsx:628 #: src/screens/Login/PasswordUpdatedForm.tsx:37 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:661 msgid "Okay" @@ -7613,27 +7757,27 @@ msgstr "One of the selected recipients does not allow group chats." msgid "One of the selected recipients has blocked you and cannot be messaged." msgstr "One of the selected recipients has blocked you and cannot be messaged." -#: src/view/com/composer/Composer.tsx:793 +#: src/view/com/composer/Composer.tsx:787 msgid "One or more GIFs is missing alt text." msgstr "" -#: src/view/com/composer/Composer.tsx:790 +#: src/view/com/composer/Composer.tsx:784 msgid "One or more images is missing alt text." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:411 +#: src/view/com/composer/SelectMediaButton.tsx:415 msgid "One or more of your selected files are not supported." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:434 -msgid "One or more of your selected files are too large. Maximum size is 100 MB." -msgstr "" +#: src/view/com/composer/SelectMediaButton.tsx:438 +msgid "One or more of your selected files are too large. Maximum size is {VIDEO_MAX_SIZE_MB} MB." +msgstr "One or more of your selected files are too large. Maximum size is {VIDEO_MAX_SIZE_MB} MB." -#: src/view/com/composer/Composer.tsx:595 +#: src/view/com/composer/Composer.tsx:589 msgid "One or more posts are too long to save as a draft. {MAX_DRAFT_GRAPHEME_LENGTH, plural, one {The maximum number of characters is # character.} other {The maximum number of characters is # characters.}}" msgstr "" -#: src/view/com/composer/Composer.tsx:800 +#: src/view/com/composer/Composer.tsx:794 msgid "One or more videos is missing alt text." msgstr "" @@ -7642,6 +7786,18 @@ msgstr "" msgid "Only {0} can reply." msgstr "" +#: src/screens/Messages/JoinRequests.tsx:196 +msgid "Only admins can accept join requests." +msgstr "Only admins can accept join requests." + +#: src/screens/Messages/JoinRequests.tsx:228 +msgid "Only admins can ignore join requests." +msgstr "Only admins can ignore join requests." + +#: src/components/intents/GroupChatJoinDialog.tsx:124 +msgid "Only followers can join this group chat." +msgstr "Only followers can join this group chat." + #: src/screens/Settings/ActivityPrivacySettings.tsx:121 #: src/screens/Settings/ActivityPrivacySettings.tsx:126 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:158 @@ -7652,6 +7808,15 @@ msgstr "" msgid "Only image files are supported" msgstr "" +#. placeholder {0}: createSanitizedDisplayName( data.joinLinkPreviews[0].owner, true, moderateProfile( data.joinLinkPreviews[0].owner, moderationOpts, ).ui('displayName'), ) +#: src/screens/Messages/JoinRequest.tsx:207 +msgid "Only people {0} follows can join." +msgstr "Only people {0} follows can join." + +#: src/components/intents/GroupChatJoinDialog.tsx:268 +msgid "Only people the chat owner follows can join" +msgstr "Only people the chat owner follows can join" + #: src/view/com/composer/videos/SubtitleFilePicker.tsx:41 msgid "Only WebVTT (.vtt) files are supported" msgstr "" @@ -7696,7 +7861,7 @@ msgstr "" #: src/screens/Messages/components/MessageComposer.tsx:176 #: src/screens/Messages/components/MessageInput.web.tsx:148 -#: src/view/com/composer/Composer.tsx:2044 +#: src/view/com/composer/Composer.tsx:2026 msgid "Open emoji picker" msgstr "" @@ -7717,6 +7882,10 @@ msgstr "" msgid "Open Germ DM" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:399 +msgid "Open group chat" +msgstr "Open group chat" + #: src/components/dms/MessagesListHeader.tsx:181 #: src/components/dms/MessagesListHeader.tsx:220 msgid "Open group chat settings" @@ -7747,8 +7916,8 @@ msgstr "" msgid "Open post options menu" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:218 -#: src/features/liveNow/components/LiveStatusDialog.tsx:228 +#: src/features/liveNow/components/LiveStatusDialog.tsx:219 +#: src/features/liveNow/components/LiveStatusDialog.tsx:229 msgid "Open profile" msgstr "" @@ -7774,6 +7943,10 @@ msgstr "" msgid "Open system log" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:400 +msgid "Open this group chat" +msgstr "Open this group chat" + #. placeholder {0}: feedInfo.displayName #: src/view/shell/desktop/Feeds.tsx:185 msgid "Opens {0} feed" @@ -7816,21 +7989,23 @@ msgid "Opens device camera" msgstr "" #. Accessibility hint for button in composer to add images, a video, or a GIF to a post. Maximum number of images that can be selected is currently 4 but may change. -#: src/view/com/composer/SelectMediaButton.tsx:505 +#: src/view/com/composer/SelectMediaButton.tsx:509 msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." msgstr "" +#: src/screens/Messages/JoinRequest.tsx:295 #: src/view/com/auth/SplashScreen.tsx:102 #: src/view/com/auth/SplashScreen.web.tsx:110 msgid "Opens flow to create a new Bluesky account" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:281 #: src/view/com/auth/SplashScreen.tsx:120 #: src/view/com/auth/SplashScreen.web.tsx:124 msgid "Opens flow to sign in to your existing Bluesky account" msgstr "" -#: src/components/images/Gallery/index.tsx:428 +#: src/components/images/Gallery/index.tsx:444 msgid "Opens full image" msgstr "Opens full image" @@ -7847,7 +8022,7 @@ msgstr "" msgid "Opens link {0}" msgstr "" -#: src/view/com/util/UserAvatar.tsx:599 +#: src/view/com/util/UserAvatar.tsx:600 msgid "Opens live status dialog" msgstr "" @@ -7878,7 +8053,7 @@ msgstr "" #: src/components/dms/MessageItem.tsx:229 #: src/view/com/notifications/NotificationFeedItem.tsx:1019 -#: src/view/com/util/UserAvatar.tsx:617 +#: src/view/com/util/UserAvatar.tsx:618 msgid "Opens this profile" msgstr "" @@ -7954,8 +8129,8 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:86 #: src/components/dms/AfterReportConversationDialog.tsx:213 -#: src/components/dms/AfterReportDialog.tsx:84 -#: src/components/dms/AfterReportDialog.tsx:176 +#: src/components/dms/AfterReportDialog.tsx:89 +#: src/components/dms/AfterReportDialog.tsx:181 msgid "Our moderation team has received your report." msgstr "" @@ -8031,12 +8206,12 @@ msgid "People {ownerName} follows can request to join" msgstr "People {ownerName} follows can request to join" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:235 +#: src/Navigation.tsx:237 msgid "People followed by @{0}" msgstr "" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:228 +#: src/Navigation.tsx:230 msgid "People following @{0}" msgstr "" @@ -8198,7 +8373,7 @@ msgstr "" msgid "Please complete the verification captcha." msgstr "" -#: src/view/com/composer/state/video.ts:454 +#: src/view/com/composer/state/video.ts:439 msgid "Please confirm your email address to upload videos." msgstr "" @@ -8337,7 +8512,7 @@ msgstr "" msgid "Porn" msgstr "" -#: src/view/com/composer/Composer.tsx:1693 +#: src/view/com/composer/Composer.tsx:1675 msgctxt "action" msgid "Post" msgstr "" @@ -8357,12 +8532,12 @@ msgstr "" msgid "Post a video" msgstr "" -#: src/view/com/composer/Composer.tsx:1691 +#: src/view/com/composer/Composer.tsx:1673 msgctxt "action" msgid "Post All" msgstr "" -#: src/view/com/composer/Composer.tsx:1351 +#: src/view/com/composer/Composer.tsx:1333 msgid "Post anyway" msgstr "Post anyway" @@ -8371,10 +8546,10 @@ msgid "Post blocked" msgstr "" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:261 -#: src/Navigation.tsx:268 -#: src/Navigation.tsx:275 -#: src/Navigation.tsx:282 +#: src/Navigation.tsx:263 +#: src/Navigation.tsx:270 +#: src/Navigation.tsx:277 +#: src/Navigation.tsx:284 msgid "Post by @{0}" msgstr "" @@ -8383,7 +8558,7 @@ msgctxt "toast" msgid "Post deleted" msgstr "" -#: src/lib/api/index.ts:193 +#: src/lib/api/index.ts:186 msgid "Post failed to upload. Please check your Internet connection and try again." msgstr "" @@ -8408,7 +8583,7 @@ msgstr "" msgid "Post interaction settings" msgstr "" -#: src/Navigation.tsx:195 +#: src/Navigation.tsx:197 #: src/screens/ModerationInteractionSettings/index.tsx:35 msgid "Post Interaction Settings" msgstr "" @@ -8418,8 +8593,8 @@ msgstr "" msgid "Post language selection" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:354 -#: src/screens/Messages/components/InviteLinkDialog.tsx:366 +#: src/screens/Messages/components/InviteLinkDialog.tsx:371 +#: src/screens/Messages/components/InviteLinkDialog.tsx:383 msgid "Post link" msgstr "Post link" @@ -8477,6 +8652,7 @@ msgstr "" #: src/components/Error.tsx:61 #: src/components/Lists.tsx:104 #: src/screens/Messages/components/MessageListError.tsx:23 +#: src/screens/Messages/JoinRequests.tsx:350 #: src/screens/Signup/BackNextButtons.tsx:48 msgid "Press to retry" msgstr "" @@ -8485,7 +8661,7 @@ msgstr "" msgid "Press to view followers of this account that you also follow" msgstr "" -#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:120 +#: src/features/liveEvents/components/LiveEventFeedCardWide.tsx:121 msgid "Preview" msgstr "" @@ -8508,8 +8684,8 @@ msgstr "" msgid "Privacy and security" msgstr "" -#: src/Navigation.tsx:412 -#: src/Navigation.tsx:420 +#: src/Navigation.tsx:414 +#: src/Navigation.tsx:422 #: src/screens/Settings/ActivityPrivacySettings.tsx:41 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:45 msgid "Privacy and Security" @@ -8521,7 +8697,7 @@ msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:36 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:103 -#: src/Navigation.tsx:328 +#: src/Navigation.tsx:330 #: src/screens/Settings/AboutSettings.tsx:95 #: src/screens/Settings/AboutSettings.tsx:98 #: src/view/screens/PrivacyPolicy.tsx:25 @@ -8534,15 +8710,15 @@ msgstr "" msgid "Privacy violation of a minor" msgstr "" -#: src/view/com/composer/Composer.tsx:2478 +#: src/view/com/composer/Composer.tsx:2460 msgid "Processing GIF..." msgstr "" -#: src/view/com/composer/Composer.tsx:2480 +#: src/view/com/composer/Composer.tsx:2462 msgid "Processing video..." msgstr "" -#: src/lib/api/index.ts:66 +#: src/lib/api/index.ts:60 msgid "Processing..." msgstr "" @@ -8583,22 +8759,22 @@ msgid "Public, sharable lists of users to mute or block in bulk." msgstr "" #. Accessibility label for button to publish a single post -#: src/view/com/composer/Composer.tsx:1677 +#: src/view/com/composer/Composer.tsx:1659 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1672 +#: src/view/com/composer/Composer.tsx:1654 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:1661 +#: src/view/com/composer/Composer.tsx:1643 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:1666 +#: src/view/com/composer/Composer.tsx:1648 msgid "Publish reply" msgstr "" @@ -8677,11 +8853,11 @@ msgstr "" msgid "Re-attach quote" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:384 +#: src/screens/Messages/components/InviteLinkDialog.tsx:401 msgid "Re-enable invite link" msgstr "Re-enable invite link" -#: src/screens/Messages/components/InviteLinkDialog.tsx:391 +#: src/screens/Messages/components/InviteLinkDialog.tsx:408 msgid "Re-enable link" msgstr "Re-enable link" @@ -8689,8 +8865,8 @@ msgstr "Re-enable link" msgid "React with {emoji}" msgstr "" -#: src/components/dms/ReactionsDialog.tsx:65 -#: src/components/dms/ReactionsDialog.tsx:90 +#: src/components/dms/ReactionsDialog.tsx:66 +#: src/components/dms/ReactionsDialog.tsx:94 msgid "Reactions" msgstr "Reactions" @@ -8708,8 +8884,8 @@ msgctxt "english-only-resource" msgid "read about how to use search filters" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:160 -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:171 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:162 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:173 msgid "Read blog post" msgstr "" @@ -8844,13 +9020,13 @@ msgstr "" msgid "Remove attachment" msgstr "" -#: src/view/com/util/UserAvatar.tsx:519 -#: src/view/com/util/UserAvatar.tsx:522 +#: src/view/com/util/UserAvatar.tsx:520 +#: src/view/com/util/UserAvatar.tsx:523 msgid "Remove Avatar" msgstr "" -#: src/view/com/util/UserBanner.tsx:189 -#: src/view/com/util/UserBanner.tsx:192 +#: src/view/com/util/UserBanner.tsx:190 +#: src/view/com/util/UserBanner.tsx:193 msgid "Remove Banner" msgstr "" @@ -9047,7 +9223,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1689 +#: src/view/com/composer/Composer.tsx:1671 msgctxt "action" msgid "Reply" msgstr "" @@ -9085,9 +9261,9 @@ msgstr "" msgid "Reply was successfully hidden" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:193 -#: src/features/liveNow/components/LiveStatusDialog.tsx:266 -#: src/screens/Messages/ConversationSettings/index.tsx:518 +#: src/components/dms/MessageContextMenu.tsx:176 +#: src/features/liveNow/components/LiveStatusDialog.tsx:267 +#: src/screens/Messages/ConversationSettings/index.tsx:517 msgid "Report" msgstr "" @@ -9103,8 +9279,8 @@ msgstr "" msgid "Report conversation" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:96 -#: src/components/moderation/ReportDialog/index.tsx:261 +#: src/components/moderation/ReportDialog/index.tsx:98 +#: src/components/moderation/ReportDialog/index.tsx:263 msgid "Report dialog" msgstr "" @@ -9118,7 +9294,7 @@ msgstr "" msgid "Report list" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:190 +#: src/components/dms/MessageContextMenu.tsx:173 msgid "Report message" msgstr "" @@ -9138,8 +9314,8 @@ msgstr "" #: src/components/dms/AfterReportConversationDialog.tsx:83 #: src/components/dms/AfterReportConversationDialog.tsx:210 -#: src/components/dms/AfterReportDialog.tsx:81 -#: src/components/dms/AfterReportDialog.tsx:173 +#: src/components/dms/AfterReportDialog.tsx:86 +#: src/components/dms/AfterReportDialog.tsx:178 msgid "Report submitted" msgstr "" @@ -9152,7 +9328,7 @@ msgstr "" msgid "Report this feed" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:517 +#: src/screens/Messages/ConversationSettings/index.tsx:516 msgid "Report this group chat" msgstr "Report this group chat" @@ -9161,7 +9337,7 @@ msgid "Report this list" msgstr "" #: src/components/moderation/ReportDialog/copy.ts:19 -#: src/features/liveNow/components/LiveStatusDialog.tsx:249 +#: src/features/liveNow/components/LiveStatusDialog.tsx:250 msgid "Report this livestream" msgstr "" @@ -9232,16 +9408,42 @@ msgstr "" msgid "Reposts of your reposts" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:414 +msgid "Request access to group chat" +msgstr "Request access to group chat" + +#: src/components/intents/GroupChatJoinDialog.tsx:419 +msgid "Request access to join this group chat" +msgstr "Request access to join this group chat" + +#: src/screens/Messages/JoinRequests.tsx:178 +msgid "Request approved." +msgstr "Request approved." + #: src/screens/Settings/components/ChangePasswordDialog.tsx:226 #: src/screens/Settings/components/ChangePasswordDialog.tsx:232 msgid "Request code" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:210 +msgid "Request ignored." +msgstr "Request ignored." + +#: src/components/intents/GroupChatJoinDialog.tsx:252 +msgid "Request to join" +msgstr "Request to join" + #. Incoming message requests #: src/screens/Messages/components/InboxRequests.tsx:24 msgid "Requests" msgstr "Requests" +#: src/Navigation.tsx:495 +#: src/screens/Messages/JoinRequests.tsx:58 +#: src/screens/Messages/JoinRequests.tsx:421 +msgid "Requests to join" +msgstr "Requests to join" + #: src/screens/Settings/AccessibilitySettings.tsx:59 #: src/screens/Settings/AccessibilitySettings.tsx:64 msgid "Require alt text before posting" @@ -9259,6 +9461,10 @@ msgstr "" msgid "Required in your region" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:272 +msgid "Rescind request" +msgstr "Rescind request" + #: src/components/dialogs/EmailDialog/components/ResendEmailText.tsx:39 msgid "Resend" msgstr "" @@ -9324,7 +9530,7 @@ msgstr "" #: src/components/contacts/screens/VerifyNumber.tsx:355 #: src/components/Error.tsx:65 #: src/components/Lists.tsx:115 -#: src/components/moderation/ReportDialog/index.tsx:295 +#: src/components/moderation/ReportDialog/index.tsx:297 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:59 #: src/components/StarterPack/ProfileStarterPacks.tsx:377 @@ -9333,6 +9539,7 @@ msgstr "" #: src/screens/Messages/ChatList.tsx:354 #: src/screens/Messages/components/MessageListError.tsx:24 #: src/screens/Messages/Inbox.tsx:220 +#: src/screens/Messages/JoinRequests.tsx:356 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:268 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:271 #: src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx:92 @@ -9346,7 +9553,7 @@ msgstr "" msgid "Retry" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:292 +#: src/components/moderation/ReportDialog/index.tsx:294 #: src/view/screens/Storybook/Admonitions.tsx:61 msgid "Retry loading report options" msgstr "" @@ -9410,27 +9617,29 @@ msgstr "" #: src/screens/SavedFeeds.tsx:124 #: src/screens/SavedFeeds.tsx:311 #: src/screens/SavedFeeds.tsx:315 -#: src/view/com/composer/Composer.tsx:1332 +#: src/view/com/composer/Composer.tsx:1314 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save changes" msgstr "" -#: src/view/com/composer/Composer.tsx:1304 +#: src/view/com/composer/Composer.tsx:1286 #: src/view/com/composer/drafts/DraftsButton.tsx:93 msgid "Save changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:1332 +#: src/view/com/composer/Composer.tsx:1314 #: src/view/com/composer/drafts/DraftsButton.tsx:125 msgid "Save draft" msgstr "" -#: src/view/com/composer/Composer.tsx:1306 +#: src/view/com/composer/Composer.tsx:1288 #: src/view/com/composer/drafts/DraftsButton.tsx:95 msgid "Save draft?" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:99 +#: src/components/MediaPreview.tsx:197 +#: src/components/Post/Embed/ImageContextMenu.tsx:70 #: src/components/StarterPack/ShareDialog.tsx:144 #: src/components/StarterPack/ShareDialog.tsx:150 msgid "Save image" @@ -9465,8 +9674,8 @@ msgstr "" msgid "Saved Feeds" msgstr "" -#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:144 -#: src/Navigation.tsx:548 +#: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:145 +#: src/Navigation.tsx:555 #: src/screens/Bookmarks/index.tsx:59 msgid "Saved Posts" msgstr "" @@ -9518,7 +9727,7 @@ msgstr "" #. placeholder {0}: profile.handle #. placeholder {0}: route.params.name -#: src/Navigation.tsx:254 +#: src/Navigation.tsx:256 #: src/screens/Profile/ProfileSearch.tsx:37 msgid "Search @{0}'s posts" msgstr "" @@ -9690,7 +9899,7 @@ msgstr "" msgid "Select a color" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:378 +#: src/components/moderation/ReportDialog/index.tsx:380 msgid "Select a reason" msgstr "" @@ -9759,7 +9968,7 @@ msgstr "" msgid "Select GIF \"{0}\"" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:668 +#: src/components/dms/InitiateChatFlow.tsx:669 msgid "Select group chat members" msgstr "Select group chat members" @@ -9781,7 +9990,7 @@ msgstr "" msgid "Select languages" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:428 +#: src/components/moderation/ReportDialog/index.tsx:430 msgid "Select moderation service" msgstr "" @@ -9839,7 +10048,7 @@ msgstr "" msgid "Select your preferred notification channels" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:414 +#: src/view/com/composer/SelectMediaButton.tsx:418 msgid "Selecting multiple media types is not supported." msgstr "" @@ -9883,7 +10092,7 @@ msgstr "" msgid "Send post to..." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:816 +#: src/components/moderation/ReportDialog/index.tsx:818 msgid "Send report to {title}" msgstr "" @@ -9905,7 +10114,7 @@ msgid "Send via direct message" msgstr "" #. placeholder {0}: i18n.date(new Date(message.sentAt), { timeStyle: 'short', }) -#: src/components/dms/MessageContextMenu.tsx:154 +#: src/components/dms/MessageContextMenu.tsx:137 msgid "Sent at {0}" msgstr "Sent at {0}" @@ -9946,7 +10155,7 @@ msgstr "" msgid "Sets email for password reset" msgstr "" -#: src/Navigation.tsx:210 +#: src/Navigation.tsx:212 #: src/screens/Settings/Settings.tsx:98 #: src/view/shell/desktop/LeftNav.tsx:754 #: src/view/shell/Drawer.tsx:612 @@ -10010,10 +10219,12 @@ msgstr "" msgid "Sexually Suggestive" msgstr "" +#: src/components/MediaPreview.tsx:203 +#: src/components/Post/Embed/ImageContextMenu.tsx:74 #: src/components/StarterPack/QrCodeDialog.tsx:195 #: src/screens/Hashtag.tsx:130 -#: src/screens/Messages/components/InviteLinkDialog.tsx:370 -#: src/screens/Messages/components/InviteLinkDialog.tsx:377 +#: src/screens/Messages/components/InviteLinkDialog.tsx:387 +#: src/screens/Messages/components/InviteLinkDialog.tsx:394 #: src/screens/StarterPack/StarterPackScreen.tsx:447 #: src/screens/Topic.tsx:90 msgid "Share" @@ -10029,8 +10240,8 @@ msgid "Share author DID" msgstr "" #: src/components/Lightbox/chrome/ImageMenu.tsx:94 -#: src/components/Lightbox/Lightbox.web.tsx:273 -#: src/components/Lightbox/Lightbox.web.tsx:299 +#: src/components/Lightbox/Lightbox.web.tsx:277 +#: src/components/Lightbox/Lightbox.web.tsx:303 msgid "Share image" msgstr "Share image" @@ -10082,7 +10293,7 @@ msgstr "" msgid "Share your contacts to find friends" msgstr "" -#: src/Navigation.tsx:313 +#: src/Navigation.tsx:315 msgid "Shared Preferences Tester" msgstr "" @@ -10098,16 +10309,16 @@ msgstr "" #: src/components/moderation/ScreenHider.tsx:179 #: src/components/moderation/ScreenHider.tsx:182 -#: src/features/liveNow/components/LiveStatusDialog.tsx:317 -#: src/features/liveNow/components/LiveStatusDialog.tsx:321 +#: src/features/liveNow/components/LiveStatusDialog.tsx:318 +#: src/features/liveNow/components/LiveStatusDialog.tsx:322 #: src/screens/List/ListHiddenScreen.tsx:194 #: src/screens/VideoFeed/index.tsx:646 #: src/screens/VideoFeed/index.tsx:652 msgid "Show anyway" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:181 -#: src/screens/Settings/AutomationLabelSettings.tsx:194 +#: src/screens/Settings/AutomationLabelSettings.tsx:185 +#: src/screens/Settings/AutomationLabelSettings.tsx:198 msgid "Show automation label" msgstr "Show automation label" @@ -10201,7 +10412,7 @@ msgid "Show warning and filter from feeds" msgstr "" #: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:60 -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:170 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:171 msgid "Show when you’re live" msgstr "" @@ -10228,6 +10439,8 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:179 #: src/screens/Login/LoginForm.tsx:350 #: src/screens/Login/LoginForm.tsx:356 +#: src/screens/Messages/JoinRequest.tsx:280 +#: src/screens/Messages/JoinRequest.tsx:286 #: src/screens/Search/SearchResults.tsx:316 #: src/view/com/auth/SplashScreen.tsx:118 #: src/view/com/auth/SplashScreen.tsx:124 @@ -10260,6 +10473,10 @@ msgstr "" msgid "Sign in or create your account to join the conversation!" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:205 +msgid "Sign in to accept invite." +msgstr "Sign in to accept invite." + #: src/components/AccountList.tsx:70 msgid "Sign in to account that is not listed" msgstr "" @@ -10268,6 +10485,10 @@ msgstr "" msgid "Sign in to Bluesky or create a new account" msgstr "" +#: src/screens/Messages/JoinRequest.tsx:204 +msgid "Sign in to request access to this group chat." +msgstr "Sign in to request access to this group chat." + #: src/components/PostControls/PostMenu/PostMenuItems.tsx:571 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:573 msgid "Sign in to view post" @@ -10322,7 +10543,7 @@ msgstr "" msgid "Skip contact sharing and continue to the app" msgstr "" -#: src/view/com/composer/Composer.tsx:1349 +#: src/view/com/composer/Composer.tsx:1331 msgid "Skip empty posts?" msgstr "Skip empty posts?" @@ -10336,7 +10557,7 @@ msgstr "" msgid "Skip to next step" msgstr "" -#: src/components/images/Gallery/index.tsx:417 +#: src/components/images/Gallery/index.tsx:427 msgid "slide" msgstr "slide" @@ -10393,7 +10614,7 @@ msgid "Someone left the group" msgstr "Someone left the group" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:294 +#: src/components/dms/MessageItem.tsx:270 msgid "Someone reacted {0}" msgstr "" @@ -10418,17 +10639,18 @@ msgstr "Someone was removed" msgid "Someone was removed from the group" msgstr "Someone was removed from the group" -#: src/components/moderation/ReportDialog/index.tsx:101 +#: src/components/moderation/ReportDialog/index.tsx:103 msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" #: src/screens/Messages/Conversation.tsx:135 #: src/screens/Messages/ConversationSettings/index.tsx:112 +#: src/screens/Messages/JoinRequests.tsx:87 msgid "Something went wrong" msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:139 -#: src/components/moderation/ReportDialog/index.tsx:287 +#: src/components/moderation/ReportDialog/index.tsx:289 #: src/screens/Deactivated.tsx:86 #: src/screens/Settings/components/DeactivateAccountDialog.tsx:59 #: src/view/screens/Storybook/Admonitions.tsx:56 @@ -10448,7 +10670,7 @@ msgstr "" msgid "Something went wrong. Please try again in a moment." msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:245 +#: src/components/moderation/ReportDialog/index.tsx:247 msgid "Something went wrong. Please try again." msgstr "" @@ -10513,17 +10735,17 @@ msgstr "" msgid "Start adding people!" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:669 +#: src/components/dms/InitiateChatFlow.tsx:670 msgid "Start chat" msgstr "Start chat" #: src/components/dialogs/SearchablePeopleList.tsx:428 -#: src/components/dms/InitiateChatFlow.tsx:817 +#: src/components/dms/InitiateChatFlow.tsx:818 msgid "Start chat with {displayName}" msgstr "" -#: src/Navigation.tsx:519 -#: src/Navigation.tsx:524 +#: src/Navigation.tsx:526 +#: src/Navigation.tsx:531 #: src/screens/StarterPack/Wizard/index.tsx:197 msgid "Starter Pack" msgstr "" @@ -10585,12 +10807,12 @@ msgstr "" msgid "Stored as part of a secure code for matching with others" msgstr "" -#: src/Navigation.tsx:303 +#: src/Navigation.tsx:305 #: src/screens/Settings/Settings.tsx:456 msgid "Storybook" msgstr "" -#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:181 +#: src/components/dialogs/nuxs/LiveNowBetaDialog.tsx:182 msgid "Streaming on Twitch? Set your live status on Bluesky to add a badge to your avatar. Tapping it takes people straight to your stream." msgstr "" @@ -10615,9 +10837,9 @@ msgstr "" msgid "Submit Appeal" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:506 -#: src/components/moderation/ReportDialog/index.tsx:567 -#: src/components/moderation/ReportDialog/index.tsx:574 +#: src/components/moderation/ReportDialog/index.tsx:508 +#: src/components/moderation/ReportDialog/index.tsx:569 +#: src/components/moderation/ReportDialog/index.tsx:576 msgid "Submit report" msgstr "" @@ -10626,13 +10848,13 @@ msgid "Subscribe" msgstr "" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:425 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:434 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:426 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:435 msgid "Subscribe on {0}" msgstr "Subscribe on {0}" #. placeholder {0}: highlightedPublisher.name -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:433 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:434 msgid "Subscribe to {publicationTitle} on {0}" msgstr "Subscribe to {publicationTitle} on {0}" @@ -10667,12 +10889,16 @@ msgstr "" msgid "Success!" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:100 +msgid "Successfully joined the group chat!" +msgstr "Successfully joined the group chat!" + #: src/components/verification/VerificationCreatePrompt.tsx:37 msgid "Successfully verified" msgstr "" #: src/components/dms/AddMembersFlow.tsx:226 -#: src/components/dms/InitiateChatFlow.tsx:325 +#: src/components/dms/InitiateChatFlow.tsx:326 msgid "Suggested" msgstr "Suggested" @@ -10701,7 +10927,7 @@ msgctxt "Name of app icon variant" msgid "Sunset" msgstr "" -#: src/Navigation.tsx:323 +#: src/Navigation.tsx:325 #: src/view/screens/Support.tsx:25 #: src/view/screens/Support.tsx:28 msgid "Support" @@ -10759,7 +10985,7 @@ msgstr "" msgid "Tap below to allow Bluesky to access your GPS location. We will then use that data to more accurately determine the content and features available in your region." msgstr "" -#: src/components/dms/MessageItem.tsx:598 +#: src/components/dms/MessageItem.tsx:567 msgid "Tap for details" msgstr "Tap for details" @@ -10789,29 +11015,29 @@ msgstr "" msgid "Tap to dismiss" msgstr "" -#: src/components/dms/ReactionsDialog.tsx:192 +#: src/components/dms/ReactionsDialog.tsx:196 msgid "Tap to remove" msgstr "Tap to remove" #. placeholder {0}: reaction.value -#: src/components/dms/ReactionsDialog.tsx:208 +#: src/components/dms/ReactionsDialog.tsx:212 msgid "Tap to remove your {0} reaction" msgstr "Tap to remove your {0} reaction" -#: src/components/dms/MessageItem.tsx:563 +#: src/components/dms/MessageItem.tsx:532 msgid "Tap to retry" msgstr "Tap to retry" #. placeholder {0}: tab.value -#: src/components/dms/ReactionsDialog.tsx:354 +#: src/components/dms/ReactionsDialog.tsx:358 msgid "Tap to show {0} reactions" msgstr "Tap to show {0} reactions" -#: src/components/dms/ReactionsDialog.tsx:353 +#: src/components/dms/ReactionsDialog.tsx:357 msgid "Tap to show all reactions" msgstr "Tap to show all reactions" -#: src/components/dms/MessageItem.tsx:317 +#: src/components/dms/MessageItem.tsx:293 msgid "Tap to view reactions" msgstr "Tap to view reactions" @@ -10855,7 +11081,7 @@ msgstr "" #: src/components/dialogs/BirthDateSettings.tsx:181 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:31 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:98 -#: src/Navigation.tsx:333 +#: src/Navigation.tsx:335 #: src/screens/Settings/AboutSettings.tsx:87 #: src/screens/Settings/AboutSettings.tsx:90 #: src/view/screens/TermsOfService.tsx:25 @@ -10980,6 +11206,11 @@ msgstr "" msgid "The laws in your region require you to verify you're an adult to access certain features. Tap to learn more." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:134 +#: src/screens/Messages/JoinRequests.tsx:201 +msgid "The member limit has been reached." +msgstr "The member limit has been reached." + #: src/view/com/composer/select-language/SuggestedLanguage.tsx:400 msgid "The post you’re replying to was marked as being written in {suggestedLanguageName} by its author. Would you like to reply in <0>{suggestedLanguageName}?" msgstr "The post you’re replying to was marked as being written in {suggestedLanguageName} by its author. Would you like to reply in <0>{suggestedLanguageName}?" @@ -10988,10 +11219,10 @@ msgstr "The post you’re replying to was marked as being written in {suggestedL msgid "The Privacy Policy has been moved to <0/>" msgstr "" -#: src/view/com/composer/state/video.ts:407 -#: src/view/com/composer/state/video.ts:451 -msgid "The selected video is larger than {videoSize} MB. Please try again with a smaller file." -msgstr "The selected video is larger than {videoSize} MB. Please try again with a smaller file." +#: src/view/com/composer/state/video.ts:397 +#: src/view/com/composer/state/video.ts:436 +msgid "The selected video is larger than {VIDEO_MAX_SIZE_MB} MB. Please try again with a smaller file." +msgstr "The selected video is larger than {VIDEO_MAX_SIZE_MB} MB. Please try again with a smaller file." #: src/lib/hooks/useCleanError.ts:41 #: src/lib/strings/errors.ts:19 @@ -11032,7 +11263,7 @@ msgstr "" msgid "There is a limit to how often you can change your birthdate. You may need to wait a day or two before updating it again." msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:476 +#: src/screens/Messages/components/InviteLinkDialog.tsx:487 msgid "There is no invite link for this group chat." msgstr "There is no invite link for this group chat." @@ -11046,6 +11277,8 @@ msgid "There was a problem loading GIFs. Check your connection and try again." msgstr "There was a problem loading GIFs. Check your connection and try again." #: src/components/contacts/screens/GetContacts.tsx:147 +#: src/components/intents/GroupChatJoinDialog.tsx:118 +#: src/components/intents/GroupChatJoinDialog.tsx:152 msgid "There was a problem with your internet connection, please try again" msgstr "" @@ -11194,7 +11427,7 @@ msgstr "" msgid "This author has chosen to make their posts visible only to people who are signed in." msgstr "" -#: src/screens/Profile/components/GermButton.tsx:243 +#: src/screens/Profile/components/GermButton.tsx:244 msgid "This button lets others open the Germ DM app to send you a message. You can manage its visibility from the Germ DM app, or you can disconnect your Bluesky account from Germ DM altogether by clicking the button below." msgstr "" @@ -11202,6 +11435,10 @@ msgstr "" msgid "This chat has ended" msgstr "This chat has ended" +#: src/components/intents/GroupChatJoinDialog.tsx:263 +msgid "This chat is full" +msgstr "This chat is full" + #: src/screens/Messages/components/ChatListItem.tsx:375 #: src/screens/Messages/components/ChatLocked.tsx:61 msgid "This chat is locked" @@ -11241,6 +11478,10 @@ msgstr "" msgid "This content is not viewable without a Bluesky account." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:120 +msgid "This conversation is locked." +msgstr "This conversation is locked." + #: src/screens/Messages/components/ChatListItem.tsx:155 msgid "This conversation is with a deleted or a deactivated account. Press for options" msgstr "" @@ -11299,6 +11540,18 @@ msgstr "This image could not be used. Try a different format like .jpg or .png." msgid "This information is private and not shared with other users." msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:130 +msgid "This invite link has been disabled." +msgstr "This invite link has been disabled." + +#: src/screens/Messages/JoinRequest.tsx:83 +msgid "This invite link has expired" +msgstr "This invite link has expired" + +#: src/components/intents/GroupChatJoinDialog.tsx:194 +msgid "This invite link is invalid" +msgstr "This invite link is invalid" + #: src/view/screens/Debug.tsx:327 msgid "This is an empty state" msgstr "" @@ -11308,7 +11561,7 @@ msgstr "" msgid "This is not a valid link" msgstr "" -#: src/screens/Settings/AutomationLabelSettings.tsx:169 +#: src/screens/Settings/AutomationLabelSettings.tsx:173 msgid "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." msgstr "This label lets the world know that this account is automated. If turned on, this label appears next to the account's name on their profile and posts. It can be turned on or off at any time." @@ -11341,13 +11594,13 @@ msgstr "" msgid "This list is empty." msgstr "" -#: src/components/dms/MessageItem.tsx:596 -#: src/components/dms/MessageItem.tsx:625 +#: src/components/dms/MessageItem.tsx:565 +#: src/components/dms/MessageItem.tsx:594 msgid "This message is hidden because this user is blocking you." msgstr "This message is hidden because this user is blocking you." -#: src/components/dms/MessageItem.tsx:595 -#: src/components/dms/MessageItem.tsx:621 +#: src/components/dms/MessageItem.tsx:564 +#: src/components/dms/MessageItem.tsx:590 msgid "This message is hidden because you are blocking this user." msgstr "This message is hidden because you are blocking this user." @@ -11385,7 +11638,7 @@ msgstr "" msgid "This post will be hidden from feeds and threads. This cannot be undone." msgstr "" -#: src/view/com/composer/Composer.tsx:963 +#: src/view/com/composer/Composer.tsx:945 msgid "This post's author has disabled quote posts." msgstr "" @@ -11398,6 +11651,7 @@ msgid "This reply will be sorted into a hidden section at the bottom of your thr msgstr "" #: src/screens/Messages/ConversationSettings/index.tsx:132 +#: src/screens/Messages/JoinRequests.tsx:111 msgid "This screen is only available for group conversations." msgstr "This screen is only available for group conversations." @@ -11505,7 +11759,7 @@ msgstr "" msgid "Threaded" msgstr "" -#: src/Navigation.tsx:366 +#: src/Navigation.tsx:368 msgid "Threads Preferences" msgstr "" @@ -11577,12 +11831,12 @@ msgstr "" msgid "Top replies first" msgstr "" -#: src/Navigation.tsx:477 +#: src/Navigation.tsx:479 msgid "Topic" msgstr "" -#: src/components/dms/MessageContextMenu.tsx:162 -#: src/components/dms/MessageContextMenu.tsx:165 +#: src/components/dms/MessageContextMenu.tsx:145 +#: src/components/dms/MessageContextMenu.tsx:148 #: src/components/Post/Translated/index.tsx:150 #: src/components/Post/Translated/index.tsx:157 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:553 @@ -11633,7 +11887,7 @@ msgstr "" msgid "Trolling" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:140 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:142 msgid "Trust emerges from relationships, communities, and shared context, so we’re also enabling <0>trusted verifiers: organizations that can directly issue verification." msgstr "" @@ -11694,6 +11948,10 @@ msgstr "" msgid "Unable to delete" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:346 +msgid "Unable to fetch join requests." +msgstr "Unable to fetch join requests." + #: src/components/dms/dialogs/NewChatDialog.tsx:109 msgid "Unable to find a selected recipient." msgstr "Unable to find a selected recipient." @@ -11722,7 +11980,7 @@ msgstr "" msgid "Unavailable feed information" msgstr "" -#: src/components/dms/MessageItem.tsx:663 +#: src/components/dms/MessageItem.tsx:632 #: src/components/dms/MessagesListBlockedFooter.tsx:97 #: src/components/dms/MessagesListBlockedFooter.tsx:104 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:222 @@ -11768,8 +12026,8 @@ msgstr "" #: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:79 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:40 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:46 -#: src/screens/Profile/components/GermButton.tsx:185 #: src/screens/Profile/components/GermButton.tsx:186 +#: src/screens/Profile/components/GermButton.tsx:187 msgid "Undo" msgstr "" @@ -11803,7 +12061,7 @@ msgstr "" msgid "Unfollows the user" msgstr "" -#: src/components/moderation/ReportDialog/index.tsx:490 +#: src/components/moderation/ReportDialog/index.tsx:492 msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" @@ -11849,7 +12107,7 @@ msgstr "" msgid "Unlock chat" msgstr "Unlock chat" -#: src/screens/Messages/ConversationSettings/index.tsx:502 +#: src/screens/Messages/ConversationSettings/index.tsx:501 msgid "Unlock this group chat" msgstr "Unlock this group chat" @@ -11886,7 +12144,7 @@ msgstr "" msgid "Unmute list" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:464 +#: src/screens/Messages/ConversationSettings/index.tsx:463 msgid "Unmute this group chat" msgstr "Unmute this group chat" @@ -11965,7 +12223,7 @@ msgstr "" msgid "Unsupported clipboard content" msgstr "Unsupported clipboard content" -#: src/view/com/composer/Composer.tsx:1442 +#: src/view/com/composer/Composer.tsx:1424 msgid "Unsupported video type: {mimeType}" msgstr "" @@ -11985,9 +12243,9 @@ msgstr "" msgid "Update email" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:205 -#: src/screens/Messages/components/InviteLinkDialog.tsx:243 -#: src/screens/Messages/components/InviteLinkDialog.tsx:267 +#: src/screens/Messages/components/InviteLinkDialog.tsx:217 +#: src/screens/Messages/components/InviteLinkDialog.tsx:255 +#: src/screens/Messages/components/InviteLinkDialog.tsx:283 msgid "Update invite link" msgstr "Update invite link" @@ -12029,39 +12287,39 @@ msgstr "" msgid "Upload a text file to:" msgstr "" -#: src/view/com/util/UserAvatar.tsx:490 -#: src/view/com/util/UserAvatar.tsx:493 -#: src/view/com/util/UserBanner.tsx:160 -#: src/view/com/util/UserBanner.tsx:163 +#: src/view/com/util/UserAvatar.tsx:491 +#: src/view/com/util/UserAvatar.tsx:494 +#: src/view/com/util/UserBanner.tsx:161 +#: src/view/com/util/UserBanner.tsx:164 msgid "Upload from Camera" msgstr "" -#: src/view/com/util/UserAvatar.tsx:507 -#: src/view/com/util/UserBanner.tsx:177 +#: src/view/com/util/UserAvatar.tsx:508 +#: src/view/com/util/UserBanner.tsx:178 msgid "Upload from Files" msgstr "" -#: src/view/com/util/UserAvatar.tsx:501 -#: src/view/com/util/UserAvatar.tsx:505 -#: src/view/com/util/UserBanner.tsx:171 -#: src/view/com/util/UserBanner.tsx:175 +#: src/view/com/util/UserAvatar.tsx:502 +#: src/view/com/util/UserAvatar.tsx:506 +#: src/view/com/util/UserBanner.tsx:172 +#: src/view/com/util/UserBanner.tsx:176 msgid "Upload from Library" msgstr "" -#: src/view/com/composer/Composer.tsx:2471 +#: src/view/com/composer/Composer.tsx:2453 msgid "Uploading GIF..." msgstr "" -#: src/lib/api/index.ts:338 +#: src/lib/api/index.ts:322 msgid "Uploading images..." msgstr "" -#: src/lib/api/index.ts:409 -#: src/lib/api/index.ts:433 +#: src/lib/api/index.ts:390 +#: src/lib/api/index.ts:414 msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:2473 +#: src/view/com/composer/Composer.tsx:2455 msgid "Uploading video..." msgstr "" @@ -12105,7 +12363,7 @@ msgid "user" msgstr "user" #: src/components/dms/AfterReportConversationDialog.tsx:187 -#: src/components/dms/AfterReportDialog.tsx:150 +#: src/components/dms/AfterReportDialog.tsx:155 msgctxt "toast" msgid "User blocked" msgstr "" @@ -12208,7 +12466,7 @@ msgstr "" msgid "Verification settings" msgstr "" -#: src/Navigation.tsx:203 +#: src/Navigation.tsx:205 #: src/screens/Moderation/VerificationSettings.tsx:34 msgid "Verification Settings" msgstr "" @@ -12311,11 +12569,11 @@ msgstr "" msgid "Video" msgstr "" -#: src/view/com/composer/state/video.ts:364 +#: src/view/com/composer/state/video.ts:359 msgid "Video failed to process" msgstr "" -#: src/Navigation.tsx:540 +#: src/Navigation.tsx:547 msgid "Video Feed" msgstr "" @@ -12350,7 +12608,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:2491 +#: src/view/com/composer/Composer.tsx:2473 msgid "Video uploaded" msgstr "" @@ -12363,18 +12621,18 @@ msgstr "" msgid "Videos" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:428 +#: src/view/com/composer/SelectMediaButton.tsx:432 msgid "Videos must be less than 3 minutes long." msgstr "" -#: src/view/com/composer/Composer.tsx:1055 +#: src/view/com/composer/Composer.tsx:1037 msgctxt "Action to view the post the user just created" msgid "View" msgstr "" #. placeholder {0}: view.source.title -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:319 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:588 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 msgid "View {0}" msgstr "View {0}" @@ -12403,7 +12661,7 @@ msgstr "View {0}’s profile" msgid "View {displayName}’s profile" msgstr "View {displayName}’s profile" -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:436 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:437 msgid "View {publicationTitle}" msgstr "View {publicationTitle}" @@ -12434,7 +12692,7 @@ msgstr "" msgid "View full thread" msgstr "" -#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:43 +#: src/screens/Messages/ConversationSettings/MembersAndRequests.tsx:48 msgid "View incoming group chat requests" msgstr "View incoming group chat requests" @@ -12453,7 +12711,7 @@ msgstr "" msgid "View more trending videos" msgstr "" -#: src/view/com/composer/Composer.tsx:1050 +#: src/view/com/composer/Composer.tsx:1032 msgid "View post" msgstr "" @@ -12470,10 +12728,10 @@ msgstr "" msgid "View profile banner" msgstr "View profile banner" -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:319 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:426 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:437 -#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:588 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:320 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:427 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:438 +#: src/components/Post/Embed/StandardSiteEmbed/index.tsx:589 msgid "View publication" msgstr "View publication" @@ -12481,7 +12739,7 @@ msgstr "View publication" msgid "View the avatar" msgstr "" -#: src/screens/Messages/ConversationSettings/index.tsx:489 +#: src/screens/Messages/ConversationSettings/index.tsx:488 msgid "View the invite link for this group chat" msgstr "View the invite link for this group chat" @@ -12527,8 +12785,8 @@ msgstr "" msgid "View your verifications" msgstr "" -#: src/components/images/AutoSizedImage.tsx:207 -#: src/components/images/AutoSizedImage.tsx:239 +#: src/components/images/AutoSizedImage.tsx:221 +#: src/components/images/AutoSizedImage.tsx:253 msgid "Views full image" msgstr "" @@ -12571,8 +12829,8 @@ msgstr "" msgid "Warn content and filter from feeds" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:189 -#: src/features/liveNow/components/LiveStatusDialog.tsx:198 +#: src/features/liveNow/components/LiveStatusDialog.tsx:190 +#: src/features/liveNow/components/LiveStatusDialog.tsx:199 msgid "Watch now" msgstr "" @@ -12600,6 +12858,10 @@ msgstr "" msgid "We couldn't load this conversation" msgstr "" +#: src/screens/Messages/JoinRequests.tsx:88 +msgid "We couldn’t load this conversation’s join requests" +msgstr "We couldn’t load this conversation’s join requests" + #: src/screens/Messages/ConversationSettings/index.tsx:113 msgid "We couldn’t load this conversation’s settings" msgstr "We couldn’t load this conversation’s settings" @@ -12650,7 +12912,7 @@ msgstr "" msgid "We sent an email to <0>{0} containing a link. Please click on it to complete the email verification process." msgstr "" -#: src/view/com/composer/state/video.ts:434 +#: src/view/com/composer/state/video.ts:419 msgid "We were unable to determine if you are allowed to upload videos. Please try again." msgstr "" @@ -12715,7 +12977,7 @@ msgstr "" msgid "We’re having network issues, try again" msgstr "We’re having network issues, try again" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:96 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:97 msgid "We’re introducing a new layer of verification on Bluesky — an easy-to-see checkmark." msgstr "" @@ -12745,7 +13007,7 @@ msgstr "We’re sorry, but your search could not be completed. Please try again msgid "We're sorry, you cannot access this screen at this time." msgstr "" -#: src/view/com/composer/Composer.tsx:961 +#: src/view/com/composer/Composer.tsx:943 msgid "We're sorry! The post you are replying to has been deleted." msgstr "" @@ -12796,12 +13058,12 @@ msgid "What do you want to call your starter pack?" msgstr "" #: src/view/com/auth/SplashScreen.web.tsx:98 -#: src/view/com/composer/Composer.tsx:1402 +#: src/view/com/composer/Composer.tsx:1384 #: src/view/com/feeds/ComposerPrompt.tsx:193 msgid "What's up?" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:148 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:150 msgid "When you tap on a check, you’ll see which organizations have granted verification." msgstr "" @@ -12809,7 +13071,7 @@ msgstr "" msgid "Who can interact with this post?" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:216 +#: src/screens/Messages/components/InviteLinkDialog.tsx:227 msgid "Who can join this group chat and how" msgstr "Who can join this group chat and how" @@ -12818,7 +13080,7 @@ msgstr "Who can join this group chat and how" msgid "Who can reply" msgstr "" -#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:129 +#: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:131 msgid "Who can verify?" msgstr "" @@ -12870,7 +13132,7 @@ msgstr "" msgid "Why should this user be reviewed?" msgstr "" -#: src/components/dms/AfterReportDialog.tsx:46 +#: src/components/dms/AfterReportDialog.tsx:51 msgid "Would you like to block this user and/or delete this conversation?" msgstr "" @@ -12882,7 +13144,7 @@ msgstr "Would you like to block this user and/or leave this conversation?" msgid "Would you like to save this as a draft before viewing your drafts?" msgstr "" -#: src/view/com/composer/Composer.tsx:1320 +#: src/view/com/composer/Composer.tsx:1302 msgid "Would you like to save this as a draft to edit later?" msgstr "" @@ -12891,12 +13153,12 @@ msgstr "" msgid "Write a post" msgstr "" -#: src/view/com/composer/Composer.tsx:1502 +#: src/view/com/composer/Composer.tsx:1484 msgid "Write post" msgstr "" #: src/screens/PostThread/components/ThreadComposePrompt.tsx:91 -#: src/view/com/composer/Composer.tsx:1400 +#: src/view/com/composer/Composer.tsx:1382 msgid "Write your reply" msgstr "" @@ -12910,6 +13172,7 @@ msgid "Wrong DID returned from server. Received: {0}" msgstr "" #: src/screens/Messages/ConversationSettings/index.tsx:131 +#: src/screens/Messages/JoinRequests.tsx:110 msgid "Wrong kind of conversation" msgstr "Wrong kind of conversation" @@ -12961,7 +13224,7 @@ msgid "You are accessing Bluesky from a region that legally requires us to verif msgstr "" #. placeholder {0}: sanitizeHandle(profile.handle, '@') -#: src/components/dms/MessageItem.tsx:636 +#: src/components/dms/MessageItem.tsx:605 msgid "You are blocking {0}" msgstr "You are blocking {0}" @@ -12999,7 +13262,7 @@ msgstr "" msgid "You are no longer live" msgstr "" -#: src/view/com/composer/state/video.ts:427 +#: src/view/com/composer/state/video.ts:412 msgid "You are not allowed to upload videos." msgstr "" @@ -13053,7 +13316,7 @@ msgstr "" msgid "You can continue ongoing conversations regardless of which setting you choose." msgstr "" -#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:149 +#: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:150 msgid "You can now choose to be notified when specific people post. If there’s someone you want timely updates from, go to their profile and find the new bell icon near the follow button." msgstr "" @@ -13062,7 +13325,7 @@ msgstr "" msgid "You can now sign in with your new password." msgstr "" -#: src/view/com/composer/Composer.tsx:1325 +#: src/view/com/composer/Composer.tsx:1307 msgid "You can only save drafts up to 1000 characters." msgstr "" @@ -13070,11 +13333,11 @@ msgstr "" msgid "You can only save drafts up to 1000 characters. Would you like to discard this post before viewing your drafts?" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:431 +#: src/view/com/composer/SelectMediaButton.tsx:435 msgid "You can only select one GIF at a time." msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:425 +#: src/view/com/composer/SelectMediaButton.tsx:429 msgid "You can only select one video at a time." msgstr "" @@ -13087,7 +13350,7 @@ msgid "You can read chat history but can’t send new messages." msgstr "You can read chat history but can’t send new messages." #. Error message for maximum number of images that can be selected to add to a post, currently 4 but may change. -#: src/view/com/composer/SelectMediaButton.tsx:417 +#: src/view/com/composer/SelectMediaButton.tsx:421 msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." msgstr "" @@ -13128,6 +13391,10 @@ msgstr "" msgid "You got here first" msgstr "" +#: src/components/intents/GroupChatJoinDialog.tsx:136 +msgid "You have been removed from this group." +msgstr "You have been removed from this group." + #: src/components/moderation/ModerationDetailsDialog.tsx:77 #: src/lib/moderation/useModerationCauseDescription.ts:58 #: src/lib/moderation/useModerationCauseDescription.ts:66 @@ -13197,7 +13464,7 @@ msgstr "" msgid "You have temporarily reached the limit for video uploads. Please try again later." msgstr "" -#: src/view/com/composer/Composer.tsx:1315 +#: src/view/com/composer/Composer.tsx:1297 msgid "You have unsaved changes to this draft, would you like to save them?" msgstr "" @@ -13267,7 +13534,7 @@ msgstr "" msgid "You must grant access to your photo library to save a QR code" msgstr "" -#: src/view/com/composer/SelectMediaButton.tsx:460 +#: src/view/com/composer/SelectMediaButton.tsx:464 msgid "You need to allow access to your media library." msgstr "" @@ -13285,7 +13552,7 @@ msgid "You probably want to restart the app now." msgstr "" #. placeholder {0}: reaction.value -#: src/components/dms/MessageItem.tsx:287 +#: src/components/dms/MessageItem.tsx:263 msgid "You reacted {0}" msgstr "" @@ -13394,7 +13661,7 @@ msgstr "" msgid "You've reached the end of your feed! Find some more accounts to follow." msgstr "" -#: src/view/com/composer/Composer.tsx:582 +#: src/view/com/composer/Composer.tsx:576 msgid "You've reached the maximum number of drafts" msgstr "" @@ -13406,11 +13673,11 @@ msgstr "" msgid "You've reached the start of the active content." msgstr "" -#: src/view/com/composer/state/video.ts:438 +#: src/view/com/composer/state/video.ts:423 msgid "You've reached your daily limit for video uploads (too many bytes)" msgstr "" -#: src/view/com/composer/state/video.ts:442 +#: src/view/com/composer/state/video.ts:427 msgid "You've reached your daily limit for video uploads (too many videos)" msgstr "" @@ -13430,15 +13697,15 @@ msgstr "" msgid "Your account has been suspended" msgstr "" -#: src/view/com/composer/state/video.ts:446 +#: src/view/com/composer/state/video.ts:431 msgid "Your account is not yet old enough to upload videos. Please try again later." msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:674 +#: src/components/dms/InitiateChatFlow.tsx:675 msgid "Your account is too new" msgstr "Your account is too new" -#: src/components/dms/InitiateChatFlow.tsx:675 +#: src/components/dms/InitiateChatFlow.tsx:676 msgid "Your account must be at least 7 days old to create a new group chat." msgstr "Your account must be at least 7 days old to create a new group chat." @@ -13521,7 +13788,7 @@ msgstr "" msgid "Your full handle will be <0>@{0}" msgstr "" -#: src/Navigation.tsx:449 +#: src/Navigation.tsx:451 #: src/screens/Search/modules/ExploreInterestsCard.tsx:68 #: src/screens/Settings/ContentAndMediaSettings.tsx:94 #: src/screens/Settings/ContentAndMediaSettings.tsx:97 @@ -13550,9 +13817,9 @@ msgstr "" msgid "Your muted words" msgstr "" -#: src/screens/Messages/components/InviteLinkDialog.tsx:179 -msgid "Your name, avatar, and the name of the group chat will be visible to everyone." -msgstr "Your name, avatar, and the name of the group chat will be visible to everyone." +#: src/screens/Messages/components/InviteLinkDialog.tsx:189 +msgid "Your name, avatar, the name of the group chat, and the number of members will be visible to everyone." +msgstr "Your name, avatar, the name of the group chat, and the number of members will be visible to everyone." #: src/screens/Settings/components/ChangePasswordDialog.tsx:72 msgid "Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on." @@ -13562,11 +13829,11 @@ msgstr "" msgid "Your password must be at least 8 characters long." msgstr "" -#: src/view/com/composer/Composer.tsx:1046 +#: src/view/com/composer/Composer.tsx:1028 msgid "Your post was sent" msgstr "" -#: src/view/com/composer/Composer.tsx:1043 +#: src/view/com/composer/Composer.tsx:1025 msgid "Your posts were sent" msgstr "" @@ -13574,7 +13841,7 @@ msgstr "" msgid "Your preferred language" msgstr "" -#: src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx:100 +#: src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx:102 #: src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx:53 msgid "Your profile picture" msgstr "" @@ -13587,12 +13854,12 @@ msgstr "" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "" -#: src/view/com/composer/Composer.tsx:1045 +#: src/view/com/composer/Composer.tsx:1027 msgid "Your reply was sent" msgstr "" #. placeholder {0}: state.selectedLabeler?.creator.displayName -#: src/components/moderation/ReportDialog/index.tsx:517 +#: src/components/moderation/ReportDialog/index.tsx:519 msgid "Your report will be sent to <0>{0}." msgstr "" @@ -13600,7 +13867,7 @@ msgstr "" msgid "Your selected interests help us serve you content you care about." msgstr "" -#: src/view/com/composer/Composer.tsx:1350 +#: src/view/com/composer/Composer.tsx:1332 msgid "Your thread has empty posts that will be skipped. The remaining posts will be published as a thread." msgstr "Your thread has empty posts that will be skipped. The remaining posts will be published as a thread." diff --git a/src/routes.ts b/src/routes.ts index 7ae73dd6e1..f7e73ff2c2 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -74,6 +74,7 @@ export const router = new Router({ MessagesInbox: '/messages/inbox', MessagesConversation: '/messages/:conversation', MessagesConversationSettings: '/messages/:conversation/settings', + MessagesJoinRequests: '/messages/:conversation/requests', // starter packs Start: '/start/:name/:rkey', StarterPackEdit: '/starter-pack/edit/:rkey', diff --git a/src/screens/Messages/ConversationSettings/MembersAndRequests.tsx b/src/screens/Messages/ConversationSettings/MembersAndRequests.tsx index b9d9ecc2b2..bb3ebafb1c 100644 --- a/src/screens/Messages/ConversationSettings/MembersAndRequests.tsx +++ b/src/screens/Messages/ConversationSettings/MembersAndRequests.tsx @@ -1,26 +1,31 @@ import {View} from 'react-native' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' +import {useNavigation} from '@react-navigation/native' +import {type NavigationProp} from '#/lib/routes/types' import {atoms as a, useTheme} from '#/alf' -import {InlineLinkText} from '#/components/Link' +import {type ConvoWithDetails} from '#/components/dms/util' +import {createStaticClick, InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' export function MembersAndRequests({ - memberCount, - memberLimit, + convo, requestCount, hasMoreRequests, isOwner, }: { - memberCount: number - memberLimit: number + convo: Extract requestCount: number hasMoreRequests: boolean isOwner: boolean }) { const t = useTheme() const {t: l} = useLingui() + const navigation = useNavigation() + + const memberCount = convo.details.memberCount + const memberLimit = convo.details.memberLimit return ( @@ -42,8 +47,11 @@ export function MembersAndRequests({ + {...createStaticClick(() => { + navigation.navigate('MessagesJoinRequests', { + conversation: convo.view.id, + }) + })}> {hasMoreRequests ? l({ message: `${requestCount}+ requests`, diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 056afa8b5c..5e4ad91424 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -247,8 +247,7 @@ function GroupSettings({ case 'MEMBERS_AND_REQUESTS': return ( void +} + +export function JoinRequest({setScreenState}: Props) { + const t = useTheme() + const {t: l} = useLingui() + + const {gtMobile, gtTablet} = useBreakpoints() + const moderationOpts = useModerationOpts() + + // Get code from context (logged-out only) + const contextJoinRequest = useActiveGroupChatJoinRequest() + const code = contextJoinRequest?.code + + const {data, error} = useJoinLinkPreviewsQuery({ + codes: code ? [code] : undefined, + hasSession: false, + }) + + const isDarkMode = t.name !== 'light' + const background = gtMobile + ? isDarkMode + ? desktopDarkBg + : desktopLightBg + : isDarkMode + ? mobileDarkBg + : mobileLightBg + + const requiresApproval = data?.joinLinkPreviews[0]?.requireApproval + const requiresFollow = + data?.joinLinkPreviews[0]?.joinRule === 'followedByOwner' + + return ( + + + + {error ? ( + + + + {l`This invite link has expired`} + + + + ) : data && moderationOpts ? ( + + + + + + Group chat + + + + + + {data.joinLinkPreviews[0].memberCount}/ + {data.joinLinkPreviews[0].memberLimit} + + + + + + {data.joinLinkPreviews[0].name} + + + + + + + By{' '} + {createSanitizedDisplayName( + data.joinLinkPreviews[0].owner, + true, + moderateProfile( + data.joinLinkPreviews[0].owner, + moderationOpts, + ).ui('displayName'), + )} + + + + + + {sanitizeHandle(data.joinLinkPreviews[0].owner.handle, '@')} + + + + {requiresApproval + ? l`Sign in to request access to this group chat.` + : l`Sign in to accept invite.`}{' '} + {requiresFollow && + l`Only people ${createSanitizedDisplayName( + data.joinLinkPreviews[0].owner, + true, + moderateProfile( + data.joinLinkPreviews[0].owner, + moderationOpts, + ).ui('displayName'), + )} follows can join.`} + + + + ) : null} + + + + ) +} + +function Wrapper({children}: React.PropsWithChildren) { + const t = useTheme() + + const isDarkMode = t.name !== 'light' + + return ( + <> + + + {children} + + + ) +} + +function ActionButtons({ + setScreenState, +}: { + setScreenState: (state: LoggedOutScreenState) => void +}) { + const t = useTheme() + const {t: l} = useLingui() + const isDarkMode = t.name !== 'light' + + return ( + + + + + ) +} diff --git a/src/screens/Messages/JoinRequests.tsx b/src/screens/Messages/JoinRequests.tsx new file mode 100644 index 0000000000..98952aee34 --- /dev/null +++ b/src/screens/Messages/JoinRequests.tsx @@ -0,0 +1,490 @@ +import {useState} from 'react' +import {View} from 'react-native' +import { + ChatBskyGroupApproveJoinRequest, + type ChatBskyGroupListJoinRequests, + ChatBskyGroupRejectJoinRequest, +} from '@atproto/api' +import {Plural, Trans, useLingui} from '@lingui/react/macro' +import {useNavigation} from '@react-navigation/native' +import {type InfiniteData, useQueryClient} from '@tanstack/react-query' + +import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' +import {isNetworkError} from '#/lib/hooks/useCleanError' +import { + type CommonNavigatorParams, + type NativeStackScreenProps, + type NavigationProp, +} from '#/lib/routes/types' +import {logger} from '#/logger' +import {ConvoProvider, useConvo} from '#/state/messages/convo' +import {ConvoStatus} from '#/state/messages/convo/types' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useJoinRequestMutation} from '#/state/queries/messages/join-requests' +import { + createListJoinRequestsQueryKey, + useListJoinRequestsQuery, +} from '#/state/queries/messages/list-join-requests' +import {useSession} from '#/state/session' +import {List} from '#/view/com/util/List' +import {atoms as a, useTheme} from '#/alf' +import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' +import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {type ConvoWithDetails} from '#/components/dms/util' +import {Error} from '#/components/Error' +import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate' +import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' +import {KnownFollowers} from '#/components/KnownFollowers' +import * as Layout from '#/components/Layout' +import {Loader} from '#/components/Loader' +import * as ProfileCard from '#/components/ProfileCard' +import * as Toast from '#/components/Toast' +import {Text} from '#/components/Typography' +import type * as bsky from '#/types/bsky' +import {InviteLinkDialog} from './components/InviteLinkDialog' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'MessagesJoinRequests' +> + +export function MessagesJoinRequestsScreen(props: Props) { + const {t: l} = useLingui() + const aaCopy = useAgeAssuranceCopy() + return ( + + + + ) +} + +function MessagesJoinRequestsScreenInner({route}: Props) { + const convoId = route.params.conversation + + return ( + + + + + + ) +} + +function JoinRequestsInner() { + const {t: l} = useLingui() + const convoState = useConvo() + const navigation = useNavigation() + + if (convoState.status === ConvoStatus.Error) { + return ( + <> +
+ convoState.error.retry()} + sideBorders={false} + /> + + ) + } + + if (!convoState.convo) { + return ( + <> +
+ + + + + ) + } + + if (convoState.convo.kind !== 'group') { + return ( + { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.replace('Messages', {animation: 'pop'}) + } + }} + /> + ) + } + + return +} + +function JoinRequestsList({ + convo, +}: { + convo: Extract +}) { + const t = useTheme() + const {t: l} = useLingui() + const moderationOpts = useModerationOpts() + const bottomBarOffset = useBottomBarOffset() + const {currentAccount} = useSession() + const navigation = useNavigation() + const queryClient = useQueryClient() + const inviteLinkControl = Dialog.useDialogControl() + + const getRemainingRequestCount = () => { + const data = queryClient.getQueryData< + InfiniteData + >(createListJoinRequestsQueryKey({convoId: convo.view.id})) + return data?.pages.reduce((sum, page) => sum + page.requests.length, 0) ?? 0 + } + + const [isPTRing, setIsPTRing] = useState(false) + const [footerHeight, setFooterHeight] = useState(0) + + const owner = convo.primaryMember + const isOwner = !!owner && owner.did === currentAccount?.did + + const { + data: joinRequestsData, + isPending, + isError, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + refetch, + } = useListJoinRequestsQuery({ + convoId: convo.view.id, + }) + + const items = + joinRequestsData?.pages.flatMap(page => + page.requests.map(request => request.requestedBy), + ) ?? [] + const requestCount = + joinRequestsData?.pages.reduce( + (sum, page) => sum + page.requests.length, + 0, + ) ?? 0 + + const {mutate: approveJoinRequest, isPending: isApprovePending} = + useJoinRequestMutation('approve', convo.view.id, { + onSuccess: () => { + Toast.show(l`Request approved.`) + if (getRemainingRequestCount() < 1) { + navigation.replace('MessagesConversationSettings', { + conversation: convo.view.id, + }) + } + }, + onError: error => { + let errorMessage = l`Failed to accept join request` + if (isNetworkError(error)) { + errorMessage = l`A network error occurred. Please check your internet connection.` + } else if ( + error instanceof ChatBskyGroupApproveJoinRequest.InvalidConvoError + ) { + errorMessage = l`Conversation not found.` + } else if ( + error instanceof ChatBskyGroupApproveJoinRequest.InsufficientRoleError + ) { + errorMessage = l`Only admins can accept join requests.` + } else if ( + error instanceof + ChatBskyGroupApproveJoinRequest.MemberLimitReachedError + ) { + errorMessage = l`The member limit has been reached.` + } + Toast.show(errorMessage, {type: 'error'}) + }, + }) + + const {mutate: rejectJoinRequest, isPending: isRejectPending} = + useJoinRequestMutation('reject', convo.view.id, { + onSuccess: () => { + Toast.show(l`Request ignored.`) + if (getRemainingRequestCount() < 1) { + navigation.replace('MessagesConversationSettings', { + conversation: convo.view.id, + }) + } + }, + onError: error => { + let errorMessage = l`Failed to ignore join request` + if (isNetworkError(error)) { + errorMessage = l`A network error occurred. Please check your internet connection.` + } else if ( + error instanceof ChatBskyGroupRejectJoinRequest.InvalidConvoError + ) { + errorMessage = l`Conversation not found.` + } else if ( + error instanceof ChatBskyGroupRejectJoinRequest.InsufficientRoleError + ) { + errorMessage = l`Only admins can ignore join requests.` + } + Toast.show(errorMessage, {type: 'error'}) + }, + }) + + const isMutating = isApprovePending || isRejectPending + + const renderItem = ({item}: {item: bsky.profile.AnyProfileView}) => { + if (!moderationOpts) return null + return ( + + + + + + + + + + + + + approveJoinRequest({member: item.did})} + /> + rejectJoinRequest({member: item.did})} + /> + + + + + ) + } + + const footer = ( + setFooterHeight(evt.nativeEvent.layout.height)} + style={[ + a.absolute, + a.left_0, + a.right_0, + {bottom: 0}, + a.px_xl, + a.border_t, + t.atoms.bg, + t.atoms.border_contrast_low, + { + paddingTop: a.py_lg.paddingTop, + paddingBottom: a.py_lg.paddingBottom + bottomBarOffset, + }, + ]}> + + + ) + + const onEndReached = async () => { + if (isFetchingNextPage || !hasNextPage || isError) return + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more join requests', {message: err}) + } + } + + const onRefresh = async () => { + setIsPTRing(true) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh group chat requests', {message: err}) + } + setIsPTRing(false) + } + + if (isError) { + return ( + <> +
+ + + + Unable to fetch join requests. + + + + + ) + } + + const showFooter = isOwner + + return ( + <> +
+ item.did} + renderItem={renderItem} + ListEmptyComponent={ + isPending ? ( + + + + ) : null + } + contentContainerStyle={ + showFooter ? {paddingBottom: footerHeight} : undefined + } + scrollIndicatorInsets={showFooter ? {bottom: footerHeight} : undefined} + refreshing={isPTRing} + onEndReached={() => void onEndReached()} + onRefresh={() => void onRefresh()} + keyboardDismissMode="on-drag" + sideBorders={false} + desktopFixedHeight + /> + {showFooter ? footer : null} + {owner && moderationOpts && ( + + )} + + ) +} + +function Header({ + count, + hasMoreRequests, +}: { + count?: number + hasMoreRequests?: boolean +}) { + return ( + + + + + {count === undefined ? ( + Requests to join + ) : hasMoreRequests ? ( + + ) : ( + + )} + + + + + ) +} + +function AcceptButton({ + disabled, + onPress, +}: { + disabled?: boolean + onPress: () => void +}) { + const {t: l} = useLingui() + + return ( + + ) +} + +function RejectButton({ + disabled, + onPress, +}: { + disabled?: boolean + onPress: () => void +}) { + const {t: l} = useLingui() + + return ( + + ) +} diff --git a/src/screens/Messages/components/InviteLinkDialog.tsx b/src/screens/Messages/components/InviteLinkDialog.tsx index 755eec2108..6e898a0a9c 100644 --- a/src/screens/Messages/components/InviteLinkDialog.tsx +++ b/src/screens/Messages/components/InviteLinkDialog.tsx @@ -5,8 +5,7 @@ import { moderateProfile, type ModerationOpts, } from '@atproto/api' -import {plural} from '@lingui/core/macro' -import {Trans, useLingui} from '@lingui/react/macro' +import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -178,11 +177,7 @@ export function InviteLinkDialog({ Group chats can only have a maximum of{' '} - {plural(convo.details.memberLimit, { - one: '# person', - other: '# people', - })} - . + . @@ -297,8 +292,8 @@ export function InviteLinkDialog({ const linkEnabled = joinLink?.enabledStatus === 'enabled' const linkDisabled = joinLink?.enabledStatus === 'disabled' const joinLinkURI = joinLink?.code - ? `https://bsky.app/chat/${joinLink.code}` - : 'https://bsky.app/chat' + ? `https://bsky.app/c/${joinLink.code}` + : 'https://bsky.app/' const createdAt = joinLink ? new Date(joinLink.createdAt) : null const currentOption = whoCanJoinOptions.find( diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 99b725267e..fbebdc0745 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -56,6 +56,7 @@ import {MessageListError} from '#/screens/Messages/components/MessageListError' import {atoms as a, platform, tokens, useTheme, web} from '#/alf' import {DateDivider} from '#/components/dms/DateDivider' import {MessageItem} from '#/components/dms/MessageItem' +import {MessageOverlays} from '#/components/dms/MessageOverlays' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {SystemMessageGroup} from '#/components/dms/SystemMessageGroup' import {SystemMessageItem} from '#/components/dms/SystemMessageItem' @@ -498,127 +499,133 @@ export function MessagesList({ return ( - - {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} - - - - - {convoState.hasAllHistory ? ( - convoState.convo?.kind === 'group' ? ( - - ) : ( - - ) - ) : null} - - } - // native only (prop is not supported on web) - renderScrollComponent={renderScrollComponent} - contentContainerStyle={{ - paddingBottom: platform({ - // ios is slightly larger as the input has no top padding - ios: tokens.space.lg, - android: tokens.space.md, - web: 0, // web uses ListFooterComponent instead for scroll reasons - }), - }} - ListFooterComponent={ - - } - style={[ - web({ - scrollbarWidth: 'thin', - scrollbarColor: `${t.palette.contrast_100} transparent`, - scrollbarGutter: 'stable', - }), - ]} - pointerEvents={!hasScrolled ? 'none' : 'auto'} - contentInset={{top: transparentHeaderHeight}} - scrollIndicatorInsets={{top: transparentHeaderHeight}} - /> - - - - {footer ?? ( - - {({loading}) => - ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? ( - - void onSendMessage(message) - } - hasEmbed={!!embedUri} - setEmbed={setEmbed} - loading={loading}> - + + {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} + + + + + {convoState.hasAllHistory ? ( + convoState.convo?.kind === 'group' ? ( + + ) : ( + + ) + ) : null} + + } + // native only (prop is not supported on web) + renderScrollComponent={renderScrollComponent} + contentContainerStyle={{ + paddingBottom: platform({ + // ios is slightly larger as the input has no top padding + ios: tokens.space.lg, + android: tokens.space.md, + web: 0, // web uses ListFooterComponent instead for scroll reasons + }), + }} + ListFooterComponent={ + + } + style={[ + web({ + scrollbarWidth: 'thin', + scrollbarColor: `${t.palette.contrast_100} transparent`, + scrollbarGutter: 'stable', + }), + ]} + pointerEvents={!hasScrolled ? 'none' : 'auto'} + contentInset={{top: transparentHeaderHeight}} + scrollIndicatorInsets={{top: transparentHeaderHeight}} + /> + + + + {footer ?? ( + + {({loading}) => + ax.features.enabled( + ax.features.DmsNewMessageComposerEnable, + ) ? ( + + void onSendMessage(message) + } + hasEmbed={!!embedUri} setEmbed={setEmbed} - /> - - ) : ( - - + + + ) : ( + - - ) - } - - )} - - + loading={loading}> + + + ) + } + + )} + + - {newMessagesPill.show && } + {newMessagesPill.show && ( + + )} + ) } diff --git a/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx b/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx index cb76485af6..72d92a24ea 100644 --- a/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx +++ b/src/screens/Messages/components/MessagesListGroupInfoPanel.tsx @@ -43,14 +43,11 @@ export function MessagesListGroupInfoPanel({ }, }) - // TODO Enable this once the feature is working end-to-end. -dsb - // const joinLink = groupConvo?.details.joinLink - const isJoinLinkEnabled = false - // (isOwner && groupConvo) || - // (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled') - const isOwner = convo.primaryMember?.did === currentAccount?.did + const isJoinLinkEnabled = + isOwner || convo.details.joinLink?.enabledStatus === 'enabled' + const members = (convo.members ?? []).filter( profile => profile.did !== currentAccount?.did, ) diff --git a/src/screens/Messages/components/RequestListItem.tsx b/src/screens/Messages/components/RequestListItem.tsx index 64dbebb164..79eab480ae 100644 --- a/src/screens/Messages/components/RequestListItem.tsx +++ b/src/screens/Messages/components/RequestListItem.tsx @@ -28,6 +28,9 @@ export function RequestListItem({ const isDeletedAccount = !convo.primaryMember || convo.primaryMember.handle === 'missing.invalid' + const canAcceptRequest = + convo.kind === 'direct' || convo.details.lockStatus === 'unlocked' + return ( @@ -65,7 +68,9 @@ export function RequestListItem({ ]}> {convo.primaryMember && !isDeletedAccount ? ( <> - + {canAcceptRequest ? ( + + ) : null} {page === 1 && ( {_(msg`Your )} diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index e477528f1b..11dfd35ceb 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -28,11 +28,11 @@ import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' -import {useProgressGuideControls} from '#/state/shell/progress-guide' import { useActiveStarterPack, useSetActiveStarterPack, -} from '#/state/shell/starter-pack' +} from '#/state/shell/landing' +import {useProgressGuideControls} from '#/state/shell/progress-guide' import { OnboardingControls, OnboardingHeaderSlot, diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index a9cc64396a..eaa68d9254 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -1,16 +1,18 @@ -import {useCallback, useEffect, useRef, useState} from 'react' +import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {View} from 'react-native' import {type ModerationOpts} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation, useQueryClient} from '@tanstack/react-query' +import * as bcp47Match from 'bcp-47-match' import {wait} from '#/lib/async/wait' import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' +import {useLanguagePrefs} from '#/state/preferences' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAgent, useSession} from '#/state/session' import { @@ -51,6 +53,19 @@ export function StepSuggestedAccounts() { // so we can enable/disable the button without having to dig through the shadow cache const [followedUsers, setFollowedUsers] = useState([]) + /* + * Special language handling copied wholesale from the Explore screen + */ + const {contentLanguages} = useLanguagePrefs() + const useFullExperience = useMemo(() => { + if (contentLanguages.length === 0) return true + return bcp47Match.basicFilter('en', contentLanguages).length > 0 + }, [contentLanguages]) + const interestsDisplayNames = useInterestsDisplayNames() + const interests = Object.keys(interestsDisplayNames) + .sort(boostInterests(popularInterests)) + .sort(boostInterests(state.interestsStepResults.selectedInterests)) + const { data: suggestedUsers, isLoading, @@ -58,8 +73,8 @@ export function StepSuggestedAccounts() { isRefetching, refetch, } = useSuggestedOnboardingUsers({ - category: selectedInterest, - search: false, + category: selectedInterest || (useFullExperience ? null : interests[0]), + search: !useFullExperience, overrideInterests: state.interestsStepResults.selectedInterests, }) @@ -90,6 +105,7 @@ export function StepSuggestedAccounts() { ax.metric('suggestedUser:follow', { logContext: 'Onboarding', location: 'FollowAll', + recSource: !useFullExperience ? 'Search' : undefined, recId: suggestedUsers?.recId, position: i, suggestedDid: did, @@ -140,6 +156,7 @@ export function StepSuggestedAccounts() { seenProfilesRef.current.add(did) ax.metric('suggestedUser:seen', { logContext: 'Onboarding', + recSource: !useFullExperience ? 'Search' : undefined, recId: suggestedUsers?.recId, position, suggestedDid: did, @@ -147,7 +164,7 @@ export function StepSuggestedAccounts() { }) } }, - [ax, selectedInterest, suggestedUsers?.recId], + [ax, selectedInterest, suggestedUsers?.recId, useFullExperience], ) useEffect(() => { @@ -233,6 +250,7 @@ export function StepSuggestedAccounts() { position={index} category={selectedInterest} onSeen={onProfileSeen} + recSource={!useFullExperience ? 'Search' : undefined} recId={suggestedUsers.recId} /> ))} diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx index 0bef3a22c3..e37e57edbf 100644 --- a/src/screens/Profile/components/GermButton.tsx +++ b/src/screens/Profile/components/GermButton.tsx @@ -105,6 +105,7 @@ function GermLogo({size}: {size: 'small' | 'large'}) { source={require('../../../../assets/images/germ_logo.webp')} accessibilityIgnoresInvertColors={false} contentFit="cover" + useAppleWebpCodec style={[ a.rounded_full, size === 'large' ? {width: 32, height: 32} : {width: 16, height: 16}, diff --git a/src/screens/Settings/AutomationLabelSettings.tsx b/src/screens/Settings/AutomationLabelSettings.tsx index f4c6823d63..423bc0073a 100644 --- a/src/screens/Settings/AutomationLabelSettings.tsx +++ b/src/screens/Settings/AutomationLabelSettings.tsx @@ -123,7 +123,11 @@ export function AutomationLabelSettingsScreen({}: Props) { paddingRight: 20, // helps visually center }, ]}> - + diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index fe67773a40..d98b6b4fea 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -9,7 +9,7 @@ import {FEEDBACK_FORM_URL} from '#/lib/constants' import {logger} from '#/logger' import {useServiceQuery} from '#/state/queries/service' import {useStarterPackQuery} from '#/state/queries/starter-packs' -import {useActiveStarterPack} from '#/state/shell/starter-pack' +import {useActiveStarterPack} from '#/state/shell/landing' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import { initialState, diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx index 2b08212794..f15d259e1f 100644 --- a/src/screens/StarterPack/StarterPackLandingScreen.tsx +++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx @@ -19,7 +19,7 @@ import {useStarterPackQuery} from '#/state/queries/starter-packs' import { useActiveStarterPack, useSetActiveStarterPack, -} from '#/state/shell/starter-pack' +} from '#/state/shell/landing' import {LoggedOutScreenState} from '#/view/com/auth/LoggedOut' import {formatCount} from '#/view/com/util/numeric/format' import {Logo} from '#/view/icons/Logo' diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index adb27ef2ec..ddbd5cd840 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -37,12 +37,12 @@ import { useStarterPackQuery, } from '#/state/queries/starter-packs' import {useAgent, useSession} from '#/state/session' +import {useSetActiveStarterPack} from '#/state/shell/landing' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import { ProgressGuideAction, useProgressGuideControls, } from '#/state/shell/progress-guide' -import {useSetActiveStarterPack} from '#/state/shell/starter-pack' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader' import {bulkWriteFollows} from '#/screens/Onboarding/util' diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx index 85bd4fddfd..acaf02453c 100644 --- a/src/screens/VideoFeed/components/Scrubber.tsx +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -23,8 +23,7 @@ import { import {useEventListener} from 'expo' import {type VideoPlayer} from 'expo-video' -import {tokens} from '#/alf' -import {atoms as a} from '#/alf' +import {atoms as a, tokens} from '#/alf' import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils' import {Text} from '#/components/Typography' diff --git a/src/state/preferences/moderation-opts.tsx b/src/state/preferences/moderation-opts.tsx index a6e36b7d2b..b8592d77e0 100644 --- a/src/state/preferences/moderation-opts.tsx +++ b/src/state/preferences/moderation-opts.tsx @@ -1,5 +1,5 @@ import {createContext, useContext, useMemo} from 'react' -import {BskyAgent, type ModerationOpts} from '@atproto/api' +import {AtpAgent, type ModerationOpts} from '@atproto/api' import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences' import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' @@ -43,7 +43,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ...moderationPrefs, labelers: moderationPrefs.labelers.length ? moderationPrefs.labelers - : BskyAgent.appLabelers.map(did => ({ + : AtpAgent.appLabelers.map(did => ({ did, labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, })), diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts new file mode 100644 index 0000000000..99a37b6b06 --- /dev/null +++ b/src/state/queries/join-links.ts @@ -0,0 +1,74 @@ +import {AtpAgent} from '@atproto/api' +import {useQuery, useQueryClient} from '@tanstack/react-query' + +import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants' +import {logger} from '#/logger' +import {STALE} from '#/state/queries/index' +import {createQueryKey} from '#/state/queries/util' +import {useAgent} from '#/state/session' + +const joinLinkPreviewQueryKeyRoot = 'join-link-preview' + +export const createJoinLinkPreviewQueryKey = (args: { + codes: string[] + hasSession: boolean +}) => + createQueryKey(joinLinkPreviewQueryKeyRoot, args, { + persistedVersion: 1, + }) + +export function useJoinLinkPreviewsQuery({ + codes, + hasSession, + staleTime = STALE.MINUTES.ONE, +}: { + codes?: string[] + hasSession: boolean + staleTime?: number +}) { + const agent = useAgent() + + return useQuery({ + queryKey: createJoinLinkPreviewQueryKey({codes: codes ?? [], hasSession}), + queryFn: async () => { + if (!codes) throw new Error('No invite code') + try { + const previewAgent = new AtpAgent({service: CHAT_SERVICE}) + const res = hasSession + ? await agent.chat.bsky.group.getJoinLinkPreviews( + {codes}, + {headers: DM_SERVICE_HEADERS}, + ) + : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) + return res.data + } catch (error) { + logger.error('Failed to fetch join link preview', {safeMessage: error}) + throw error + } + }, + enabled: codes != null && codes.length > 0, + staleTime, + }) +} + +export function usePrefetchJoinLinkPreviews() { + const agent = useAgent() + const queryClient = useQueryClient() + + return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => { + return queryClient.prefetchQuery({ + queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}), + queryFn: async () => { + const previewAgent = new AtpAgent({service: CHAT_SERVICE}) + const res = hasSession + ? await agent.chat.bsky.group.getJoinLinkPreviews( + {codes}, + {headers: DM_SERVICE_HEADERS}, + ) + : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) + return res.data + }, + staleTime: STALE.MINUTES.ONE, + }) + } +} diff --git a/src/state/queries/messages/join-requests.ts b/src/state/queries/messages/join-requests.ts new file mode 100644 index 0000000000..a3eda671e8 --- /dev/null +++ b/src/state/queries/messages/join-requests.ts @@ -0,0 +1,129 @@ +import { + type ChatBskyActorDefs, + type ChatBskyGroupApproveJoinRequest, + type ChatBskyGroupListJoinRequests, + type ChatBskyGroupRejectJoinRequest, +} from '@atproto/api' +import { + type InfiniteData, + useMutation, + useQueryClient, +} from '@tanstack/react-query' + +import {DM_SERVICE_HEADERS} from '#/lib/constants' +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import {listConvoMembersQueryKey} from './list-convo-members' +import {createListJoinRequestsQueryKey} from './list-join-requests' + +type JoinRequestAction = 'approve' | 'reject' + +type JoinRequestOutput = A extends 'approve' + ? ChatBskyGroupApproveJoinRequest.OutputSchema + : ChatBskyGroupRejectJoinRequest.OutputSchema + +export function useJoinRequestMutation( + action: A, + convoId: string | undefined, + { + onSuccess, + onError, + }: { + onSuccess?: (data: JoinRequestOutput) => void + onError?: (error: Error) => void + }, +) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async ({member}: {member: string}) => { + if (!convoId) throw new Error('No convoId provided') + const endpoint = + action === 'approve' + ? agent.chat.bsky.group.approveJoinRequest + : agent.chat.bsky.group.rejectJoinRequest + const {data} = await endpoint( + {convoId, member}, + {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, + ) + return data as JoinRequestOutput + }, + onMutate: ({member}) => { + if (!convoId) return + + const requestsKey = createListJoinRequestsQueryKey({convoId}) + const prevRequests = + queryClient.getQueryData< + InfiniteData + >(requestsKey) + + const requestedByProfile = prevRequests?.pages + .flatMap(page => page.requests) + .find(request => request.requestedBy.did === member)?.requestedBy + + queryClient.setQueryData< + InfiniteData + >(requestsKey, prev => { + if (!prev?.pages) return prev + return { + ...prev, + pages: prev.pages.map(page => ({ + ...page, + requests: page.requests.filter( + request => request.requestedBy.did !== member, + ), + })), + } + }) + + let prevMembers: ChatBskyActorDefs.ProfileViewBasic[] | undefined + if (action === 'approve' && requestedByProfile) { + const membersKey = listConvoMembersQueryKey(convoId) + prevMembers = + queryClient.getQueryData( + membersKey, + ) + queryClient.setQueryData( + membersKey, + prev => { + if (!prev) return prev + if (prev.some(m => m.did === member)) return prev + return [...prev, requestedByProfile] + }, + ) + } + + return {prevRequests, prevMembers} + }, + onSuccess: data => { + if (convoId) { + void queryClient.invalidateQueries({ + queryKey: createListJoinRequestsQueryKey({convoId}), + }) + if (action === 'approve') { + void queryClient.invalidateQueries({ + queryKey: listConvoMembersQueryKey(convoId), + }) + } + } + onSuccess?.(data) + }, + onError: (error, _variables, context) => { + logger.error(error) + if (convoId && context?.prevRequests) { + queryClient.setQueryData( + createListJoinRequestsQueryKey({convoId}), + context.prevRequests, + ) + } + if (convoId && action === 'approve' && context?.prevMembers) { + queryClient.setQueryData( + listConvoMembersQueryKey(convoId), + context.prevMembers, + ) + } + onError?.(error) + }, + }) +} diff --git a/src/state/queries/messages/request-join-group-chat.ts b/src/state/queries/messages/request-join-group-chat.ts new file mode 100644 index 0000000000..04e63395f7 --- /dev/null +++ b/src/state/queries/messages/request-join-group-chat.ts @@ -0,0 +1,37 @@ +import {type ChatBskyGroupRequestJoin} from '@atproto/api' +import {useMutation} from '@tanstack/react-query' + +import {DM_SERVICE_HEADERS} from '#/lib/constants' +import {logger} from '#/logger' +import {useAgent, useSession} from '#/state/session' + +export function useRequestJoinGroupChat({ + onSuccess, + onError, +}: { + onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void + onError?: (error: Error) => void +} = {}) { + const agent = useAgent() + const {hasSession} = useSession() + + return useMutation({ + mutationFn: async ({code}: {code: string}) => { + if (!hasSession) throw new Error('Must be logged in to join') + if (!code) throw new Error('No invite code') + + const res = await agent.chat.bsky.group.requestJoin( + {code}, + {headers: DM_SERVICE_HEADERS}, + ) + return res.data + }, + onSuccess: data => { + onSuccess?.(data) + }, + onError: error => { + logger.error('Failed to join group chat', {safeMessage: error}) + onError?.(error) + }, + }) +} diff --git a/src/state/queries/messages/withdraw-join-group-chat.ts b/src/state/queries/messages/withdraw-join-group-chat.ts new file mode 100644 index 0000000000..92025cc2be --- /dev/null +++ b/src/state/queries/messages/withdraw-join-group-chat.ts @@ -0,0 +1,38 @@ +import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api' +import {useMutation} from '@tanstack/react-query' + +import {DM_SERVICE_HEADERS} from '#/lib/constants' +import {logger} from '#/logger' +import {useAgent, useSession} from '#/state/session' + +export function useWithdrawJoinGroupChatRequest({ + onSuccess, + onError, +}: { + onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void + onError?: (error: Error) => void +} = {}) { + const agent = useAgent() + const {hasSession} = useSession() + + return useMutation({ + mutationFn: async ({convoId}: {convoId: string}) => { + if (!hasSession) + throw new Error('Must be logged in to withdraw a join request') + if (!convoId) throw new Error('No convoId provided') + + const res = await agent.chat.bsky.group.withdrawJoinRequest( + {convoId}, + {headers: DM_SERVICE_HEADERS}, + ) + return res.data + }, + onSuccess: data => { + onSuccess?.(data) + }, + onError: error => { + logger.error('Failed to withdraw join request', {safeMessage: error}) + onError?.(error) + }, + }) +} diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 57d83cb5bb..ca885107d8 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -298,7 +298,7 @@ export function* findAllPostsInQueryData( if (AppBskyFeedDefs.isPostView(item.subject)) { const quotedPost = getEmbeddedPost(item.subject?.embed) if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) { - yield embedViewRecordToPostView(quotedPost!) + yield embedViewRecordToPostView(quotedPost) } } } diff --git a/src/state/queries/nuxs/util.ts b/src/state/queries/nuxs/util.ts index ef323b422e..dea35a9166 100644 --- a/src/state/queries/nuxs/util.ts +++ b/src/state/queries/nuxs/util.ts @@ -34,7 +34,7 @@ export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined { export function serializeAppNux(nux: AppNux): AppBskyActorDefs.Nux { const {data, ...rest} = nux - const schema = NuxSchemas[nux.id as Nux] + const schema = NuxSchemas[nux.id] const result: AppBskyActorDefs.Nux = { ...rest, diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index c1ec4a12a7..7197eb2980 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -85,7 +85,7 @@ export function useSuggestedFollowsByActorWithDismiss({ const profiles = useMemo(() => { return (data?.suggestions ?? []).map(profile => ({ - actor: profile as bsky.profile.AnyProfileView, + actor: profile, recId: data?.recId, })) }, [data?.suggestions, data?.recId]) diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 889c208c35..81abd958fb 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -196,7 +196,7 @@ export function sortAndAnnotateThreadItems( * `repliesSeenCounter` later on, since `repliesSeenCounter` * is 1-indexed and `replyIndex` is 0-indexed. */ - childMetadata!.replyIndex = + childMetadata.replyIndex = childParentMetadata.repliesSeenCounter } diff --git a/src/state/shell/landing.tsx b/src/state/shell/landing.tsx new file mode 100644 index 0000000000..f071c79e39 --- /dev/null +++ b/src/state/shell/landing.tsx @@ -0,0 +1,66 @@ +import {createContext, useContext, useState} from 'react' + +import {logger} from '#/logger' + +type StarterPackLanding = { + type: 'starterpack' + uri: string + isClip?: boolean +} + +type GroupChatJoinRequestLanding = { + type: 'groupchat' + uri: string + code: string +} + +type LandingType = StarterPackLanding | GroupChatJoinRequestLanding | undefined + +type SetContext = (v: LandingType) => void + +const stateContext = createContext(undefined) +stateContext.displayName = 'ActiveLandingStateContext' +const setContext = createContext((_: LandingType) => {}) +setContext.displayName = 'ActiveLandingSetContext' + +export function Provider({children}: {children: React.ReactNode}) { + const [state, setState] = useState() + + return ( + + {children} + + ) +} + +// Core hooks +export const useActiveLanding = () => useContext(stateContext) +export const useSetActiveLanding = () => useContext(setContext) + +// Filtered hooks for convenience +export const useActiveStarterPack = () => { + const landing = useActiveLanding() + return landing?.type === 'starterpack' ? landing : undefined +} + +export const useSetActiveStarterPack = () => { + const setLanding = useSetActiveLanding() + const currentLanding = useActiveLanding() + return (pack: {uri: string; isClip?: boolean} | undefined) => { + if (!pack) { + setLanding(undefined) + } else { + if (currentLanding && currentLanding.type !== 'starterpack') { + logger.debug( + `[landing] Replacing ${currentLanding.type} landing with starterpack`, + ) + } + setLanding({type: 'starterpack', ...pack}) + } + } +} + +export const useActiveGroupChatJoinRequest = () => { + const landing = useActiveLanding() + return landing?.type === 'groupchat' ? landing : undefined +} diff --git a/src/state/shell/logged-out.tsx b/src/state/shell/logged-out.tsx index fdc588dcce..fcb76818c4 100644 --- a/src/state/shell/logged-out.tsx +++ b/src/state/shell/logged-out.tsx @@ -1,7 +1,7 @@ import {createContext, useContext, useMemo, useState} from 'react' import {useSession} from '#/state/session' -import {useActiveStarterPack} from '#/state/shell/starter-pack' +import {useActiveLanding} from '#/state/shell/landing' import {IS_WEB} from '#/env' type State = { @@ -26,7 +26,12 @@ type Controls = { /** * The did of the account to populate the login form with. */ - requestedAccount?: (string & {}) | 'none' | 'new' | 'starterpack' + requestedAccount?: + | (string & {}) + | 'none' + | 'new' + | 'starterpack' + | 'groupchat' }) => void /** * Clears the requested account so that next time the logged out view is @@ -48,17 +53,34 @@ const ControlsContext = createContext({ }) ControlsContext.displayName = 'LoggedOutControlsContext' +function getRequestedAccountFromLanding( + landing: ReturnType, + hasSession: boolean, +): string | undefined { + if (hasSession || !landing) return undefined + + switch (landing.type) { + case 'starterpack': + return IS_WEB ? 'starterpack' : 'new' + case 'groupchat': + return 'groupchat' + default: + return undefined + } +} + export function Provider({children}: React.PropsWithChildren<{}>) { - const activeStarterPack = useActiveStarterPack() + const activeLanding = useActiveLanding() const {hasSession} = useSession() - const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession + + const requestedAccount = getRequestedAccountFromLanding( + activeLanding, + hasSession, + ) + const [state, setState] = useState({ - showLoggedOut: shouldShowStarterPack, - requestedAccountSwitchTo: shouldShowStarterPack - ? IS_WEB - ? 'starterpack' - : 'new' - : undefined, + showLoggedOut: Boolean(requestedAccount), + requestedAccountSwitchTo: requestedAccount, }) const controls = useMemo( diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 0cfea7cf16..91c0b9afca 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -9,6 +9,7 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale' import {STALE} from '#/state/queries' import {profilesQueryKey} from '#/state/queries/profile' import {useAgent, useSession} from '#/state/session' +import {useSetActiveLanding} from '#/state/shell/landing' import { useLoggedOutView, useLoggedOutViewControls, @@ -16,6 +17,7 @@ import { import {useEnableMinimalShellMode} from '#/state/shell/minimal-mode' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {Login} from '#/screens/Login' +import {JoinRequest} from '#/screens/Messages/JoinRequest' import {Signup} from '#/screens/Signup' import {LandingScreen} from '#/screens/StarterPack/StarterPackLandingScreen' import {atoms as a, native, tokens, useTheme} from '#/alf' @@ -29,9 +31,26 @@ enum ScreenState { S_Login, S_CreateAccount, S_StarterPack, + S_GroupChatJoinRequest, } export {ScreenState as LoggedOutScreenState} +function getInitialScreenState(requestedAccountSwitchTo?: string): ScreenState { + switch (requestedAccountSwitchTo) { + case 'new': + return ScreenState.S_CreateAccount + case 'starterpack': + return ScreenState.S_StarterPack + case 'groupchat': + return ScreenState.S_GroupChatJoinRequest + case undefined: + return ScreenState.S_LoginOrCreateAccount + default: + // DID or other string = login with that account + return ScreenState.S_Login + } +} + export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { const {_} = useLingui() const ax = useAnalytics() @@ -39,18 +58,11 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { const insets = useSafeAreaInsets() useEnableMinimalShellMode() const {requestedAccountSwitchTo} = useLoggedOutView() - const [screenState, setScreenState] = useState(() => { - if (requestedAccountSwitchTo === 'new') { - return ScreenState.S_CreateAccount - } else if (requestedAccountSwitchTo === 'starterpack') { - return ScreenState.S_StarterPack - } else if (requestedAccountSwitchTo != null) { - return ScreenState.S_Login - } else { - return ScreenState.S_LoginOrCreateAccount - } - }) + const initialScreenState = getInitialScreenState(requestedAccountSwitchTo) + const [screenState, setScreenState] = + useState(initialScreenState) const {clearRequestedAccount} = useLoggedOutViewControls() + const setActiveLanding = useSetActiveLanding() const queryClient = useQueryClient() const {accounts} = useSession() @@ -73,7 +85,9 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { onDismiss() } clearRequestedAccount() - }, [clearRequestedAccount, onDismiss]) + // Clear landing context when user dismisses the modal + setActiveLanding(undefined) + }, [clearRequestedAccount, onDismiss, setActiveLanding]) return ( void}) { {screenState === ScreenState.S_StarterPack ? ( + ) : screenState === ScreenState.S_GroupChatJoinRequest ? ( + ) : screenState === ScreenState.S_LoginOrCreateAccount ? ( { @@ -122,17 +138,12 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { {screenState === ScreenState.S_Login ? ( { - setScreenState(ScreenState.S_LoginOrCreateAccount) - clearRequestedAccount() + setScreenState(initialScreenState) }} /> ) : undefined} {screenState === ScreenState.S_CreateAccount ? ( - - setScreenState(ScreenState.S_LoginOrCreateAccount) - } - /> + setScreenState(initialScreenState)} /> ) : undefined} diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index a6ef1a344c..bac3e74d31 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -151,8 +151,8 @@ function ComposerReplyToImages({ )) || (images.length === 2 && ( @@ -160,14 +160,14 @@ function ComposerReplyToImages({ )) || @@ -176,21 +176,21 @@ function ComposerReplyToImages({ @@ -201,28 +201,28 @@ function ComposerReplyToImages({ diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index cc7b983c31..5cc87c63f5 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -11,6 +11,7 @@ import {atoms as a, useTheme} from '#/alf' import {Loader} from '#/components/Loader' import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed' import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed' +import {JoinRequestEmbed} from '#/components/Post/Embed/JoinRequestEmbed' import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed' import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' @@ -98,7 +99,8 @@ export const ExternalEmbedLink = ({ uri, description: data.view?.external?.description || data.description, - thumb: data.view?.external?.thumb || data.thumb?.source.path, + // prefer opengraph data to atproto record-derived image + thumb: data.thumb?.source.path || data.view?.external?.thumb, }} /> ) @@ -114,6 +116,8 @@ export const ExternalEmbedLink = ({ hideAlt /> ) + } else if (data.type === 'chat-invite') { + return } else if (data.kind === 'feed') { return ( void} & ViewStyleProp) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() return (