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
)
}
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}0>, <1>{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}0>?"
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}0>?"
@@ -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}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}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, {