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 2748f783ba..fe9d5b1c5b 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -47,7 +47,7 @@ jobs: run: pnpm install --frozen-lockfile - name: 🔨 Setup Expo CLI - uses: expo/expo-github-action@main + uses: expo/expo-github-action@v9 with: eas-version: '19.0.5' packager: 'pnpm --allow-build=dtrace-provider' @@ -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 fd09b64577..d2cc7a7abb 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -47,7 +47,7 @@ jobs: run: pnpm install --frozen-lockfile - name: 🔨 Setup Expo CLI - uses: expo/expo-github-action@main + uses: expo/expo-github-action@v9 with: eas-version: '19.0.5' packager: 'pnpm --allow-build=dtrace-provider' @@ -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 f4be27b0de..983494dd18 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -87,7 +87,7 @@ jobs: run: pnpm typecheck - name: 🔨 Setup EAS - uses: expo/expo-github-action@main + uses: expo/expo-github-action@v9 if: ${{ !steps.fingerprint.outputs.includes-changes }} with: eas-version: '19.0.5' @@ -180,7 +180,7 @@ jobs: cache: pnpm - name: 🔨 Setup EAS - uses: expo/expo-github-action@main + uses: expo/expo-github-action@v9 with: eas-version: '19.0.5' packager: 'pnpm --allow-build=dtrace-provider' @@ -327,7 +327,7 @@ jobs: cache: pnpm - name: 🔨 Setup EAS - uses: expo/expo-github-action@main + uses: expo/expo-github-action@v9 with: eas-version: '19.0.5' packager: 'pnpm --allow-build=dtrace-provider' @@ -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 d7715e8ea2..aa6c92765d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,33 +6,42 @@ on: branches: - main concurrency: - group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}" + group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' cancel-in-progress: true +# These jobs only check out the repo and run checks, so read access to the +# repo contents is all the GITHUB_TOKEN needs. +permissions: + contents: read + jobs: linting: name: Run linters runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + job: [lint, prettier, typecheck] steps: - name: Check out Git repository uses: actions/checkout@v5 - - name: Verify Node version pins match .nvmrc + - name: Verify Node version pins match package.json run: | set -euo pipefail - expected=$(tr -d '[:space:]' < .nvmrc) + expected=$(node -p "require('./package.json').engines.node.replace(/[^0-9.]/g, '')") rc=0 check() { if [ "$2" != "$expected" ]; then - echo "::error file=$1::Node version mismatch: $1 pins '$2' but .nvmrc is '$expected'" + echo "::error file=$1::Node version mismatch: $1 pins '$2' but package.json is '$expected'" rc=1 fi } - # FROM node:X.Y.Z — service runtime images + # FROM node:X.Y.Z - service runtime images for f in Dockerfile.bskylink Dockerfile.bskyogcard; do v=$(grep -oE 'FROM node:[0-9]+\.[0-9]+\.[0-9]+' "$f" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u) check "$f" "$v" done - # ENV NODE_VERSION=X.Y.Z — Go images that nvm-install Node for the JS build stage + # ENV NODE_VERSION=X.Y.Z - Go images that nvm-install Node for the JS build stage for f in Dockerfile.embedr; do v=$(grep -oE 'NODE_VERSION=[0-9]+\.[0-9]+\.[0-9]+' "$f" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u) check "$f" "$v" @@ -53,17 +62,33 @@ jobs: command: pnpm install --frozen-lockfile attempt_limit: 3 attempt_delay: 2000 - - name: Lint check - run: pnpm lint - - name: Prettier check - run: pnpm prettier --check . - name: Check & compile i18n run: pnpm intl:build - - name: Type check - run: pnpm typecheck + - name: Lint checks + run: pnpm ${{ matrix.job }} + # Aggregates the matrix results into a single stable check name so branch + # protection can require "Run linters" regardless of how many matrix jobs run. + # The result is asserted in `run` (not `if`) so a malformed expression can + # never silently skip the check and report a false pass. + linting-summary: + name: Run linters + if: always() + needs: [linting] + runs-on: ubuntu-latest + steps: + - name: Require linting to have succeeded + env: + RESULT: ${{ needs.linting.result }} + run: | + echo "linting result: $RESULT" + test "$RESULT" = "success" testing: name: Run tests runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] steps: - name: Check out Git repository uses: actions/checkout@v5 @@ -83,4 +108,20 @@ jobs: run: pnpm intl:build - name: Run tests run: | - NODE_ENV=test pnpm test --forceExit + NODE_ENV=test pnpm test --forceExit --shard=${{ matrix.shard }}/${{ strategy.job-total }} + # Aggregates the sharded test results into a single stable check name so branch + # protection can require "Run tests" regardless of how many shards run. + # The result is asserted in `run` (not `if`) so a malformed expression can + # never silently skip the check and report a false pass. + testing-summary: + name: Run tests + if: always() + needs: [testing] + runs-on: ubuntu-latest + steps: + - name: Require testing to have succeeded + env: + RESULT: ${{ needs.testing.result }} + run: | + echo "testing result: $RESULT" + test "$RESULT" = "success" diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml index 3b70b754ca..a497a5625d 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 }} @@ -141,7 +141,7 @@ jobs: run: pnpm typecheck - name: 🔨 Setup EAS - uses: expo/expo-github-action@main + uses: expo/expo-github-action@v9 with: eas-version: '19.0.5' packager: 'pnpm --allow-build=dtrace-provider' @@ -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/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..f2e677dc79 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,22 @@ "count": 3 } }, + "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/api/index.ts": { "@typescript-eslint/no-explicit-any": { "count": 5 + }, + "@typescript-eslint/no-unsafe-member-access": { + "count": 3 } }, "src/lib/async/retry.ts": { @@ -232,9 +834,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 +855,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 +936,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 +970,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 +1011,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 +1051,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 +1350,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 +1515,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 +1633,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 +1850,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 +2242,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 +2292,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 +2343,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 +2380,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 +2403,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 e76f590e99..b8bd26e241 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "make-deploy-bundle": "bash scripts/bundleUpdate.sh", "generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 pnpm build-web", "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 pnpm build-web", - "icons:optimize": "svgo -f ./assets/icons" + "icons:optimize": "svgo -f ./assets/icons", + "prettier": "prettier --check ." }, "dependencies": { "@atproto/api": "0.20.8", @@ -101,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/analytics/features/types.ts b/src/analytics/features/types.ts index 39df48e532..c8a8cd72d9 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -9,8 +9,6 @@ export enum Features { ImportContactsOnboardingDisable = 'import_contacts:onboarding:disable', ImportContactsSettingsDisable = 'import_contacts:settings:disable', LiveNowBetaDisable = 'live_now_beta:disable', - ImageUploadsHighResolution = 'image_uploads:high_resolution', - ImageUploadsBlobSize2mbEnabled = 'image_uploads:blob_size_2mb:enabled', GroupChatsEnable = 'group_chats:enable', GroupChatsHasBeenReleased = 'group_chats:has_been_released', DmsNewMessageComposerEnable = 'dms:new_message_composer:enable', 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/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/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..9aad6da864 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') { @@ -156,6 +167,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/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 f67d4ec254..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,21 +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.isWithinQuote} - /> + + { + singleContainerRef.current = ref + }} + onDimsChange={dims => { + singleDimsRef.current = dims + }} + onPress={(containerRef, dims) => + onPress(0, [containerRef], [dims]) + } + onPressIn={() => onPressIn(0)} + hideBadge={rest.isWithinQuote} + /> + ) } diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 916a7ec2d9..5fdf18e2a1 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -224,6 +224,7 @@ export function Basic({ cancelButtonCta, confirmButtonCta, onConfirm, + onClose, confirmButtonColor, showCancel = true, }: React.PropsWithChildren<{ @@ -240,11 +241,12 @@ export function Basic({ * should NOT close the dialog as a side effect of this method. */ onConfirm: (e: GestureResponderEvent) => 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/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..79b41311cb 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,10 +133,12 @@ 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" diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 6363758a2a..fe2b63e606 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,6 +36,7 @@ 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' @@ -271,6 +272,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} /> ) }} @@ -385,6 +386,7 @@ function GalleryImage({ onThumbDims, onPress, onPressIn, + onPreviewPress, }: { contentHeight: number image: AppBskyEmbedImages.ViewImage @@ -398,6 +400,7 @@ function GalleryImage({ onThumbDims: (index: number, dims: Dimensions) => void onPress?: () => void onPressIn?: () => void + onPreviewPress?: () => void }) { const t = useTheme() const {t: l} = useLingui() @@ -423,124 +426,131 @@ 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, + }) + }} + /> - + {isCropped && ( + + + + )} + {hasAlt && ( + + + ALT + + + )} + + ) : null} + + - + /> + + ) } diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index b809a783fa..0c349b6612 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,63 @@ 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" + /> + + + {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, + }) + + 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/lib/api/index.ts b/src/lib/api/index.ts index 1edc8c8016..d9a026ca83 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -51,16 +51,10 @@ interface PostOpts { langs?: string[] } -type FeatureFlags = { - highResolutionImages?: boolean - increasedBlobSizeLimit?: boolean -} - export async function post( agent: BskyAgent, queryClient: QueryClient, opts: PostOpts, - featureFlags?: FeatureFlags, ) { const thread = opts.thread opts.onStateChange?.(t`Processing...`) @@ -97,7 +91,6 @@ export async function post( queryClient, draft, opts.onStateChange, - featureFlags, ) let labels: $Typed | undefined if (draft.labels.length) { @@ -257,7 +250,6 @@ async function resolveEmbed( queryClient: QueryClient, draft: PostDraft, onStateChange: ((state: string) => void) | undefined, - featureFlags?: FeatureFlags, ): Promise< | $Typed | $Typed @@ -268,13 +260,7 @@ async function resolveEmbed( > { if (draft.embed.quote) { const [resolvedMedia, resolvedQuote] = await Promise.all([ - resolveMedia( - agent, - queryClient, - draft.embed, - onStateChange, - featureFlags, - ), + resolveMedia(agent, queryClient, draft.embed, onStateChange), resolveRecord(agent, queryClient, draft.embed.quote.uri), ]) if (resolvedMedia) { @@ -297,7 +283,6 @@ async function resolveEmbed( queryClient, draft.embed, onStateChange, - featureFlags, ) if (resolvedMedia) { return resolvedMedia @@ -323,7 +308,6 @@ async function resolveMedia( queryClient: QueryClient, embedDraft: EmbedDraft, onStateChange: ((state: string) => void) | undefined, - featureFlags?: FeatureFlags, ): Promise< | $Typed | $Typed @@ -339,10 +323,7 @@ async function resolveMedia( const images: AppBskyEmbedImages.Image[] = await Promise.all( imagesDraft.map(async (image, i) => { logger.debug(`Compressing image #${i}`) - const {path, width, height, mime} = await compressImage(image, { - highResolution: featureFlags?.highResolutionImages, - increasedBlobSizeLimit: featureFlags?.increasedBlobSizeLimit, - }) + const {path, width, height, mime} = await compressImage(image) logger.debug(`Uploading image #${i}`) const res = await uploadBlob(agent, path, mime) return { diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index d0600b2eb8..f75cd74db2 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -246,6 +246,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 95eb57fd54..a22700f880 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..b5c278dacf 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -178,6 +178,28 @@ export function isBskyStarterPackUrl(url: string): boolean { return false } +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 diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 4de882a998..b1fd0337d7 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -86,6 +86,11 @@ 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 @@ -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 "" @@ -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,10 +882,20 @@ 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 msgid "Accept Request" @@ -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 @@ -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 "" @@ -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 "" @@ -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" @@ -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:203 +#: src/components/images/Gallery/index.tsx:531 +#: src/components/images/ImageLayoutGridItem.tsx:138 #: 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" @@ -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 @@ -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 @@ -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 "" @@ -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/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:256 msgid "carousel" msgstr "carousel" @@ -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" @@ -2621,7 +2678,9 @@ msgstr "" #: src/components/dms/AfterReportDialog.tsx:207 #: src/components/dms/AfterReportDialog.tsx:212 #: 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 @@ -2635,8 +2694,8 @@ msgstr "" #: 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 @@ -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" @@ -2948,6 +3011,11 @@ 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 "" @@ -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 "" @@ -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,12 +3304,13 @@ 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" @@ -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 "" @@ -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,14 +3650,14 @@ 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 "" @@ -3624,7 +3694,7 @@ msgstr "" msgid "Dismiss banner" msgstr "" -#: src/view/com/composer/Composer.tsx:2385 +#: src/view/com/composer/Composer.tsx:2367 msgid "Dismiss error" 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" @@ -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 @@ -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:65 +#: 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,6 +4415,10 @@ 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 msgid "Failed to add emoji reaction" @@ -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" @@ -4403,7 +4486,7 @@ msgstr "Failed to disable invite link" 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" @@ -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 @@ -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 "" @@ -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,17 +5176,17 @@ 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" @@ -5175,17 +5274,17 @@ 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:147 #: 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 @@ -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" @@ -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:436 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:421 msgid "Image {0} of {imageCount}" msgstr "Image {0} of {imageCount}" @@ -5810,7 +5929,7 @@ msgid "Image cache cleared, freed {0}" msgstr "" #. placeholder {0}: images.length -#: src/components/images/Gallery/index.tsx:256 +#: src/components/images/Gallery/index.tsx:257 msgid "Image gallery, {0} images" msgstr "Image gallery, {0} images" @@ -5825,12 +5944,12 @@ 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" @@ -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 "" @@ -5974,6 +6097,10 @@ msgstr "" 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 "" @@ -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 @@ -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" @@ -6798,7 +6942,7 @@ msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" msgid "Message options" msgstr "" -#: src/Navigation.tsx:748 +#: src/Navigation.tsx:755 msgid "Messages" msgstr "" @@ -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 "" @@ -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 @@ -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,7 +7635,7 @@ 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 "" @@ -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,7 +7723,7 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 -#: src/components/dms/InitiateChatFlow.tsx:676 +#: src/components/dms/InitiateChatFlow.tsx:677 #: src/components/dms/MessageItem.tsx:659 #: src/screens/Login/PasswordUpdatedForm.tsx:37 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:661 @@ -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" @@ -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:437 msgid "Opens full image" msgstr "Opens full image" @@ -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 "" @@ -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" @@ -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 "" @@ -9087,7 +9263,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:193 #: src/features/liveNow/components/LiveStatusDialog.tsx:266 -#: src/screens/Messages/ConversationSettings/index.tsx:518 +#: src/screens/Messages/ConversationSettings/index.tsx:517 msgid "Report" 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" @@ -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 "" @@ -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 @@ -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:196 +#: src/components/Post/Embed/ImageContextMenu.tsx:70 #: src/components/StarterPack/ShareDialog.tsx:144 #: src/components/StarterPack/ShareDialog.tsx:150 msgid "Save image" @@ -9466,7 +9675,7 @@ msgid "Saved Feeds" msgstr "" #: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:144 -#: src/Navigation.tsx:548 +#: 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 "" @@ -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" @@ -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 "" @@ -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:202 +#: 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 "" @@ -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:420 msgid "slide" msgstr "slide" @@ -10424,6 +10645,7 @@ 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 "" @@ -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,7 +10807,7 @@ 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 "" @@ -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" @@ -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 "" @@ -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 "" @@ -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,7 +11831,7 @@ msgstr "" msgid "Top replies first" msgstr "" -#: src/Navigation.tsx:477 +#: src/Navigation.tsx:479 msgid "Topic" 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." @@ -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" @@ -12048,20 +12306,20 @@ msgstr "" 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 "" @@ -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,11 +12621,11 @@ 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 "" @@ -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 "" @@ -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:220 +#: src/components/images/AutoSizedImage.tsx:252 msgid "Views full image" 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 "" @@ -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,7 +13058,7 @@ msgid "What do you want to call your starter pack?" msgstr "" #: src/view/com/auth/SplashScreen.web.tsx:98 -#: src/view/com/composer/Composer.tsx:1402 +#: src/view/com/composer/Composer.tsx:1384 #: src/view/com/feeds/ComposerPrompt.tsx:193 msgid "What's up?" 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" @@ -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" @@ -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 "" @@ -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 "" @@ -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 "" @@ -13587,7 +13854,7 @@ msgstr "" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "" -#: src/view/com/composer/Composer.tsx:1045 +#: src/view/com/composer/Composer.tsx:1027 msgid "Your reply was sent" 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..80d31299dc --- /dev/null +++ b/src/screens/Messages/JoinRequests.tsx @@ -0,0 +1,491 @@ +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 +}) { + const {t: l} = useLingui() + return ( + + + + + {count === undefined ? ( + Requests to join + ) : hasMoreRequests ? ( + l({ + message: `${count}+ requests to join`, + comment: + 'Displayed when there are more requests to join a group chat than have been loaded', + }) + ) : ( + + )} + + + + + ) +} + +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..ad6d4a08e5 100644 --- a/src/screens/Messages/components/InviteLinkDialog.tsx +++ b/src/screens/Messages/components/InviteLinkDialog.tsx @@ -297,8 +297,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} { - if (vals[0]) onChange(vals[0] as DynamicAppIcon.IconName) + if (vals[0]) onChange(vals[0]) }}> {children} 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/gallery.ts b/src/state/gallery.ts index c9c7f37750..e1c1541bc3 100644 --- a/src/state/gallery.ts +++ b/src/state/gallery.ts @@ -13,7 +13,6 @@ import { } from 'expo-image-manipulator' import {nanoid} from 'nanoid/non-secure' -import {POST_IMG_MAX} from '#/lib/constants' import {getImageDim} from '#/lib/media/manip' import {openCropper} from '#/lib/media/picker' import {type PickerImage} from '#/lib/media/picker.shared' @@ -202,19 +201,12 @@ export function resetImageManipulation( return img } -export async function compressImage( - img: ComposerImage, - options?: { - highResolution?: boolean - increasedBlobSizeLimit?: boolean - }, -): Promise { +export async function compressImage(img: ComposerImage): Promise { const source = img.transformed || img.source - const highResolution = options?.highResolution ?? false let attempts = 0 - let maxDimension = highResolution ? 4000 : POST_IMG_MAX.width - let maxBytes = options?.increasedBlobSizeLimit ? 2000000 : POST_IMG_MAX.size + let maxDimension = 4000 + let maxBytes = 2000000 let minQualityPercentage = 0 let maxQualityPercentage = 101 // exclusive 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..6aa10331ef --- /dev/null +++ b/src/state/queries/join-links.ts @@ -0,0 +1,72 @@ +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, +}: { + codes?: string[] + hasSession: boolean +}) { + 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: STALE.SECONDS.FIFTEEN, + }) +} + +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.SECONDS.FIFTEEN, + }) + } +} 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/Composer.tsx b/src/view/com/composer/Composer.tsx index 7299abcb1b..5c9f1a05fe 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -877,24 +877,12 @@ export const ComposePost = ({ try { logger.info(`composer: posting...`) postUri = ( - await apilib.post( - agent, - queryClient, - { - thread: filteredThread, - replyTo: replyTo?.uri, - onStateChange: setPublishingStage, - langs: currentLanguages, - }, - { - highResolutionImages: ax.features.enabled( - ax.features.ImageUploadsHighResolution, - ), - increasedBlobSizeLimit: ax.features.enabled( - ax.features.ImageUploadsBlobSize2mbEnabled, - ), - }, - ) + await apilib.post(agent, queryClient, { + thread: filteredThread, + replyTo: replyTo?.uri, + onStateChange: setPublishingStage, + langs: currentLanguages, + }) ).uris[0] /* diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index cc7b983c31..ecd22d7c39 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -98,7 +98,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, }} /> ) diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index 1099c1a0fd..8f178d5282 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -470,11 +470,11 @@ export async function draftToComposerPosts( height, mime: 'image/jpeg', }, - } as ComposerImage + } satisfies ComposerImage }) const images = (await Promise.all(imagePromises)).filter( - (img): img is ComposerImage => img !== null, + (img): img is NonNullable => img !== null, ) if (images.length > 0) { embed.media = {type: 'images', images} @@ -511,7 +511,7 @@ export async function draftToComposerPosts( tinygif: mediaObject, preview: mediaObject, }, - } as Gif, + }, alt: gifData.alt, } break diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 5f337044e5..ac1488e4b3 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -1093,6 +1093,7 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { ) diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index b01651d7b1..2110c9540e 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -55,7 +55,7 @@ export function TestCtrls() { accessibilityLabel="Text input field" accessibilityHint="Enter proxy header" testID="e2eProxyHeaderInput" - onChangeText={val => setProxyHeader(val as any)} + onChangeText={val => setProxyHeader(val)} autoComplete="off" autoCorrect={false} autoCapitalize="none" diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 7e58218852..658a3ca3b3 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -13,6 +13,7 @@ import { import {sanitizeUrl} from '@braintree/sanitize-url' import {StackActions} from '@react-navigation/native' +import {useGroupChatJoinIntent} from '#/lib/hooks/useIntentHandler' import { type DebouncedNavigationProp, useNavigationDeduped, @@ -21,6 +22,7 @@ import {useOpenLink} from '#/lib/hooks/useOpenLink' import {getTabState, TabState} from '#/lib/routes/helpers' import { convertBskyAppUrlIfNeeded, + getChatInviteCodeFromUrl, isExternalUrl, linkRequiresWarning, } from '#/lib/strings/url-helpers' @@ -81,6 +83,7 @@ export const Link = memo(function Link({ const navigation = useNavigationDeduped() const anchorHref = asAnchor ? sanitizeUrl(href) : undefined const openLink = useOpenLink() + const groupChatJoinIntent = useGroupChatJoinIntent() const onPress = useCallback( (e?: Event) => { @@ -92,11 +95,20 @@ export const Link = memo(function Link({ sanitizeUrl(href), navigationAction, openLink, + groupChatJoinIntent, e, ) } }, - [closeModal, navigation, navigationAction, href, openLink, onBeforePress], + [ + closeModal, + navigation, + navigationAction, + href, + openLink, + onBeforePress, + groupChatJoinIntent, + ], ) const accessibilityActionsWithActivate = [ @@ -196,6 +208,7 @@ export const TextLink = memo(function TextLink({ const {closeModal} = useModalControls() const {linkWarningDialogControl} = useGlobalDialogsControlContext() const openLink = useOpenLink() + const groupChatJoinIntent = useGroupChatJoinIntent() if (!disableMismatchWarning && typeof text !== 'string') { console.error('Unable to detect mismatching label') @@ -238,6 +251,7 @@ export const TextLink = memo(function TextLink({ sanitizeUrl(href), navigationAction, openLink, + groupChatJoinIntent, e, ) }, @@ -252,6 +266,7 @@ export const TextLink = memo(function TextLink({ navigationAction, openLink, linkWarningDialogControl, + groupChatJoinIntent, ], ) const hrefAttrs = useMemo(() => { @@ -373,6 +388,7 @@ function onPressInner( href: string, navigationAction: 'push' | 'replace' | 'navigate' = 'push', openLink: (href: string) => void, + groupChatJoinIntent: (code: string, uri?: string) => void, e?: Event, ) { let shouldHandle = false @@ -400,6 +416,11 @@ function onPressInner( if (shouldHandle) { href = convertBskyAppUrlIfNeeded(href) + const chatInviteCode = getChatInviteCodeFromUrl(href) + if (chatInviteCode) { + groupChatJoinIntent(chatInviteCode, href) + return + } if ( newTab || href.startsWith('http') || diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index eaea3c47a9..787ca9e278 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -345,7 +345,7 @@ function ListImpl( style, disableFullWindowScroll && { flex: 1, - overflowY: 'scroll', + overflowY: isWithinSplitView ? 'auto' : 'scroll', }, ]} ref={nativeRef as unknown as React.RefObject}> diff --git a/src/view/com/util/PressableWithHover.tsx b/src/view/com/util/PressableWithHover.tsx index f4006a5268..a4a2d194fd 100644 --- a/src/view/com/util/PressableWithHover.tsx +++ b/src/view/com/util/PressableWithHover.tsx @@ -3,9 +3,9 @@ import { Pressable, type PressableProps, type StyleProp, + type View, type ViewStyle, } from 'react-native' -import {type View} from 'react-native' import {addStyle} from '#/lib/styles' import {useInteractionState} from '#/components/hooks/useInteractionState' diff --git a/src/view/icons/LogomarkWithType.tsx b/src/view/icons/LogomarkWithType.tsx new file mode 100644 index 0000000000..7437a5db14 --- /dev/null +++ b/src/view/icons/LogomarkWithType.tsx @@ -0,0 +1,55 @@ +import Svg, {Path, type PathProps, type SvgProps} from 'react-native-svg' + +import {useTheme} from '#/alf' + +const ratio = 17 / 64 + +export function LogomarkWithType({ + fill, + ...rest +}: {fill?: PathProps['fill']} & SvgProps) { + const t = useTheme() + const size = parseInt(`${rest.width || 32}`) + + return ( + + + + + + + + + + + ) +} diff --git a/src/view/shell/BlockDrawerGesture.tsx b/src/view/shell/BlockDrawerGesture.tsx index bae9a8db82..93ef18cfae 100644 --- a/src/view/shell/BlockDrawerGesture.tsx +++ b/src/view/shell/BlockDrawerGesture.tsx @@ -4,6 +4,8 @@ import {Gesture, GestureDetector} from 'react-native-gesture-handler' export function BlockDrawerGesture({children}: {children: React.ReactNode}) { const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web - const scrollGesture = Gesture.Native().blocksExternalGesture(drawerGesture) + let scrollGesture = Gesture.Native() + .shouldCancelWhenOutside(false) // for some reason defaults to true on Android + .blocksExternalGesture(drawerGesture) return {children} } diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 4191f94176..a23ed693ec 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -171,7 +171,7 @@ function NativeStackNavigator({ } // Evicted screens get a lightweight placeholder instead of their full tree - finalDescriptors = {} as typeof descriptors + finalDescriptors = {} for (const key in descriptors) { if (mountSet.has(key)) { finalDescriptors[key] = descriptors[key] diff --git a/webpack.config.js b/webpack.config.js index 101a762ffb..bccf95adef 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -43,7 +43,7 @@ function patchSourceMapFilter(rules, pathPattern) { module.exports = async function (env, argv) { env.babel = { - dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo'], + dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo', '@atproto/api'], } let config = await createExpoWebpackConfigAsync(env, argv) config = withAlias(config, {