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