Merge branch 'app-2273' of github.com:bluesky-social/social-app into app-2273
This commit is contained in:
@@ -0,0 +1 @@
|
||||
alt-blur@3076a2b5a593b0d7b37198b5aebfe3a222afd52d
|
||||
@@ -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]
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 🔨 Setup Expo CLI
|
||||
uses: expo/expo-github-action@main
|
||||
uses: expo/expo-github-action@v9
|
||||
with:
|
||||
eas-version: '19.0.5'
|
||||
packager: 'pnpm --allow-build=dtrace-provider'
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v2.1.1
|
||||
uses: slackapi/slack-github-action@v3.0.3
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
webhook-type: incoming-webhook
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
|
||||
- name: 🔔 Notify Slack of Testflight Build
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
uses: slackapi/slack-github-action@v2.1.1
|
||||
uses: slackapi/slack-github-action@v3.0.3
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
webhook-type: incoming-webhook
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
|
||||
- name: 🔔 Notify Slack of Production APK Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v2.1.1
|
||||
uses: slackapi/slack-github-action@v3.0.3
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
webhook-type: incoming-webhook
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 🔨 Setup Expo CLI
|
||||
uses: expo/expo-github-action@main
|
||||
uses: expo/expo-github-action@v9
|
||||
with:
|
||||
eas-version: '19.0.5'
|
||||
packager: 'pnpm --allow-build=dtrace-provider'
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v2.1.1
|
||||
uses: slackapi/slack-github-action@v3.0.3
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
webhook-type: incoming-webhook
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@main
|
||||
uses: expo/expo-github-action@v9
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
eas-version: '19.0.5'
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
cache: pnpm
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@main
|
||||
uses: expo/expo-github-action@v9
|
||||
with:
|
||||
eas-version: '19.0.5'
|
||||
packager: 'pnpm --allow-build=dtrace-provider'
|
||||
@@ -327,7 +327,7 @@ jobs:
|
||||
cache: pnpm
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@main
|
||||
uses: expo/expo-github-action@v9
|
||||
with:
|
||||
eas-version: '19.0.5'
|
||||
packager: 'pnpm --allow-build=dtrace-provider'
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
path: build.apk
|
||||
|
||||
- name: 🔔 Notify Slack
|
||||
uses: slackapi/slack-github-action@v2.1.1
|
||||
uses: slackapi/slack-github-action@v3.0.3
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
webhook-type: incoming-webhook
|
||||
|
||||
+72
-11
@@ -6,33 +6,42 @@ on:
|
||||
branches:
|
||||
- main
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
# These jobs only check out the repo and run checks, so read access to the
|
||||
# repo contents is all the GITHUB_TOKEN needs.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
linting:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job: [lint, prettier]
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Verify Node version pins match .nvmrc
|
||||
- name: Verify Node version pins match package.json
|
||||
run: |
|
||||
set -euo pipefail
|
||||
expected=$(tr -d '[:space:]' < .nvmrc)
|
||||
expected=$(node -p "require('./package.json').engines.node.replace(/[^0-9.]/g, '')")
|
||||
rc=0
|
||||
check() {
|
||||
if [ "$2" != "$expected" ]; then
|
||||
echo "::error file=$1::Node version mismatch: $1 pins '$2' but .nvmrc is '$expected'"
|
||||
echo "::error file=$1::Node version mismatch: $1 pins '$2' but package.json is '$expected'"
|
||||
rc=1
|
||||
fi
|
||||
}
|
||||
# FROM node:X.Y.Z — service runtime images
|
||||
# FROM node:X.Y.Z - service runtime images
|
||||
for f in Dockerfile.bskylink Dockerfile.bskyogcard; do
|
||||
v=$(grep -oE 'FROM node:[0-9]+\.[0-9]+\.[0-9]+' "$f" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
|
||||
check "$f" "$v"
|
||||
done
|
||||
# ENV NODE_VERSION=X.Y.Z — Go images that nvm-install Node for the JS build stage
|
||||
# ENV NODE_VERSION=X.Y.Z - Go images that nvm-install Node for the JS build stage
|
||||
for f in Dockerfile.embedr; do
|
||||
v=$(grep -oE 'NODE_VERSION=[0-9]+\.[0-9]+\.[0-9]+' "$f" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
|
||||
check "$f" "$v"
|
||||
@@ -53,10 +62,42 @@ jobs:
|
||||
command: pnpm install --frozen-lockfile
|
||||
attempt_limit: 3
|
||||
attempt_delay: 2000
|
||||
- name: Lint check
|
||||
run: pnpm lint
|
||||
- name: Prettier check
|
||||
run: pnpm prettier --check .
|
||||
- name: Lint checks
|
||||
run: pnpm ${{ matrix.job }}
|
||||
# Aggregates the matrix results into a single stable check name so branch
|
||||
# protection can require "Run linters" regardless of how many matrix jobs run.
|
||||
# The result is asserted in `run` (not `if`) so a malformed expression can
|
||||
# never silently skip the check and report a false pass.
|
||||
linting-summary:
|
||||
name: Run linters
|
||||
if: always()
|
||||
needs: [linting]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require linting to have succeeded
|
||||
env:
|
||||
RESULT: ${{ needs.linting.result }}
|
||||
run: |
|
||||
echo "linting result: $RESULT"
|
||||
test "$RESULT" = "success"
|
||||
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
|
||||
@@ -64,6 +105,10 @@ jobs:
|
||||
testing:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -83,4 +128,20 @@ jobs:
|
||||
run: pnpm intl:build
|
||||
- name: Run tests
|
||||
run: |
|
||||
NODE_ENV=test pnpm test --forceExit
|
||||
NODE_ENV=test pnpm test --forceExit --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||
# Aggregates the sharded test results into a single stable check name so branch
|
||||
# protection can require "Run tests" regardless of how many shards run.
|
||||
# The result is asserted in `run` (not `if`) so a malformed expression can
|
||||
# never silently skip the check and report a false pass.
|
||||
testing-summary:
|
||||
name: Run tests
|
||||
if: always()
|
||||
needs: [testing]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require testing to have succeeded
|
||||
env:
|
||||
RESULT: ${{ needs.testing.result }}
|
||||
run: |
|
||||
echo "testing result: $RESULT"
|
||||
test "$RESULT" = "success"
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
core.setOutput('head-ref', pr.data.head.ref);
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
uses: marocchino/sticky-pull-request-comment@v3
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@main
|
||||
uses: expo/expo-github-action@v9
|
||||
with:
|
||||
eas-version: '19.0.5'
|
||||
packager: 'pnpm --allow-build=dtrace-provider'
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
RUNTIME_VERSION:
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
uses: marocchino/sticky-pull-request-comment@v3
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
with:
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
uses: marocchino/sticky-pull-request-comment@v3
|
||||
if: failure()
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -206,7 +206,7 @@ function MyComponent() {
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]}>
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>Hello</Text>
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_high]}>Hello</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -414,12 +414,16 @@ import {Text, H1, H2, P} from '#/components/Typography'
|
||||
|
||||
<H1 style={[a.text_xl, a.font_bold]}>Heading</H1>
|
||||
<P>Paragraph text with default styling.</P>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>Custom text</Text>
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>Custom text</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
|
||||
<Text emoji>Hello! 👋</Text>
|
||||
```
|
||||
|
||||
The `Text` component's default style is `[a.text_sm, a.leading_snug, t.atoms.text]`.
|
||||
|
||||
### TextField
|
||||
|
||||
```tsx
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ RUN mkdir --parents $NVM_DIR && \
|
||||
RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
nvm install $NODE_VERSION && \
|
||||
nvm use $NODE_VERSION && \
|
||||
npm install --global pnpm@11.1.3 && \
|
||||
npm install --global pnpm@11.5.0 && \
|
||||
pnpm install --frozen-lockfile && \
|
||||
cd bskyembed && pnpm install --frozen-lockfile && cd .. && \
|
||||
pnpm intl:build && \
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 259 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -46,6 +46,12 @@ func run(args []string) {
|
||||
Required: false,
|
||||
EnvVars: []string{"OGCARD_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "chat-host",
|
||||
Usage: "scheme, hostname, and port of chat appview",
|
||||
Required: false,
|
||||
EnvVars: []string{"CHAT_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-address",
|
||||
Usage: "Specify the local IP/port to bind to",
|
||||
|
||||
@@ -17,11 +17,13 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
appbsky "github.com/bluesky-social/indigo/api/bsky"
|
||||
chat "github.com/bluesky-social/indigo/api/chat"
|
||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||
"github.com/bluesky-social/indigo/util/cliutil"
|
||||
"github.com/bluesky-social/indigo/xrpc"
|
||||
@@ -37,10 +39,11 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
xrpcc *xrpc.Client
|
||||
cfg *Config
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
xrpcc *xrpc.Client
|
||||
chatXrpcc *xrpc.Client
|
||||
cfg *Config
|
||||
|
||||
ipccClient http.Client
|
||||
|
||||
@@ -54,6 +57,7 @@ type Config struct {
|
||||
debug bool
|
||||
httpAddress string
|
||||
appviewHost string
|
||||
chatHost string
|
||||
ogcardHost string
|
||||
linkHost string
|
||||
ipccHost string
|
||||
@@ -64,6 +68,7 @@ func serve(cctx *cli.Context) error {
|
||||
debug := cctx.Bool("debug")
|
||||
httpAddress := cctx.String("http-address")
|
||||
appviewHost := cctx.String("appview-host")
|
||||
chatHost := cctx.String("chat-host")
|
||||
ogcardHost := cctx.String("ogcard-host")
|
||||
linkHost := cctx.String("link-host")
|
||||
ipccHost := cctx.String("ipcc-host")
|
||||
@@ -83,6 +88,15 @@ func serve(cctx *cli.Context) error {
|
||||
Host: appviewHost,
|
||||
}
|
||||
|
||||
// optional client for the chat appview, used by /c/<code> for OG previews.
|
||||
var chatXrpcc *xrpc.Client
|
||||
if chatHost != "" {
|
||||
chatXrpcc = &xrpc.Client{
|
||||
Client: cliutil.NewHttpClient(),
|
||||
Host: chatHost,
|
||||
}
|
||||
}
|
||||
|
||||
// httpd
|
||||
var (
|
||||
httpTimeout = 2 * time.Minute
|
||||
@@ -106,12 +120,14 @@ func serve(cctx *cli.Context) error {
|
||||
// server
|
||||
//
|
||||
server := &Server{
|
||||
echo: e,
|
||||
xrpcc: xrpcc,
|
||||
echo: e,
|
||||
xrpcc: xrpcc,
|
||||
chatXrpcc: chatXrpcc,
|
||||
cfg: &Config{
|
||||
debug: debug,
|
||||
httpAddress: httpAddress,
|
||||
appviewHost: appviewHost,
|
||||
chatHost: chatHost,
|
||||
ogcardHost: ogcardHost,
|
||||
linkHost: linkHost,
|
||||
ipccHost: ipccHost,
|
||||
@@ -323,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)
|
||||
@@ -349,6 +366,9 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/starter-pack-short/:code", server.WebGeneric)
|
||||
e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
|
||||
|
||||
// chat invites
|
||||
e.GET("/c/:code", server.WebChatInvite)
|
||||
|
||||
// bookmarks
|
||||
e.GET("/saved", server.WebGeneric)
|
||||
|
||||
@@ -644,6 +664,43 @@ func (srv *Server) WebStarterPack(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "starterpack.html", data)
|
||||
}
|
||||
|
||||
// chatInviteCodeRe is a permissive sanity check on the join code so we don't
|
||||
// proxy obviously-bad input to the chat appview. Codes are opaque per the
|
||||
// lexicon, but in practice URL-safe and short.
|
||||
var chatInviteCodeRe = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
|
||||
|
||||
func (srv *Server) WebChatInvite(c echo.Context) error {
|
||||
req := c.Request()
|
||||
ctx := req.Context()
|
||||
data := srv.NewTemplateContext()
|
||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||
|
||||
code := c.Param("code")
|
||||
if !chatInviteCodeRe.MatchString(code) {
|
||||
return c.Render(http.StatusOK, "chatinvite.html", data)
|
||||
}
|
||||
if srv.chatXrpcc == nil {
|
||||
return c.Render(http.StatusOK, "chatinvite.html", data)
|
||||
}
|
||||
|
||||
out, err := chat.GroupGetJoinLinkPreviews(ctx, srv.chatXrpcc, []string{code})
|
||||
if err != nil {
|
||||
log.Errorf("failed to fetch chat invite preview for code=%s: %v", code, err)
|
||||
return c.Render(http.StatusOK, "chatinvite.html", data)
|
||||
}
|
||||
if len(out.JoinLinkPreviews) == 0 {
|
||||
return c.Render(http.StatusOK, "chatinvite.html", data)
|
||||
}
|
||||
preview := out.JoinLinkPreviews[0]
|
||||
|
||||
data["title"] = preview.Name
|
||||
if srv.cfg.ogcardHost != "" {
|
||||
// bskyogcard registers this route as /chat-invite/:code, not /c/:code.
|
||||
data["imgThumbUrl"] = fmt.Sprintf("%s/chat-invite/%s", srv.cfg.ogcardHost, code)
|
||||
}
|
||||
return c.Render(http.StatusOK, "chatinvite.html", data)
|
||||
}
|
||||
|
||||
func (srv *Server) WebProfile(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
data := srv.NewTemplateContext()
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ module github.com/bluesky-social/social-app/bskyweb
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/bluesky-social/indigo v0.0.0-20260528134852-3e368bd18814
|
||||
github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0
|
||||
github.com/flosch/pongo2/v6 v6.0.0
|
||||
github.com/ipfs/go-log v1.0.5
|
||||
github.com/joho/godotenv v1.5.1
|
||||
|
||||
+2
-4
@@ -2,10 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bluesky-social/indigo v0.0.0-20260527160159-3b7634c713b8 h1:heYuihJZQZMNij7+dKIEfApG10xrGnDV2rtKpvFyYYU=
|
||||
github.com/bluesky-social/indigo v0.0.0-20260527160159-3b7634c713b8/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo=
|
||||
github.com/bluesky-social/indigo v0.0.0-20260528134852-3e368bd18814 h1:UZK0qmB7LAe2IZY6RASlsE0Mm1Rjasddd2T5zWWvT54=
|
||||
github.com/bluesky-social/indigo v0.0.0-20260528134852-3e368bd18814/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo=
|
||||
github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0 h1:eijBaF59A5c+kPqufH7YO1GOqDMkyUhtM9P9aAWtfJY=
|
||||
github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block html_head_extra -%}
|
||||
{%- if requestURI %}
|
||||
<meta property="og:url" content="{{ requestURI }}">
|
||||
{% endif -%}
|
||||
{%- if imgThumbUrl %}
|
||||
<meta property="og:image" content="{{ imgThumbUrl }}">
|
||||
<meta property="twitter:image" content="{{ imgThumbUrl }}">
|
||||
{%- else -%}
|
||||
<meta property="og:image" content="https://bsky.app/static/social-card-default-gradient.png" />
|
||||
<meta property="twitter:image" content="https://bsky.app/static/social-card-default-gradient.png" />
|
||||
{% endif -%}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{%- if title %}
|
||||
<meta property="og:title" content="{{ title }}" />
|
||||
<meta name="twitter:title" content="{{ title }}" />
|
||||
{%- else -%}
|
||||
<meta property="og:title" content="Bluesky" />
|
||||
<meta name="twitter:title" content="Bluesky" />
|
||||
{% endif -%}
|
||||
<meta name="description" content="Join this group chat on Bluesky" />
|
||||
<meta name="og:description" content="Join this group chat on Bluesky" />
|
||||
<meta name="twitter:description" content="Join this group chat on Bluesky" />
|
||||
{%- endblock %}
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"src/components/Post/Embed/ImageEmbed.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx": {
|
||||
@@ -169,7 +169,7 @@
|
||||
},
|
||||
"src/components/images/AutoSizedImage.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/images/Gallery/index.tsx": {
|
||||
@@ -317,16 +317,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/locale/i18n.web.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/platform/polyfills.web.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/screens/Bookmarks/index.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
+19
-14
@@ -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<View>()
|
||||
for (i in 0 until innerViewGroup.childCount) {
|
||||
|
||||
+10
-9
@@ -8,7 +8,7 @@
|
||||
"devEngines": {
|
||||
"packageManager": {
|
||||
"name": "pnpm",
|
||||
"version": "11.1.3",
|
||||
"version": "11.5.0",
|
||||
"onFail": "warn"
|
||||
},
|
||||
"runtime": {
|
||||
@@ -89,10 +89,11 @@
|
||||
"make-deploy-bundle": "bash scripts/bundleUpdate.sh",
|
||||
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 pnpm build-web",
|
||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 pnpm build-web",
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
"icons:optimize": "svgo -f ./assets/icons",
|
||||
"prettier": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.20.6",
|
||||
"@atproto/api": "0.20.8",
|
||||
"@atproto/syntax": "0.6.1",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
@@ -101,6 +102,7 @@
|
||||
"@bsky.app/expo-image-crop-tool": "^0.5.1",
|
||||
"@bsky.app/expo-scroll-edge-effect": "^0.1.4",
|
||||
"@bsky.app/expo-translate-text": "^0.2.9",
|
||||
"@bsky.app/peek-menu": "^0.2.4",
|
||||
"@bsky.app/react-native-mmkv": "2.12.5",
|
||||
"@bsky.app/sift": "^0.3.8",
|
||||
"@bsky.app/tapper": "^0.5.7",
|
||||
@@ -112,16 +114,15 @@
|
||||
"@expo/webpack-config": "^19.0.1",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
"@formatjs/intl-displaynames": "^6.8.13",
|
||||
"@formatjs/intl-locale": "^4.2.13",
|
||||
"@formatjs/intl-numberformat": "^8.15.6",
|
||||
"@formatjs/intl-pluralrules": "^5.4.6",
|
||||
"@formatjs/intl-displaynames": "^7.3.9",
|
||||
"@formatjs/intl-locale": "^5.3.8",
|
||||
"@formatjs/intl-numberformat": "^9.3.10",
|
||||
"@formatjs/intl-pluralrules": "^6.3.9",
|
||||
"@growthbook/growthbook": "^1.6.5",
|
||||
"@growthbook/growthbook-react": "^1.6.5",
|
||||
"@ipld/dag-cbor": "^9.2.7",
|
||||
"@lingui/core": "^5.9.2",
|
||||
"@lingui/react": "^5.9.2",
|
||||
"@miblanchard/react-native-slider": "^2.6.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.8.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^7.15.5",
|
||||
@@ -150,7 +151,7 @@
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"bcp-47": "^2.1.0",
|
||||
"bcp-47-match": "^2.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns": "^4.4.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"emoji-regex": "^10.4.0",
|
||||
|
||||
@@ -13,3 +13,39 @@ index 47c4d15f6b10bbd77858cfff425cda9a618735b9..afe138d22d566244482498a7be0e14b8
|
||||
// Check for Content-Type
|
||||
val skipContentTypes = listOf(
|
||||
"text/event-stream", // Server Sent Events
|
||||
diff --git a/ios/Core/ExpoBridgeModule.mm b/ios/Core/ExpoBridgeModule.mm
|
||||
index 2ed1c00f47406e109750cc27ace7e0d88e42c00e..99d0d140eddf95a8db7beb57c61dfcb12c1424b4 100644
|
||||
--- a/ios/Core/ExpoBridgeModule.mm
|
||||
+++ b/ios/Core/ExpoBridgeModule.mm
|
||||
@@ -7,6 +7,9 @@
|
||||
// The runtime executor is included as of React Native 0.74 in bridgeless mode.
|
||||
#if __has_include(<ReactCommon/RCTRuntimeExecutor.h>)
|
||||
#import <ReactCommon/RCTRuntimeExecutor.h>
|
||||
+#else // React Native <0.74
|
||||
+// dispatchBlock:queue: is declared in RCTBridge+Private.h, not the public header.
|
||||
+#import <React/RCTBridge+Private.h>
|
||||
#endif // React Native >=0.74
|
||||
|
||||
@implementation ExpoBridgeModule
|
||||
@@ -46,7 +49,20 @@ - (void)setBridge:(RCTBridge *)bridge
|
||||
_appContext.reactBridge = bridge;
|
||||
|
||||
#if !__has_include(<ReactCommon/RCTRuntimeExecutor.h>)
|
||||
- _appContext._runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:bridge];
|
||||
+ // Hop the runtime install (and the prepareRuntime() chain it triggers via
|
||||
+ // _runtime.didSet) onto RCTJSThread. The original line ran synchronously
|
||||
+ // on whatever thread called setBridge: - typically the main thread - and
|
||||
+ // raced JSIExecutor::initializeRuntime() on the JS thread, corrupting
|
||||
+ // Hermes' Hades GC (HadesGC::writeBarrierSlow EXC_BAD_ACCESS).
|
||||
+ __weak EXAppContext *weakAppContext = _appContext;
|
||||
+ __weak RCTBridge *weakBridge = bridge;
|
||||
+ [bridge dispatchBlock:^{
|
||||
+ EXAppContext *strongAppContext = weakAppContext;
|
||||
+ RCTBridge *strongBridge = weakBridge;
|
||||
+ if (strongAppContext != nil && strongBridge != nil && strongAppContext._runtime == nil) {
|
||||
+ strongAppContext._runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:strongBridge];
|
||||
+ }
|
||||
+ } queue:RCTJSThread];
|
||||
#endif // React Native <0.74
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
## expo-modules-core Patch
|
||||
|
||||
This patch fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools
|
||||
This patch contains two unrelated fixes:
|
||||
|
||||
### Android: bitdrift interceptor
|
||||
|
||||
Fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools.
|
||||
|
||||
### iOS: Hermes startup race in `ExpoBridgeModule.setBridge:`
|
||||
|
||||
On the legacy bridge (old architecture, where `RCTRuntimeExecutor.h` is
|
||||
absent), `setBridge:` installed the Expo runtime synchronously on whatever
|
||||
thread called it - typically the main thread, since RN's lazy module-load
|
||||
path ignores `+requiresMainQueueSetup`. The `_runtime.didSet` then ran
|
||||
`prepareRuntime()` (JSI mutations) on the main thread while the JS thread was
|
||||
concurrently inside `JSIExecutor::initializeRuntime()`. Two threads mutating
|
||||
the same Hermes runtime corrupted Hades GC, producing intermittent
|
||||
`EXC_BAD_ACCESS` launch crashes (e.g. `HadesGC::writeBarrierSlow`,
|
||||
`prepareRuntime` / `bindNativePerformanceNow`).
|
||||
|
||||
The fix hops the runtime install onto `RCTJSThread` so all JSI mutation is
|
||||
serialized on the JS thread. This backports the upstream fix discussed in
|
||||
expo/expo#45374; the racy `ExpoBridgeModule` is removed entirely in SDK 55+
|
||||
(expo/expo#44351), so this patch can be dropped on that upgrade.
|
||||
|
||||
Refs: expo/expo#43003, expo/expo#45374, expo/expo#44351
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts
|
||||
index 0f6d7c67a307885310ab184fdf9e7a5c7b296825..1e0bdd5b1d3b1eceb47bfb0050da84061fd7f77c 100644
|
||||
index 0f6d7c67a307885310ab184fdf9e7a5c7b296825..1a01093e7909973cef5268f999158df389e77634 100644
|
||||
--- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts
|
||||
+++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts
|
||||
@@ -1,8 +1,6 @@
|
||||
@@ -19,7 +19,7 @@ index 0f6d7c67a307885310ab184fdf9e7a5c7b296825..1e0bdd5b1d3b1eceb47bfb0050da8406
|
||||
inverted,
|
||||
keyboardLiftBehavior,
|
||||
freeze,
|
||||
@@ -62,20 +59,14 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void {
|
||||
@@ -62,20 +59,23 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void {
|
||||
(target: number) => {
|
||||
"worklet";
|
||||
|
||||
@@ -37,6 +37,15 @@ index 0f6d7c67a307885310ab184fdf9e7a5c7b296825..1e0bdd5b1d3b1eceb47bfb0050da8406
|
||||
+ // otherwise the native ScrollView clamps contentOffset to the old
|
||||
+ // contentInset range (iOS Fabric) or the old contentInsetBottom (Android).
|
||||
+ requestAnimationFrame(() => {
|
||||
+ // The ScrollView can detach between scheduling this frame and now (e.g.
|
||||
+ // navigating away from the screen, or the list re-creating its scroll
|
||||
+ // component). Once detached, scrollViewRef() resolves to null, and on
|
||||
+ // Paper reanimated's scrollTo hands that null to the native
|
||||
+ // _scrollToPaper HostFunction, which throws "Value is null, expected a
|
||||
+ // number". Re-check the ref now that the frame has arrived.
|
||||
+ if (scrollViewRef() == null) {
|
||||
+ return;
|
||||
+ }
|
||||
scrollTo(scrollViewRef, 0, target, false);
|
||||
- }
|
||||
+ });
|
||||
|
||||
Generated
+116
-136
@@ -7,52 +7,52 @@ importers:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
'@pnpm/exe':
|
||||
specifier: 11.1.3
|
||||
version: 11.1.3
|
||||
specifier: 11.5.0
|
||||
version: 11.5.0
|
||||
pnpm:
|
||||
specifier: 11.1.3
|
||||
version: 11.1.3
|
||||
specifier: 11.5.0
|
||||
version: 11.5.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe@11.1.3':
|
||||
resolution: {integrity: sha512-J6bSMpZlHVUKuMKtPT/+lrFPpvBiOIgk6HnNC3vmoM/fsMgFhao6ITFwdsUMzdCl2qHf9cnVYA4ZBdsGmVUnpg==}
|
||||
'@pnpm/exe@11.5.0':
|
||||
resolution: {integrity: sha512-4hzOXq1HHrNPjwI8k1rt7Ot/Yrdx1JX3pn/L/M95ii1gid1Q6ZK6dVg4+gbSgUdPsYmYDZ4/Yfc0A7vd5C0ndg==}
|
||||
hasBin: true
|
||||
|
||||
'@pnpm/linux-arm64@11.1.3':
|
||||
resolution: {integrity: sha512-sz3fc0hSqguk2eGe/InelpBD3LP82MzF+8pLzDpYNGuhasGY+VWkuxEo02HczQYRVXTsbpZVepk+Qs2CONc04Q==}
|
||||
'@pnpm/linux-arm64@11.5.0':
|
||||
resolution: {integrity: sha512-NV9HdzzCB0epuI9LqZZeTaqjH3OweNQSQCS76GzEkFxJHS9e5Gvu7tgex91gxVL7bCZ+R4yr/3d3yexBFtr2ug==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@pnpm/linux-x64@11.1.3':
|
||||
resolution: {integrity: sha512-UadJh5fJZWa47OtdZTWLKWDj4z5WpZFB5pS2wOh/kfKypUmQyjmbOMClP7/yJGS2ZtrSjRgYjIEWAjndkWsMaA==}
|
||||
'@pnpm/linux-x64@11.5.0':
|
||||
resolution: {integrity: sha512-vH83rRx4iPk/bwm9pBVCn+5hXbcQI66I/4zk6Vc09SusJgTqOdbN4U6VhMcGIqSEdr901ksYGCyIbMv7f6Guew==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@pnpm/linuxstatic-arm64@11.1.3':
|
||||
resolution: {integrity: sha512-lWmGr96w+VrIRVsEfTXROB3GeQNxrX2Hy32j3USHr6WlqmpH1i7YjavJGpoXeVZbb77fCFjNFAtsJzGXSgy/Og==}
|
||||
'@pnpm/linuxstatic-arm64@11.5.0':
|
||||
resolution: {integrity: sha512-2nOnMW1rSwGv22q2yZz1HlGT3ly/Ij8wUlX0NB4n+Krx7nETRHA3MgWsbkVejxHknDcTulRVudAghuX9rgrXcw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/linuxstatic-x64@11.1.3':
|
||||
resolution: {integrity: sha512-I74GDBOPbr5TXgob3ct4hv6BQtLGdsjGJrII2qNl/e2xYHXekjIWE4Eh7RGN4C7xu+BZYKtvnOOr149yR7KxUw==}
|
||||
'@pnpm/linuxstatic-x64@11.5.0':
|
||||
resolution: {integrity: sha512-ONOC1Mg0JusHtjzkRlre9di1QO+GAjy4HP7jMjDx21yGhrSheNdUweTXbekMH1EflRd19kTU6d8M3zewJFPtVg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/macos-arm64@11.1.3':
|
||||
resolution: {integrity: sha512-nWn155BVa54iNyg4iolVhjMtqumXHPh8ul5CFjQFJXdwgr9MRUgc5EUBML1+7mS8C/7Wcex5HUgn/w3fliHCsQ==}
|
||||
'@pnpm/macos-arm64@11.5.0':
|
||||
resolution: {integrity: sha512-od0ALdTxs4A7s5vAH5q2l2phzCJb98+PVOW1rq7BGpWGeYxQ+EwvL+vq0KaO6iLsn/eVVoncCkgZ/k6QNYuTgw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/win-arm64@11.1.3':
|
||||
resolution: {integrity: sha512-4u6PQL7/WgwdweC2ZJcdCzhM1koRFG5ofcUSrIVsdyo9ePfGq2D9p1rkZbiLEfIGvl3GKOsfMB5DRTgs8es4AQ==}
|
||||
'@pnpm/win-arm64@11.5.0':
|
||||
resolution: {integrity: sha512-9HqbI80FjVVqFx4+EPxYYNfeP9Sx69W6kYqUDvOJn9G7RJ/2NNNQ898cVHTMpXlW1/PrMEcijmdpa/NjZIrWiQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/win-x64@11.1.3':
|
||||
resolution: {integrity: sha512-JmhH7ljJ3MWjvWFz/YHbu/27ISCNLZsQb1JG+ib3uHlfUwIJuF6BudXYsbS5Uu0o7xUvC9sPPm+Uho4Or6R3vA==}
|
||||
'@pnpm/win-x64@11.5.0':
|
||||
resolution: {integrity: sha512-Q89CQqFGAsWmfvHZs5Kbbar45q3GBYtfAdPUCiVMVNJoLi3dsBS2LCvUq8ak3AufkFDaJBpvhaFcDP2M1NXr3A==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -116,45 +116,45 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pnpm@11.1.3:
|
||||
resolution: {integrity: sha512-yFNX/hfKEt0j3XBxgiZm39fjy3b+IU4zcLXqL7NPKiMRhVCbY+cX880KyzjdP42CvNXoFyQArmeLcOpPvtCJbQ==}
|
||||
pnpm@11.5.0:
|
||||
resolution: {integrity: sha512-2/zE+Bz0hZev1Lw5H/3xLBHxqfuDo5W/prCi2cwv2P/rr9scy9UpYyFT95OQTCYVt/Cf4aNFRz/Rw1hFFyqOsQ==}
|
||||
engines: {node: '>=22.13'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe@11.1.3':
|
||||
'@pnpm/exe@11.5.0':
|
||||
dependencies:
|
||||
'@reflink/reflink': 0.1.19
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
'@pnpm/linux-arm64': 11.1.3
|
||||
'@pnpm/linux-x64': 11.1.3
|
||||
'@pnpm/linuxstatic-arm64': 11.1.3
|
||||
'@pnpm/linuxstatic-x64': 11.1.3
|
||||
'@pnpm/macos-arm64': 11.1.3
|
||||
'@pnpm/win-arm64': 11.1.3
|
||||
'@pnpm/win-x64': 11.1.3
|
||||
'@pnpm/linux-arm64': 11.5.0
|
||||
'@pnpm/linux-x64': 11.5.0
|
||||
'@pnpm/linuxstatic-arm64': 11.5.0
|
||||
'@pnpm/linuxstatic-x64': 11.5.0
|
||||
'@pnpm/macos-arm64': 11.5.0
|
||||
'@pnpm/win-arm64': 11.5.0
|
||||
'@pnpm/win-x64': 11.5.0
|
||||
|
||||
'@pnpm/linux-arm64@11.1.3':
|
||||
'@pnpm/linux-arm64@11.5.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/linux-x64@11.1.3':
|
||||
'@pnpm/linux-x64@11.5.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/linuxstatic-arm64@11.1.3':
|
||||
'@pnpm/linuxstatic-arm64@11.5.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/linuxstatic-x64@11.1.3':
|
||||
'@pnpm/linuxstatic-x64@11.5.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/macos-arm64@11.1.3':
|
||||
'@pnpm/macos-arm64@11.5.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/win-arm64@11.1.3':
|
||||
'@pnpm/win-arm64@11.5.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/win-x64@11.1.3':
|
||||
'@pnpm/win-x64@11.5.0':
|
||||
optional: true
|
||||
|
||||
'@reflink/reflink-darwin-arm64@0.1.19':
|
||||
@@ -194,7 +194,7 @@ snapshots:
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
pnpm@11.1.3: {}
|
||||
pnpm@11.5.0: {}
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
@@ -222,13 +222,13 @@ patchedDependencies:
|
||||
expo-image-picker@17.0.11: 47b28f6ddb8bcaa6483f8714c82daaa0001122f2b0dac6c05d19e98a61a6ceab
|
||||
expo-image@3.0.11: 6d46ffac33426c5285777da5bf5f88c68c8ce27290595dfa65f38c478930c2c4
|
||||
expo-media-library@18.2.1: 2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a
|
||||
expo-modules-core@3.0.30: 86e57bb33bc6c3f7021e948e099c2b5378772147b92a82a16e2e90ddcb857ace
|
||||
expo-modules-core@3.0.30: 952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e
|
||||
expo-notifications@0.32.17: a45a8dcf3d8c4b5df4ee0e62bc79b755fcf9d28ca51a6f685b85830bf4afc2e5
|
||||
expo-updates@29.0.17: 04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487
|
||||
react-native-compressor@1.13.0: 58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925
|
||||
react-native-date-picker@5.0.13: f2a6697da7a7ca79b4f39efda142eee1b82db6de3aa5010ad4bd59df41fe2ce1
|
||||
react-native-drawer-layout@4.2.3: 74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130
|
||||
react-native-keyboard-controller@1.21.8: 642d128c971380a1858f7a938dd715d6eb3bbc260c93188b65580d92e2226afe
|
||||
react-native-keyboard-controller@1.21.8: 2f6791837622fb391c3e41fc044b91ddedbe503da14da86165ee0a73924b05f1
|
||||
react-native-pager-view@6.8.0: 0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108
|
||||
react-native-reanimated@3.19.1: 97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6
|
||||
react-native-svg@15.12.1: 31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9
|
||||
@@ -242,8 +242,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@atproto/api':
|
||||
specifier: 0.20.6
|
||||
version: 0.20.6
|
||||
specifier: 0.20.8
|
||||
version: 0.20.8
|
||||
'@atproto/syntax':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
@@ -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)
|
||||
@@ -302,17 +305,17 @@ importers:
|
||||
specifier: ^2.0.8
|
||||
version: 2.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@formatjs/intl-displaynames':
|
||||
specifier: ^6.8.13
|
||||
version: 6.8.13
|
||||
specifier: ^7.3.9
|
||||
version: 7.3.9
|
||||
'@formatjs/intl-locale':
|
||||
specifier: ^4.2.13
|
||||
version: 4.2.13
|
||||
specifier: ^5.3.8
|
||||
version: 5.3.8
|
||||
'@formatjs/intl-numberformat':
|
||||
specifier: ^8.15.6
|
||||
version: 8.15.6
|
||||
specifier: ^9.3.10
|
||||
version: 9.3.10
|
||||
'@formatjs/intl-pluralrules':
|
||||
specifier: ^5.4.6
|
||||
version: 5.4.6
|
||||
specifier: ^6.3.9
|
||||
version: 6.3.9
|
||||
'@growthbook/growthbook':
|
||||
specifier: ^1.6.5
|
||||
version: 1.6.5
|
||||
@@ -328,9 +331,6 @@ importers:
|
||||
'@lingui/react':
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0)
|
||||
'@miblanchard/react-native-slider':
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.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)
|
||||
'@mozzius/expo-dynamic-app-icon':
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.1(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)
|
||||
@@ -416,8 +416,8 @@ importers:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
date-fns:
|
||||
specifier: ^2.30.0
|
||||
version: 2.30.0
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
email-validator:
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
@@ -633,7 +633,7 @@ importers:
|
||||
version: 2.28.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-keyboard-controller:
|
||||
specifier: ^1.21.8
|
||||
version: 1.21.8(patch_hash=642d128c971380a1858f7a938dd715d6eb3bbc260c93188b65580d92e2226afe)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.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)
|
||||
version: 1.21.8(patch_hash=2f6791837622fb391c3e41fc044b91ddedbe503da14da86165ee0a73924b05f1)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.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-pager-view:
|
||||
specifier: 6.8.0
|
||||
version: 6.8.0(patch_hash=0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108)(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)
|
||||
@@ -877,8 +877,8 @@ packages:
|
||||
graphql:
|
||||
optional: true
|
||||
|
||||
'@atproto/api@0.20.6':
|
||||
resolution: {integrity: sha512-WnFPcUl+qZdXmt27+Tg93BDIvBt/WpXfLIiBzBTp3ms9aszM5hAsfc7G8KEsnsmnRvcm0xRfiKEjIt5FxTKdYg==}
|
||||
'@atproto/api@0.20.8':
|
||||
resolution: {integrity: sha512-rTkA6kOmA2axSrg6VgpdXpsCFWpofnHBOn6pKg69Ju5MpIHqk4haQMgjBcVh1G3kUxzwgSAr7SYrPS3dFe5Etg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/common-web@0.5.0':
|
||||
@@ -1652,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:
|
||||
@@ -2065,33 +2072,32 @@ packages:
|
||||
'@floating-ui/utils@0.2.11':
|
||||
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.6':
|
||||
resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==}
|
||||
'@formatjs/bigdecimal@0.2.5':
|
||||
resolution: {integrity: sha512-2XTKNrZRaCUyXK2976wfutqxMBuPO/S/zbJnQdysLI2Zy5mWPVNVEkE6tsTcSVWSE7DgO88t8DtBy+uf3I8bxg==}
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||
'@formatjs/fast-memoize@3.1.5':
|
||||
resolution: {integrity: sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==}
|
||||
|
||||
'@formatjs/intl-displaynames@6.8.13':
|
||||
resolution: {integrity: sha512-VbY7BdYJX5eURVKLk2grndUQtnbCLNbcJId/Sb/PsX7fWXiqWvg7qt/mecVHRzqoSEoGCQToKDxzpJj8RC0s3g==}
|
||||
'@formatjs/intl-displaynames@7.3.9':
|
||||
resolution: {integrity: sha512-UELvrHS171ElQW9iMCGqMR/HmUYQPYkYSz1A/SSkQtJ62ZH7aUWC1QnXPxL0CInQmxa/MgC7oE402XBoTe5nTQ==}
|
||||
|
||||
'@formatjs/intl-enumerator@1.8.12':
|
||||
resolution: {integrity: sha512-rT2hnyQ0q6RDG1jISMH+MP6wlHQLUC81C0BeYEzabJb48S4tU2HGCPUxsOqMa3vDAUmpnz9Kw5vYTD5hTkOVgg==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
'@formatjs/intl-getcanonicallocales@3.2.9':
|
||||
resolution: {integrity: sha512-b1dLzhFSeccG5X1PY2KodVFRGd0p6tBPc7pRBJaW6A7+fMeZnwHOAJLkMsGNDyvM9XailkkstE4jzmNa1GVh5w==}
|
||||
|
||||
'@formatjs/intl-getcanonicallocales@2.5.6':
|
||||
resolution: {integrity: sha512-CnBbc4St61RL06gDXlCZG08Gt41uiySgsdZNBExh8/c0FBONJCrAlQ9FsyalUdq9ze0nCeknJtMmO8JnB9xHgQ==}
|
||||
'@formatjs/intl-locale@5.3.8':
|
||||
resolution: {integrity: sha512-pJgqQlg4zFoyCiVMXlKPBWpY7fAxvsUiShNmB/l8BW2ZwmFMefMWCrDzgocSH9WR+Z3pkihfjh1XbalCZhXc1Q==}
|
||||
|
||||
'@formatjs/intl-locale@4.2.13':
|
||||
resolution: {integrity: sha512-Qcs0mP9JhEPCinPfpZ5JKzOa+VYDg94TQSFZozYuqG8R+hEGUTKyoyMqB5xi5QZT/hlB0CSgeNc/0fWMGtTFXA==}
|
||||
'@formatjs/intl-localematcher@0.8.9':
|
||||
resolution: {integrity: sha512-GmB0F/gYh4Hdl4rLWjgDsgT+x4pB54fkJeRh8kAZ4XFzKeCK8dGs+SBJWXO42QZtOUni+IDWKNuCw6wiL4lTvw==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
||||
'@formatjs/intl-numberformat@9.3.10':
|
||||
resolution: {integrity: sha512-wYboXVXZyTvuMBgwE0AxcXYxV8LM0IkH+lfCoBlRVNjCwNzdHVbRcUxDRHE+HbFkQ6/zWFkyguvIP5y3PAt+jw==}
|
||||
|
||||
'@formatjs/intl-numberformat@8.15.6':
|
||||
resolution: {integrity: sha512-htynTNKm5WOnbR521tNSMkWzX3yO6Z77qjOxvRujh5/A/UBKeoNElyuKCJltizdx3X33QNWQZC4aWjLKcccyeQ==}
|
||||
'@formatjs/intl-pluralrules@6.3.9':
|
||||
resolution: {integrity: sha512-0405uU1npNs5/N04tfIQ3pme96Xb51tEkCBTBKy8HMjAsssbhnK70gf8v3ogj1WqHf7WFRoFZu8clQTsvwBQNg==}
|
||||
|
||||
'@formatjs/intl-pluralrules@5.4.6':
|
||||
resolution: {integrity: sha512-2HlOq+c7KsSps829SJ3B5987coX5mzKx9NbPcNwQ07eq8FBHgB3HfMoxt5HvLsdk4oQwCjAEnocbtd+wVwZ2Kg==}
|
||||
'@formatjs/intl-supportedvaluesof@2.3.7':
|
||||
resolution: {integrity: sha512-ZTXEKijV5H08z19odngZPsXlmGndfukHuVnjd9qyER77XSk/yJwDMiSjoJYY5C5WxFs2UN4s/EMtu75e6Tp3ug==}
|
||||
|
||||
'@gorhom/portal@1.0.14':
|
||||
resolution: {integrity: sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==}
|
||||
@@ -2321,12 +2327,6 @@ packages:
|
||||
'@messageformat/parser@5.1.1':
|
||||
resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==}
|
||||
|
||||
'@miblanchard/react-native-slider@2.6.0':
|
||||
resolution: {integrity: sha512-o7hk/f/8vkqh6QNR5L52m+ws846fQeD/qNCC9CCSRdBqjq66KiCgbxzlhRzKM/gbtxcvMYMIEEJ1yes5cr6I3A==}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-native: '>=0.59'
|
||||
|
||||
'@mozzius/expo-dynamic-app-icon@1.8.1':
|
||||
resolution: {integrity: sha512-JWNY9gw06s+q54b2SqWf6BEo7IYuJCtjJDtca+wTo6kP5dH/wYK85JdLrMbdy+cwOMScEY85uU6Mjn0wkWTLnw==}
|
||||
peerDependencies:
|
||||
@@ -4789,13 +4789,12 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||
engines: {node: '>=0.11'}
|
||||
|
||||
date-fns@3.6.0:
|
||||
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
||||
|
||||
date-fns@4.4.0:
|
||||
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
|
||||
|
||||
debounce@1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
|
||||
@@ -9494,7 +9493,7 @@ snapshots:
|
||||
|
||||
'@0no-co/graphql.web@1.2.0': {}
|
||||
|
||||
'@atproto/api@0.20.6':
|
||||
'@atproto/api@0.20.8':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.0
|
||||
'@atproto/lexicon': 0.7.1
|
||||
@@ -10472,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
|
||||
@@ -11055,56 +11060,38 @@ snapshots:
|
||||
|
||||
'@floating-ui/utils@0.2.11': {}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.6':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
decimal.js: 10.6.0
|
||||
tslib: 2.8.1
|
||||
'@formatjs/bigdecimal@0.2.5': {}
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
'@formatjs/fast-memoize@3.1.5': {}
|
||||
|
||||
'@formatjs/intl-displaynames@6.8.13':
|
||||
'@formatjs/intl-displaynames@7.3.9':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
tslib: 2.8.1
|
||||
'@formatjs/intl-localematcher': 0.8.9
|
||||
|
||||
'@formatjs/intl-enumerator@1.8.12':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
tslib: 2.8.1
|
||||
'@formatjs/intl-getcanonicallocales@3.2.9': {}
|
||||
|
||||
'@formatjs/intl-getcanonicallocales@2.5.6':
|
||||
'@formatjs/intl-locale@5.3.8':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
'@formatjs/intl-getcanonicallocales': 3.2.9
|
||||
'@formatjs/intl-supportedvaluesof': 2.3.7
|
||||
|
||||
'@formatjs/intl-locale@4.2.13':
|
||||
'@formatjs/intl-localematcher@0.8.9':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/intl-enumerator': 1.8.12
|
||||
'@formatjs/intl-getcanonicallocales': 2.5.6
|
||||
tslib: 2.8.1
|
||||
'@formatjs/fast-memoize': 3.1.5
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
'@formatjs/intl-numberformat@9.3.10':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
'@formatjs/bigdecimal': 0.2.5
|
||||
'@formatjs/intl-localematcher': 0.8.9
|
||||
|
||||
'@formatjs/intl-numberformat@8.15.6':
|
||||
'@formatjs/intl-pluralrules@6.3.9':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
decimal.js: 10.6.0
|
||||
tslib: 2.8.1
|
||||
'@formatjs/bigdecimal': 0.2.5
|
||||
'@formatjs/intl-localematcher': 0.8.9
|
||||
|
||||
'@formatjs/intl-pluralrules@5.4.6':
|
||||
'@formatjs/intl-supportedvaluesof@2.3.7':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
decimal.js: 10.6.0
|
||||
tslib: 2.8.1
|
||||
'@formatjs/fast-memoize': 3.1.5
|
||||
|
||||
'@gorhom/portal@1.0.14(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:
|
||||
@@ -11462,11 +11449,6 @@ snapshots:
|
||||
dependencies:
|
||||
moo: 0.5.3
|
||||
|
||||
'@miblanchard/react-native-slider@2.6.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:
|
||||
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)
|
||||
|
||||
'@mozzius/expo-dynamic-app-icon@1.8.1(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/image-utils': 0.8.12
|
||||
@@ -14235,12 +14217,10 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
date-fns@2.30.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
|
||||
date-fns@3.6.0: {}
|
||||
|
||||
date-fns@4.4.0: {}
|
||||
|
||||
debounce@1.2.1: {}
|
||||
|
||||
debug@2.6.9:
|
||||
@@ -15033,7 +15013,7 @@ snapshots:
|
||||
require-from-string: 2.0.2
|
||||
resolve-from: 5.0.0
|
||||
|
||||
expo-modules-core@3.0.30(patch_hash=86e57bb33bc6c3f7021e948e099c2b5378772147b92a82a16e2e90ddcb857ace)(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):
|
||||
expo-modules-core@3.0.30(patch_hash=952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e)(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:
|
||||
invariant: 2.2.4
|
||||
react: 19.1.0
|
||||
@@ -15169,7 +15149,7 @@ snapshots:
|
||||
expo-font: 14.0.11(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)
|
||||
expo-keep-awake: 15.0.8(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)
|
||||
expo-modules-autolinking: 3.0.25
|
||||
expo-modules-core: 3.0.30(patch_hash=86e57bb33bc6c3f7021e948e099c2b5378772147b92a82a16e2e90ddcb857ace)(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)
|
||||
expo-modules-core: 3.0.30(patch_hash=952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e)(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)
|
||||
pretty-format: 29.7.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)
|
||||
@@ -17926,7 +17906,7 @@ 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)
|
||||
|
||||
react-native-keyboard-controller@1.21.8(patch_hash=642d128c971380a1858f7a938dd715d6eb3bbc260c93188b65580d92e2226afe)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.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-keyboard-controller@1.21.8(patch_hash=2f6791837622fb391c3e41fc044b91ddedbe503da14da86165ee0a73924b05f1)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.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:
|
||||
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)
|
||||
|
||||
+6
-6
@@ -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() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ContextMenuProvider>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<Splash isReady={isReady && hasCheckedLanding}>
|
||||
<VideoVolumeProvider>
|
||||
<Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
@@ -246,12 +246,12 @@ function App() {
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<BottomSheetProvider>
|
||||
<StarterPackProvider>
|
||||
<LandingProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<InnerApp />
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</LandingProvider>
|
||||
</BottomSheetProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
|
||||
+6
-6
@@ -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() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ContextMenuProvider>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<Splash isReady={isReady && hasCheckedLanding}>
|
||||
<VideoVolumeProvider>
|
||||
<ActiveVideoProvider>
|
||||
<Fragment
|
||||
@@ -224,9 +224,9 @@ function App() {
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<LandingProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</LandingProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
|
||||
@@ -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}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MessagesJoinRequests"
|
||||
getComponent={() => MessagesJoinRequestsScreen}
|
||||
options={{title: title(msg`Requests to join`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MessagesSettings"
|
||||
getComponent={() => 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)
|
||||
|
||||
@@ -9,14 +9,11 @@ export enum Features {
|
||||
ImportContactsOnboardingDisable = 'import_contacts:onboarding:disable',
|
||||
ImportContactsSettingsDisable = 'import_contacts:settings:disable',
|
||||
LiveNowBetaDisable = 'live_now_beta:disable',
|
||||
ImageUploadsHighResolution = 'image_uploads:high_resolution',
|
||||
ImageUploadsBlobSize2mbEnabled = 'image_uploads:blob_size_2mb:enabled',
|
||||
GroupChatsEnable = 'group_chats:enable',
|
||||
GroupChatsHasBeenReleased = 'group_chats:has_been_released',
|
||||
DmsNewMessageComposerEnable = 'dms:new_message_composer:enable',
|
||||
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
||||
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
||||
LargeVideoUploads = 'large_video_uploads:enable',
|
||||
|
||||
AATest = 'aa-test',
|
||||
}
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -232,7 +232,11 @@ function LightboxGallery({
|
||||
// out of view. Only scrollable once expanded.
|
||||
style={[
|
||||
styles.altScroll,
|
||||
{backgroundColor: 'rgba(0, 0, 0, 0.45)'},
|
||||
{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
// @ts-expect-error web only
|
||||
backdropFilter: 'blur(16px)',
|
||||
},
|
||||
delayedFadeInAnim,
|
||||
]}
|
||||
scrollEnabled={isAltExpanded}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import {useRef} from 'react'
|
||||
import {
|
||||
LayoutAnimation,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {LayoutAnimation, ScrollView, StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
useSafeAreaFrame,
|
||||
useSafeAreaInsets,
|
||||
} from 'react-native-safe-area-context'
|
||||
import {BlurView} from 'expo-blur'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = {
|
||||
@@ -46,37 +41,66 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
|
||||
{paddingBottom: insets.bottom + 8},
|
||||
]}>
|
||||
<View style={[a.mx_md, styles.altWrap]}>
|
||||
<ScrollView
|
||||
style={{maxHeight}}
|
||||
scrollEnabled={isAltExpanded}
|
||||
onMomentumScrollBegin={() => {
|
||||
isMomentumScrolling.current = true
|
||||
}}
|
||||
onMomentumScrollEnd={() => {
|
||||
isMomentumScrolling.current = false
|
||||
}}
|
||||
contentContainerStyle={[a.px_md, a.py_sm]}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={l`Expand alt text`}
|
||||
accessibilityHint=""
|
||||
onPress={() => {
|
||||
if (isMomentumScrolling.current) return
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 450,
|
||||
update: {type: 'spring', springDamping: 1},
|
||||
})
|
||||
onToggleAltExpanded()
|
||||
}}>
|
||||
<Text
|
||||
emoji
|
||||
selectable
|
||||
style={[a.text_sm, {color: t.palette.white}]}
|
||||
numberOfLines={isAltExpanded ? undefined : 3}>
|
||||
{altText}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
<BlurView
|
||||
intensity={16}
|
||||
tint="dark"
|
||||
style={[
|
||||
// Tint kept over the blur so dense, text-heavy images stay
|
||||
// readable. On Android the blur falls back to a flat overlay, so
|
||||
// bump the opacity to keep the contrast the real blur provides
|
||||
// elsewhere.
|
||||
platform({
|
||||
ios: {backgroundColor: 'rgba(0, 0, 0, 0.5)'},
|
||||
android: {backgroundColor: 'rgba(0, 0, 0, 0.7)'},
|
||||
}),
|
||||
]}>
|
||||
<ScrollView
|
||||
style={{maxHeight}}
|
||||
scrollEnabled={isAltExpanded}
|
||||
onMomentumScrollBegin={() => {
|
||||
isMomentumScrolling.current = true
|
||||
}}
|
||||
onMomentumScrollEnd={() => {
|
||||
isMomentumScrolling.current = false
|
||||
}}
|
||||
contentContainerStyle={[a.px_md, a.py_sm]}>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={l`Expand alt text`}
|
||||
accessibilityHint="">
|
||||
{/*
|
||||
* 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.
|
||||
*/}
|
||||
<Text
|
||||
emoji
|
||||
selectable
|
||||
style={[a.text_sm, {color: t.palette.white}]}
|
||||
numberOfLines={isAltExpanded ? undefined : 3}
|
||||
onPress={() => {
|
||||
if (isMomentumScrolling.current) return
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 450,
|
||||
update: {type: 'spring', springDamping: 1},
|
||||
})
|
||||
onToggleAltExpanded()
|
||||
}}
|
||||
onLongPress={() => {}}>
|
||||
{altText}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlurView>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -84,7 +108,6 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
altWrap: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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<ViewStyle>
|
||||
peekable?: boolean
|
||||
}) {
|
||||
const e = bsky.post.parseEmbed(embed)
|
||||
|
||||
@@ -27,13 +34,17 @@ export function Embed({
|
||||
if (e.type === 'images') {
|
||||
return (
|
||||
<Outer style={style}>
|
||||
{e.view.images.map(image => (
|
||||
<ImageItem
|
||||
key={image.thumb}
|
||||
thumbnail={image.thumb}
|
||||
alt={image.alt}
|
||||
/>
|
||||
))}
|
||||
{e.view.images.map(image =>
|
||||
peekable ? (
|
||||
<PeekableImageItem key={image.thumb} image={image} />
|
||||
) : (
|
||||
<ImageItem
|
||||
key={image.thumb}
|
||||
thumbnail={image.thumb}
|
||||
alt={image.alt}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Outer>
|
||||
)
|
||||
} else if (e.type === 'link') {
|
||||
@@ -156,6 +167,45 @@ export function VideoItem({
|
||||
)
|
||||
}
|
||||
|
||||
function PeekableImageItem({image}: {image: AppBskyEmbedImages.ViewImage}) {
|
||||
const {t: l} = useLingui()
|
||||
const saveImage = useSaveImageToMediaLibrary()
|
||||
|
||||
const aspect =
|
||||
image.aspectRatio && image.aspectRatio.height > 0
|
||||
? image.aspectRatio.width / image.aspectRatio.height
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<PeekMenu.Root style={[a.flex_1, {maxWidth: 100}]}>
|
||||
<PeekMenu.Trigger
|
||||
preview={{
|
||||
type: 'image',
|
||||
uri: image.fullsize,
|
||||
thumbUri: image.thumb,
|
||||
aspectRatio: aspect && aspect > 0 ? aspect : 1,
|
||||
}}
|
||||
borderRadius={tokens.borderRadius.xs}>
|
||||
<ImageItem thumbnail={image.thumb} alt={image.alt} />
|
||||
</PeekMenu.Trigger>
|
||||
<PeekMenu.Menu>
|
||||
<PeekMenu.MenuItem
|
||||
id="save"
|
||||
onSelect={() => void saveImage(image.fullsize)}>
|
||||
<PeekMenu.MenuItemIcon icon={DownloadIcon} />
|
||||
<PeekMenu.MenuItemText>{l`Save image`}</PeekMenu.MenuItemText>
|
||||
</PeekMenu.MenuItem>
|
||||
<PeekMenu.MenuItem
|
||||
id="share"
|
||||
onSelect={() => void shareImageModal({uri: image.fullsize})}>
|
||||
<PeekMenu.MenuItemIcon icon={ShareIcon} />
|
||||
<PeekMenu.MenuItemText>{l`Share`}</PeekMenu.MenuItemText>
|
||||
</PeekMenu.MenuItem>
|
||||
</PeekMenu.Menu>
|
||||
</PeekMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
altContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@bsky.app/peek-menu'
|
||||
@@ -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<ViewStyle>
|
||||
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 (
|
||||
<PeekMenu.Root style={style}>
|
||||
<PeekMenu.Trigger
|
||||
preview={{
|
||||
type: 'image',
|
||||
uri: fullsizeUri,
|
||||
thumbUri,
|
||||
aspectRatio: aspectRatio && aspectRatio > 0 ? aspectRatio : 1,
|
||||
}}
|
||||
borderRadius={borderRadius}
|
||||
onPreviewPress={onPreviewPress}>
|
||||
{children}
|
||||
</PeekMenu.Trigger>
|
||||
<PeekMenu.Menu>
|
||||
<PeekMenu.MenuItem id="save" onSelect={handleSave}>
|
||||
<PeekMenu.MenuItemIcon icon={DownloadIcon} />
|
||||
<PeekMenu.MenuItemText>{l`Save image`}</PeekMenu.MenuItemText>
|
||||
</PeekMenu.MenuItem>
|
||||
<PeekMenu.MenuItem id="share" onSelect={handleShare}>
|
||||
<PeekMenu.MenuItemIcon icon={ShareIcon} />
|
||||
<PeekMenu.MenuItemText>{l`Share`}</PeekMenu.MenuItemText>
|
||||
</PeekMenu.MenuItem>
|
||||
</PeekMenu.Menu>
|
||||
</PeekMenu.Root>
|
||||
)
|
||||
}
|
||||
@@ -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<AnimatedRef<any> | null>(null)
|
||||
const singleDimsRef = useRef<Dimensions | null>(null)
|
||||
|
||||
if (images.length > 0) {
|
||||
const items = images.map(img => ({
|
||||
uri: img.fullsize,
|
||||
@@ -59,24 +66,49 @@ 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 (
|
||||
<View style={[a.mt_sm, rest.style]}>
|
||||
<AutoSizedImage
|
||||
crop={
|
||||
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
||||
? 'none'
|
||||
: rest.viewContext ===
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
? 'square'
|
||||
: 'constrained'
|
||||
}
|
||||
image={image}
|
||||
onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])}
|
||||
onPressIn={() => onPressIn(0)}
|
||||
hideBadge={
|
||||
rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
}
|
||||
/>
|
||||
<ImageContextMenu
|
||||
fullsizeUri={image.fullsize}
|
||||
thumbUri={image.thumb}
|
||||
aspectRatio={aspect}
|
||||
borderRadius={tokens.borderRadius.md}
|
||||
onPreviewPress={openFromSingle}>
|
||||
<AutoSizedImage
|
||||
crop={
|
||||
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
||||
? 'none'
|
||||
: rest.viewContext ===
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
? 'square'
|
||||
: 'constrained'
|
||||
}
|
||||
image={image}
|
||||
onContainerRef={ref => {
|
||||
singleContainerRef.current = ref
|
||||
}}
|
||||
onDimsChange={dims => {
|
||||
singleDimsRef.current = dims
|
||||
}}
|
||||
onPress={(containerRef, dims) =>
|
||||
onPress(0, [containerRef], [dims])
|
||||
}
|
||||
onPressIn={() => onPressIn(0)}
|
||||
hideBadge={
|
||||
rest.viewContext ===
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
}
|
||||
/>
|
||||
</ImageContextMenu>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {CountWheel} from '#/lib/custom-animations/CountWheel'
|
||||
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
@@ -93,7 +92,6 @@ let PostControls = ({
|
||||
const requireAuth = useRequireAuth()
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
const {captureAction} = useProgressGuideControls()
|
||||
const playHaptic = useHaptics()
|
||||
const isBlocked = Boolean(
|
||||
post.author.viewer?.blocking ||
|
||||
post.author.viewer?.blockedBy ||
|
||||
@@ -116,7 +114,6 @@ let PostControls = ({
|
||||
try {
|
||||
setHasLikeIconBeenToggled(true)
|
||||
if (!post.viewer?.like) {
|
||||
playHaptic('Light')
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionLike',
|
||||
|
||||
@@ -224,6 +224,7 @@ export function Basic({
|
||||
cancelButtonCta,
|
||||
confirmButtonCta,
|
||||
onConfirm,
|
||||
onClose,
|
||||
confirmButtonColor,
|
||||
showCancel = true,
|
||||
}: React.PropsWithChildren<{
|
||||
@@ -240,11 +241,12 @@ export function Basic({
|
||||
* should NOT close the dialog as a side effect of this method.
|
||||
*/
|
||||
onConfirm: (e: GestureResponderEvent) => void
|
||||
onClose?: () => void
|
||||
confirmButtonColor?: ButtonColor
|
||||
showCancel?: boolean
|
||||
}>) {
|
||||
return (
|
||||
<Outer control={control} testID="confirmModal">
|
||||
<Outer control={control} testID="confirmModal" onClose={onClose}>
|
||||
<Content>
|
||||
<TitleText>{title}</TitleText>
|
||||
{description && <DescriptionText>{description}</DescriptionText>}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,6 +31,7 @@ export function Text({
|
||||
[
|
||||
a.text_sm,
|
||||
t.atoms.text,
|
||||
a.leading_snug,
|
||||
web(numberOfLines === 1 && numberOfLinesClippingFix),
|
||||
style,
|
||||
],
|
||||
|
||||
@@ -219,7 +219,7 @@ export function AddMembersFlow({
|
||||
}
|
||||
}
|
||||
|
||||
if (searchText === '') {
|
||||
if (searchText === '' && _items.length > 0) {
|
||||
_items.unshift({
|
||||
type: 'label',
|
||||
key: 'suggested',
|
||||
|
||||
@@ -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 (
|
||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
onClose={onClose}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={l`Would you like to block this user and/or delete this conversation?`}
|
||||
|
||||
@@ -316,6 +316,7 @@ export function InitiateChatFlow({
|
||||
|
||||
if (
|
||||
searchText === '' &&
|
||||
_items.length > 0 &&
|
||||
(chatState === ChatState.NEW_CHAT ||
|
||||
chatState === ChatState.NEW_GROUP_CHAT)
|
||||
) {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
{IS_NATIVE && reactionsAvailable && (
|
||||
<ContextMenu.AuxiliaryView
|
||||
align={isFromSelf ? 'right' : 'left'}
|
||||
style={[isFromSelf && isGroupChatEnabled ? null : a.ml_sm]}>
|
||||
<EmojiReactionPicker
|
||||
message={message}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
/>
|
||||
</ContextMenu.AuxiliaryView>
|
||||
)}
|
||||
|
||||
<ContextMenu.Trigger
|
||||
label={l`Message options`}
|
||||
contentLabel={l`Message from @${
|
||||
sender?.handle ?? 'unknown' // should always be defined
|
||||
}: ${message.text}`}>
|
||||
{children}
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Outer
|
||||
<ContextMenu.Root>
|
||||
{IS_NATIVE && reactionsAvailable && (
|
||||
<ContextMenu.AuxiliaryView
|
||||
align={isFromSelf ? 'right' : 'left'}
|
||||
label={l`Sent at ${i18n.date(new Date(message.sentAt), {
|
||||
timeStyle: 'short',
|
||||
})}`}
|
||||
style={[isFromSelf && isGroupChatEnabled ? null : a.ml_sm]}>
|
||||
{message.text.length > 0 && (
|
||||
<>
|
||||
<ContextMenu.Item
|
||||
testID="messageDropdownTranslateBtn"
|
||||
label={l`Translate`}
|
||||
onPress={onPressTranslateMessage}>
|
||||
<ContextMenu.ItemIcon icon={LanguageIcon} position="left" />
|
||||
<ContextMenu.ItemText>{l`Translate`}</ContextMenu.ItemText>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item
|
||||
testID="messageDropdownCopyBtn"
|
||||
label={l`Copy message text`}
|
||||
onPress={onCopyMessage}>
|
||||
<ContextMenu.ItemIcon icon={ClipboardIcon} position="left" />
|
||||
<ContextMenu.ItemText>
|
||||
{l`Copy message text`}
|
||||
</ContextMenu.ItemText>
|
||||
</ContextMenu.Item>
|
||||
</>
|
||||
)}
|
||||
<EmojiReactionPicker
|
||||
message={message}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
/>
|
||||
</ContextMenu.AuxiliaryView>
|
||||
)}
|
||||
|
||||
<ContextMenu.Trigger
|
||||
label={l`Message options`}
|
||||
contentLabel={l`Message from @${
|
||||
sender?.handle ?? 'unknown' // should always be defined
|
||||
}: ${message.text}`}>
|
||||
{children}
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Outer
|
||||
align={isFromSelf ? 'right' : 'left'}
|
||||
label={l`Sent at ${i18n.date(new Date(message.sentAt), {
|
||||
timeStyle: 'short',
|
||||
})}`}
|
||||
style={[isFromSelf && isGroupChatEnabled ? null : a.ml_sm]}>
|
||||
{message.text.length > 0 && (
|
||||
<>
|
||||
<ContextMenu.Item
|
||||
testID="messageDropdownTranslateBtn"
|
||||
label={l`Translate`}
|
||||
onPress={onPressTranslateMessage}>
|
||||
<ContextMenu.ItemIcon icon={LanguageIcon} position="left" />
|
||||
<ContextMenu.ItemText>{l`Translate`}</ContextMenu.ItemText>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item
|
||||
testID="messageDropdownCopyBtn"
|
||||
label={l`Copy message text`}
|
||||
onPress={onCopyMessage}>
|
||||
<ContextMenu.ItemIcon icon={ClipboardIcon} position="left" />
|
||||
<ContextMenu.ItemText>
|
||||
{l`Copy message text`}
|
||||
</ContextMenu.ItemText>
|
||||
</ContextMenu.Item>
|
||||
</>
|
||||
)}
|
||||
<ContextMenu.Item
|
||||
destructive
|
||||
testID="messageDropdownDeleteBtn"
|
||||
label={l`Delete message for me`}
|
||||
onPress={() => openDeleteMessage(message)}>
|
||||
<ContextMenu.ItemIcon icon={TrashIcon} position="left" />
|
||||
<ContextMenu.ItemText>{l`Delete for me`}</ContextMenu.ItemText>
|
||||
</ContextMenu.Item>
|
||||
{!isFromSelf && (
|
||||
<ContextMenu.Item
|
||||
destructive
|
||||
testID="messageDropdownDeleteBtn"
|
||||
label={l`Delete message for me`}
|
||||
onPress={() => deleteControl.open()}>
|
||||
<ContextMenu.ItemIcon icon={TrashIcon} position="left" />
|
||||
<ContextMenu.ItemText>{l`Delete for me`}</ContextMenu.ItemText>
|
||||
testID="messageDropdownReportBtn"
|
||||
label={l`Report message`}
|
||||
onPress={() => openReportMessage(message, senderProfile)}>
|
||||
<ContextMenu.ItemIcon icon={FlagIcon} position="left" />
|
||||
<ContextMenu.ItemText>{l`Report`}</ContextMenu.ItemText>
|
||||
</ContextMenu.Item>
|
||||
{!isFromSelf && (
|
||||
<ContextMenu.Item
|
||||
destructive
|
||||
testID="messageDropdownReportBtn"
|
||||
label={l`Report message`}
|
||||
onPress={() => reportControl.open()}>
|
||||
<ContextMenu.ItemIcon icon={FlagIcon} position="left" />
|
||||
<ContextMenu.ItemText>{l`Report`}</ContextMenu.ItemText>
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
</ContextMenu.Outer>
|
||||
</ContextMenu.Root>
|
||||
<ReportDialog
|
||||
control={reportControl}
|
||||
subject={{
|
||||
view: 'message',
|
||||
convoId: convo.convo.view.id,
|
||||
message,
|
||||
}}
|
||||
onAfterSubmit={() => {
|
||||
if (sender) {
|
||||
unstableCacheProfileView(queryClient, sender)
|
||||
}
|
||||
blockOrDeleteControl.open()
|
||||
}}
|
||||
/>
|
||||
<AfterReportDialog
|
||||
control={blockOrDeleteControl}
|
||||
currentScreen="conversation"
|
||||
params={{
|
||||
convoId: convo.convo.view.id,
|
||||
did: message.sender.did,
|
||||
}}
|
||||
/>
|
||||
<Prompt.Basic
|
||||
control={deleteControl}
|
||||
title={l`Delete message`}
|
||||
description={l`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants.`}
|
||||
confirmButtonCta={l`Delete`}
|
||||
confirmButtonColor="negative"
|
||||
onConfirm={onDelete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ContextMenu.Outer>
|
||||
</ContextMenu.Root>
|
||||
)
|
||||
}
|
||||
MessageContextMenu = memo(MessageContextMenu)
|
||||
|
||||
@@ -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 = ({
|
||||
<ProfileCard.AvatarPlaceholder size={AVATAR_SIZE} />
|
||||
)
|
||||
|
||||
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 => (
|
||||
<Animated.View
|
||||
entering={native(ZoomIn.springify(200).delay(400))}
|
||||
@@ -377,13 +353,6 @@ let MessageItem = ({
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
<ReactionsDialog
|
||||
control={reactionsControl}
|
||||
relatedProfiles={relatedProfiles}
|
||||
message={message}
|
||||
reactions={message.reactions}
|
||||
groupedReactions={groupedReactions}
|
||||
/>
|
||||
</LayoutAnimationConfig>
|
||||
)
|
||||
|
||||
|
||||
@@ -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<MessageDialogsContextType | null>(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<ChatBskyConvoDefs.MessageView | null>(null)
|
||||
const [reportTarget, setReportTarget] = useState<{
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
senderProfile: bsky.profile.AnyProfileView | undefined
|
||||
} | null>(null)
|
||||
const [afterReportTarget, setAfterReportTarget] =
|
||||
useState<ChatBskyConvoDefs.MessageView | null>(null)
|
||||
const [reactionsTarget, setReactionsTarget] =
|
||||
useState<ChatBskyConvoDefs.MessageView | null>(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<MessageDialogsContextType>(
|
||||
() => ({openDeleteMessage, openReportMessage, openReactions}),
|
||||
[openDeleteMessage, openReportMessage, openReactions],
|
||||
)
|
||||
|
||||
const reportSubject = reportTarget
|
||||
? ({
|
||||
view: 'message',
|
||||
convoId: convo.convo.view.id,
|
||||
message: reportTarget.message,
|
||||
} as const)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{children}
|
||||
<ReportDialog
|
||||
control={reportControl}
|
||||
subject={reportSubject}
|
||||
onAfterSubmit={onAfterReportSubmit}
|
||||
onClose={() => setReportTarget(null)}
|
||||
/>
|
||||
{afterReportTarget && (
|
||||
<AfterReportDialog
|
||||
control={afterReportControl}
|
||||
currentScreen="conversation"
|
||||
params={{
|
||||
convoId: convo.convo.view.id,
|
||||
did: afterReportTarget.sender.did,
|
||||
}}
|
||||
onClose={() => setAfterReportTarget(null)}
|
||||
/>
|
||||
)}
|
||||
{reactionsTarget && (
|
||||
<ReactionsDialog
|
||||
control={reactionsControl}
|
||||
relatedProfiles={convo.relatedProfiles}
|
||||
message={reactionsTarget}
|
||||
onClose={() => setReactionsTarget(null)}
|
||||
/>
|
||||
)}
|
||||
<Prompt.Basic
|
||||
control={deleteControl}
|
||||
title={l`Delete message`}
|
||||
description={l`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants.`}
|
||||
confirmButtonCta={l`Delete`}
|
||||
confirmButtonColor="negative"
|
||||
onConfirm={onConfirmDelete}
|
||||
onClose={() => setDeleteTarget(null)}
|
||||
/>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
@@ -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<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||
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 (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
onClose={() => setSelected('all')}
|
||||
onClose={() => {
|
||||
setSelected('all')
|
||||
onClose?.()
|
||||
}}
|
||||
nativeOptions={{
|
||||
preventExpansion: true,
|
||||
minHeight: screenHeight / 2,
|
||||
@@ -388,3 +392,25 @@ function ReactionTab({
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export function groupReactions(
|
||||
reactions: ChatBskyConvoDefs.ReactionView[] | undefined,
|
||||
): Reaction[] {
|
||||
const grouped = new Map<string, Reaction>()
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -212,9 +212,12 @@ export function Item({
|
||||
)
|
||||
|
||||
const highlightStyle = highlightRow
|
||||
? selected
|
||||
? [a.rounded_full, a.p_md, {backgroundColor: t.palette.primary_50}]
|
||||
: [a.rounded_full, a.p_md]
|
||||
? [
|
||||
a.rounded_full,
|
||||
a.p_md,
|
||||
hovered && t.atoms.bg_contrast_25,
|
||||
selected && {backgroundColor: t.palette.primary_50},
|
||||
]
|
||||
: null
|
||||
|
||||
return (
|
||||
@@ -588,6 +591,7 @@ export function RadioWithLabel({
|
||||
style={[
|
||||
a.font_medium,
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
a.leading_tight,
|
||||
selected ? t.atoms.text : t.atoms.text_contrast_high,
|
||||
]}>
|
||||
|
||||
+28
-4
@@ -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
|
||||
}
|
||||
+7
-5
@@ -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)
|
||||
@@ -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<Svg>
|
||||
> & {
|
||||
svgPaths: string[]
|
||||
svgViewBox: string
|
||||
svgStrokeWidth: number
|
||||
}
|
||||
|
||||
export const IconTemplate_Stroke2_Corner0_Rounded = forwardRef(
|
||||
function LogoImpl(props: Props, ref) {
|
||||
const {fill, size, style, ...rest} = useCommonSVGProps(props)
|
||||
@@ -41,7 +53,7 @@ export function createSinglePathSVG({
|
||||
strokeLinecap?: 'butt' | 'round' | 'square'
|
||||
strokeLinejoin?: 'miter' | 'round' | 'bevel'
|
||||
}) {
|
||||
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
||||
const Icon = forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
||||
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
|
||||
|
||||
const hasStroke = strokeWidth > 0
|
||||
@@ -68,7 +80,11 @@ export function createSinglePathSVG({
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
})
|
||||
}) as IconWithSvgMeta
|
||||
Icon.svgPaths = [path]
|
||||
Icon.svgViewBox = viewBox || '0 0 24 24'
|
||||
Icon.svgStrokeWidth = strokeWidth
|
||||
return Icon
|
||||
}
|
||||
|
||||
export function createMultiPathSVG({
|
||||
@@ -78,7 +94,7 @@ export function createMultiPathSVG({
|
||||
paths: string[]
|
||||
viewBox?: string
|
||||
}) {
|
||||
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
||||
const Icon = forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
||||
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
|
||||
|
||||
return (
|
||||
@@ -102,5 +118,9 @@ export function createMultiPathSVG({
|
||||
))}
|
||||
</Svg>
|
||||
)
|
||||
})
|
||||
}) as IconWithSvgMeta
|
||||
Icon.svgPaths = paths
|
||||
Icon.svgViewBox = viewBox || '0 0 24 24'
|
||||
Icon.svgStrokeWidth = 0
|
||||
return Icon
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useMemo, useRef} from 'react'
|
||||
import {useEffect, useMemo, useRef} from 'react'
|
||||
import {type DimensionValue, Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
type AnimatedRef,
|
||||
@@ -69,6 +69,8 @@ export function AutoSizedImage({
|
||||
onPress,
|
||||
onLongPress,
|
||||
onPressIn,
|
||||
onContainerRef,
|
||||
onDimsChange,
|
||||
}: {
|
||||
image: AppBskyEmbedImages.ViewImage
|
||||
crop?: 'none' | 'square' | 'constrained'
|
||||
@@ -79,6 +81,11 @@ export function AutoSizedImage({
|
||||
) => void
|
||||
onLongPress?: () => void
|
||||
onPressIn?: () => void
|
||||
/** Fires once with the internal container ref so a parent can drive its
|
||||
* own lightbox-return animation without waiting for an `onPress`. */
|
||||
onContainerRef?: (ref: AnimatedRef<any>) => void
|
||||
/** Fires when the underlying image reports its natural dimensions. */
|
||||
onDimsChange?: (dims: Dimensions) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -86,6 +93,10 @@ export function AutoSizedImage({
|
||||
const containerRef = useAnimatedRef()
|
||||
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
onContainerRef?.(containerRef)
|
||||
}, [containerRef, onContainerRef])
|
||||
|
||||
let aspectRatio: number | undefined
|
||||
const dims = image.aspectRatio
|
||||
if (dims) {
|
||||
@@ -122,10 +133,12 @@ export function AutoSizedImage({
|
||||
accessibilityHint=""
|
||||
onLoad={e => {
|
||||
if (!isContain) {
|
||||
fetchedDimsRef.current = {
|
||||
const dims = {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
}
|
||||
fetchedDimsRef.current = dims
|
||||
onDimsChange?.(dims)
|
||||
}
|
||||
}}
|
||||
loading="lazy"
|
||||
|
||||
@@ -24,7 +24,7 @@ import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
|
||||
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
||||
import {
|
||||
@@ -36,6 +36,7 @@ import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandle
|
||||
import {usePointerHandlers} from '#/components/images/Gallery/usePointerHandlers'
|
||||
import {getAspectRatio} from '#/components/images/Gallery/utils'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
@@ -264,6 +265,21 @@ export function Gallery({
|
||||
data={images}
|
||||
keyExtractor={(item, index) => item.thumb + index}
|
||||
renderItem={({item, index}) => {
|
||||
const openLightboxAtIndex = onPress
|
||||
? () => {
|
||||
ax.metric('post:gallery:openLightbox', {
|
||||
fromImage: index + 1, // convert to 1-based index for easier analysis
|
||||
totalImages: images.length,
|
||||
})
|
||||
const refs: AnimatedRef<any>[] = []
|
||||
const dims: (Dimensions | null)[] = []
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
refs.push(containerRefsRef.current.get(i)!)
|
||||
dims.push(thumbDimsRef.current.get(i) ?? null)
|
||||
}
|
||||
onPress(index, refs, dims)
|
||||
}
|
||||
: undefined
|
||||
return (
|
||||
<GalleryImage
|
||||
hideBadges={hideBadges}
|
||||
@@ -288,24 +304,9 @@ export function Gallery({
|
||||
onThumbDims={(i, dims) => {
|
||||
thumbDimsRef.current.set(i, dims)
|
||||
}}
|
||||
onPress={
|
||||
onPress
|
||||
? () => {
|
||||
ax.metric('post:gallery:openLightbox', {
|
||||
fromImage: index + 1, // convert to 1-based index for easier analysis
|
||||
totalImages: images.length,
|
||||
})
|
||||
const refs: AnimatedRef<any>[] = []
|
||||
const dims: (Dimensions | null)[] = []
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
refs.push(containerRefsRef.current.get(i)!)
|
||||
dims.push(thumbDimsRef.current.get(i) ?? null)
|
||||
}
|
||||
onPress(index, refs, dims)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onPress={openLightboxAtIndex}
|
||||
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
||||
onPreviewPress={openLightboxAtIndex}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
@@ -378,6 +379,7 @@ function GalleryImage({
|
||||
onThumbDims,
|
||||
onPress,
|
||||
onPressIn,
|
||||
onPreviewPress,
|
||||
}: {
|
||||
contentHeight: number
|
||||
image: AppBskyEmbedImages.ViewImage
|
||||
@@ -391,6 +393,7 @@ function GalleryImage({
|
||||
onThumbDims: (index: number, dims: Dimensions) => void
|
||||
onPress?: () => void
|
||||
onPressIn?: () => void
|
||||
onPreviewPress?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
@@ -416,124 +419,131 @@ function GalleryImage({
|
||||
collapsable={false}
|
||||
aria-roledescription={l`slide`}
|
||||
aria-label={image.alt || l`Image ${index + 1} of ${imageCount}`}>
|
||||
<Pressable
|
||||
ref={itemRef}
|
||||
tabIndex={index === 0 ? 0 : -1}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={image.alt || l`Image ${index + 1}`}
|
||||
accessibilityHint={l`Opens full image`}
|
||||
android_ripple={{
|
||||
color: utils.alpha(t.atoms.bg.backgroundColor, 0.2),
|
||||
foreground: true,
|
||||
}}
|
||||
style={({pressed}) => [
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
web([
|
||||
{
|
||||
cursor: 'inherit',
|
||||
outline: 0,
|
||||
border: 0,
|
||||
},
|
||||
a.transition_transform,
|
||||
{transitionDuration: '200ms'},
|
||||
pressed && {transform: [{scale: 0.99}]},
|
||||
]),
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: image.thumb}}
|
||||
contentFit="cover"
|
||||
accessible={true}
|
||||
accessibilityLabel={image.alt}
|
||||
accessibilityHint=""
|
||||
accessibilityIgnoresInvertColors
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
style={[dims]}
|
||||
onLoad={e => {
|
||||
const ar = getAspectRatio(e.source)
|
||||
if (ar && ar !== aspectRatio) {
|
||||
setAspectRatio(ar)
|
||||
}
|
||||
onThumbDims(index, {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
})
|
||||
<ImageContextMenu
|
||||
fullsizeUri={image.fullsize}
|
||||
thumbUri={image.thumb}
|
||||
aspectRatio={aspectRatio}
|
||||
borderRadius={tokens.borderRadius.md}
|
||||
onPreviewPress={onPreviewPress}>
|
||||
<Pressable
|
||||
ref={itemRef}
|
||||
tabIndex={index === 0 ? 0 : -1}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={image.alt || l`Image ${index + 1}`}
|
||||
accessibilityHint={l`Opens full image`}
|
||||
android_ripple={{
|
||||
color: utils.alpha(t.atoms.bg.backgroundColor, 0.2),
|
||||
foreground: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
{(hasAlt || isCropped) && !hideBadges ? (
|
||||
<View
|
||||
accessible={false}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.flex_row,
|
||||
style={({pressed}) => [
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
web([
|
||||
{
|
||||
bottom: a.p_xs.padding,
|
||||
right: a.p_xs.padding,
|
||||
gap: 3,
|
||||
cursor: 'inherit',
|
||||
outline: 0,
|
||||
border: 0,
|
||||
},
|
||||
largeAltBadge && {
|
||||
gap: 4,
|
||||
},
|
||||
]}>
|
||||
{isCropped && (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Fullscreen
|
||||
fill={t.atoms.text_contrast_high.color}
|
||||
width={largeAltBadge ? 18 : 12}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{hasAlt && (
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
largeAltBadge ? a.text_xs : {fontSize: 8},
|
||||
]}>
|
||||
<Trans>ALT</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
a.transition_transform,
|
||||
{transitionDuration: '200ms'},
|
||||
pressed && {transform: [{scale: 0.99}]},
|
||||
]),
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: image.thumb}}
|
||||
contentFit="cover"
|
||||
accessible={true}
|
||||
accessibilityLabel={image.alt}
|
||||
accessibilityHint=""
|
||||
accessibilityIgnoresInvertColors
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
style={[dims]}
|
||||
onLoad={e => {
|
||||
const ar = getAspectRatio(e.source)
|
||||
if (ar && ar !== aspectRatio) {
|
||||
setAspectRatio(ar)
|
||||
}
|
||||
onThumbDims(index, {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<MediaInsetBorder
|
||||
style={
|
||||
focused && {
|
||||
borderWidth: 2,
|
||||
{(hasAlt || isCropped) && !hideBadges ? (
|
||||
<View
|
||||
accessible={false}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.flex_row,
|
||||
{
|
||||
bottom: a.p_xs.padding,
|
||||
right: a.p_xs.padding,
|
||||
gap: 3,
|
||||
},
|
||||
largeAltBadge && {
|
||||
gap: 4,
|
||||
},
|
||||
]}>
|
||||
{isCropped && (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Fullscreen
|
||||
fill={t.atoms.text_contrast_high.color}
|
||||
width={largeAltBadge ? 18 : 12}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{hasAlt && (
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
largeAltBadge ? a.text_xs : {fontSize: 8},
|
||||
]}>
|
||||
<Trans>ALT</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<MediaInsetBorder
|
||||
style={
|
||||
focused && {
|
||||
borderWidth: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
/>
|
||||
</Pressable>
|
||||
</ImageContextMenu>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {type Dimensions} from '#/lib/media/types'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
@@ -52,46 +53,63 @@ export function GalleryItem({
|
||||
const hasAlt = !!image.alt
|
||||
const hideBadges =
|
||||
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
|
||||
const aspect =
|
||||
image.aspectRatio && image.aspectRatio.height > 0
|
||||
? image.aspectRatio.width / image.aspectRatio.height
|
||||
: undefined
|
||||
|
||||
// The tap handler and the peek-commit handler do the same thing: open the
|
||||
// lightbox with this cell's ref + dims so the lightbox's return animation
|
||||
// can target the original thumbnail.
|
||||
const openLightboxAtIndex = onPress
|
||||
? () => onPress(index, containerRefs, thumbDimsRef.current.slice())
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<View style={a.flex_1} ref={containerRefs[index]} collapsable={false}>
|
||||
<Pressable
|
||||
onPress={
|
||||
onPress
|
||||
? () => 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="">
|
||||
<Image
|
||||
source={{uri: image.thumb}}
|
||||
style={[a.flex_1]}
|
||||
accessible={true}
|
||||
accessibilityLabel={image.alt}
|
||||
accessibilityHint=""
|
||||
accessibilityIgnoresInvertColors
|
||||
onLoad={e => {
|
||||
thumbDimsRef.current[index] = {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
}
|
||||
<ImageContextMenu
|
||||
fullsizeUri={image.fullsize}
|
||||
thumbUri={image.thumb}
|
||||
aspectRatio={aspect}
|
||||
borderRadius={tokens.borderRadius.md}
|
||||
onPreviewPress={openLightboxAtIndex}
|
||||
style={a.flex_1}>
|
||||
<Pressable
|
||||
onPress={openLightboxAtIndex}
|
||||
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
||||
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
|
||||
android_ripple={{
|
||||
color: utils.alpha(t.atoms.bg.backgroundColor, 0.2),
|
||||
foreground: true,
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<MediaInsetBorder style={insetBorderStyle} />
|
||||
</Pressable>
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
imageStyle,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={image.alt || _(msg`Image`)}
|
||||
accessibilityHint="">
|
||||
<Image
|
||||
source={{uri: image.thumb}}
|
||||
style={[a.flex_1]}
|
||||
accessible={true}
|
||||
accessibilityLabel={image.alt}
|
||||
accessibilityHint=""
|
||||
accessibilityIgnoresInvertColors
|
||||
onLoad={e => {
|
||||
thumbDimsRef.current[index] = {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
}
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<MediaInsetBorder style={insetBorderStyle} />
|
||||
</Pressable>
|
||||
</ImageContextMenu>
|
||||
{hasAlt && !hideBadges ? (
|
||||
<View
|
||||
accessible={false}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
ChatBskyGroupRequestJoin,
|
||||
ChatBskyGroupWithdrawJoinRequest,
|
||||
moderateProfile,
|
||||
} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links'
|
||||
import {useRequestJoinGroupChat} from '#/state/queries/messages/request-join-group-chat'
|
||||
import {useWithdrawJoinGroupChatRequest} from '#/state/queries/messages/withdraw-join-group-chat'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||
import {
|
||||
Button,
|
||||
type ButtonColor,
|
||||
ButtonIcon,
|
||||
ButtonText,
|
||||
} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
||||
import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight'
|
||||
import {ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon} from '#/components/icons/ChainLink'
|
||||
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
|
||||
import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ProfileBadges} from '../ProfileBadges'
|
||||
|
||||
export function GroupChatJoinDialog() {
|
||||
const {groupChatJoinDialogControl, groupChatJoinState} = useIntentDialogs()
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={groupChatJoinDialogControl}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<GroupChatJoinDialogInner code={groupChatJoinState?.code} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupChatJoinDialogInner({code}: {code?: string}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={l`Join group chat`}
|
||||
style={[web({maxWidth: 400, borderRadius: 36})]}>
|
||||
<View style={[a.gap_2xl, a.align_center]}>
|
||||
<GroupChatJoinDialogContent code={code} />
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
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<NavigationProp>()
|
||||
|
||||
const {data, error, isLoading} = useJoinLinkPreviewsQuery({
|
||||
codes: code ? [code] : undefined,
|
||||
hasSession,
|
||||
})
|
||||
|
||||
const {mutate: joinGroupChat, isPending: isJoinPending} =
|
||||
useRequestJoinGroupChat({
|
||||
onSuccess: data => {
|
||||
switch (data.status) {
|
||||
case 'pending':
|
||||
control.close(() => {
|
||||
Toast.show(
|
||||
l`Access requested! The group owner will review your request.`,
|
||||
)
|
||||
})
|
||||
break
|
||||
case 'joined': {
|
||||
if (data.convo && data.convo.id) {
|
||||
control.close(() => {
|
||||
Toast.show(l`Successfully joined the group chat!`)
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: data.convo!.id,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
logger.warn('Request to join group chat returned no convo ID', {
|
||||
status: data.status,
|
||||
convoId: data.convo?.id,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: error => {
|
||||
let errorMessage = l`Failed to join the group chat. Please try again.`
|
||||
if (isNetworkError(error)) {
|
||||
errorMessage = l`There was a problem with your internet connection, please try again`
|
||||
} else if (error instanceof ChatBskyGroupRequestJoin.ConvoLockedError) {
|
||||
errorMessage = l`This conversation is locked.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRequestJoin.FollowRequiredError
|
||||
) {
|
||||
errorMessage = l`Only followers can join this group chat.`
|
||||
} else if (error instanceof ChatBskyGroupRequestJoin.InvalidCodeError) {
|
||||
errorMessage = l`Invalid group chat code.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRequestJoin.LinkDisabledError
|
||||
) {
|
||||
errorMessage = l`This invite link has been disabled.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRequestJoin.MemberLimitReachedError
|
||||
) {
|
||||
errorMessage = l`The member limit has been reached.`
|
||||
} else if (error instanceof ChatBskyGroupRequestJoin.UserKickedError) {
|
||||
errorMessage = l`You have been removed from this group.`
|
||||
}
|
||||
Toast.show(errorMessage)
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: withdrawRequest, isPending: isWithdrawPending} =
|
||||
useWithdrawJoinGroupChatRequest({
|
||||
onSuccess: () => {
|
||||
control.close(() => {
|
||||
Toast.show(l`Join request rescinded.`)
|
||||
})
|
||||
},
|
||||
onError: error => {
|
||||
let errorMessage = l`Failed to rescind your request. Please try again.`
|
||||
if (isNetworkError(error)) {
|
||||
errorMessage = l`There was a problem with your internet connection, please try again`
|
||||
} else if (
|
||||
error instanceof
|
||||
ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError
|
||||
) {
|
||||
errorMessage = l`Invalid rescind request.`
|
||||
}
|
||||
Toast.show(errorMessage)
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
state: interacted,
|
||||
onIn: onInteract,
|
||||
onOut: onInteractOut,
|
||||
} = useInteractionState()
|
||||
|
||||
const handleJoin = () => {
|
||||
if (!code) return
|
||||
joinGroupChat({code})
|
||||
}
|
||||
|
||||
const handleWithdraw = () => {
|
||||
if (!convoId) return
|
||||
withdrawRequest({convoId})
|
||||
}
|
||||
|
||||
// Fallback if the prefetch exceeds the timeout
|
||||
if (isLoading || !data || !moderationOpts) {
|
||||
return (
|
||||
<View style={[a.p_2xl]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<ChainLinkBrokenIcon fill={t.palette.primary_500} size="3xl" />
|
||||
<Text
|
||||
style={[a.text_center, a.text_lg, a.font_semi_bold, t.atoms.text]}>
|
||||
<Trans>This invite link is invalid</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={l`Close this dialog`}
|
||||
accessibilityHint={l`Close this dialog`}
|
||||
onPress={() => control.close()}
|
||||
color="primary"
|
||||
size="large"
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const joinLinkPreview = data.joinLinkPreviews[0]
|
||||
|
||||
if (!joinLinkPreview) {
|
||||
return (
|
||||
<>
|
||||
<View style={[a.py_lg, a.align_center]}>
|
||||
<View style={[a.gap_sm, a.align_center, a.mt_lg]}>
|
||||
<WarningIcon size="3xl" fill={t.atoms.text_contrast_high.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Chat invite link no longer available</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
testID="joinButton"
|
||||
onPress={() => control.close()}
|
||||
label={l`Close this dialog`}
|
||||
accessibilityHint={l`Close this dialog`}
|
||||
size="large"
|
||||
color="secondary"
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>{l`Close`}</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<View style={[a.py_lg, a.align_center]}>
|
||||
<AvatarBubbles
|
||||
profiles={[
|
||||
joinLinkPreview.owner,
|
||||
...Array(joinLinkPreview.memberCount - 1).fill(undefined),
|
||||
]}
|
||||
self
|
||||
size={135}
|
||||
/>
|
||||
<View style={[a.gap_sm, a.align_center, a.mt_lg]}>
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Group chat</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_center, a.text_3xl, a.font_bold, t.atoms.text]}>
|
||||
{joinLinkPreview.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Text
|
||||
style={[a.text_center, a.text_xs, a.leading_snug, t.atoms.text]}>
|
||||
<Trans comment="The number of active group chat members out of the total number allowed.">
|
||||
{joinLinkPreview.memberCount}/{joinLinkPreview.memberLimit}{' '}
|
||||
members
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.ml_md]}>
|
||||
<PersonGroupIcon
|
||||
size="xs"
|
||||
style={[a.mr_xs, t.atoms.text, {marginTop: -2}]}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[a.text_center, a.text_xs, a.leading_snug, t.atoms.text]}>
|
||||
{joinLinkPreview.joinRule === 'followedByOwner'
|
||||
? l`Followers can join`
|
||||
: l`Anyone can join`}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<View
|
||||
style={[a.flex_row, a.gap_xs, a.align_center, a.justify_center]}>
|
||||
<Text
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text,
|
||||
]}>
|
||||
By{' '}
|
||||
<InlineLinkText
|
||||
label={`@${joinLinkPreview.owner.handle}`}
|
||||
to={makeProfileLink(joinLinkPreview.owner)}
|
||||
style={[
|
||||
a.mb_2xs,
|
||||
a.text_sm,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text,
|
||||
interacted && {
|
||||
...web({
|
||||
outline: 0,
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor: t.palette.contrast_1000,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
{...web({
|
||||
onMouseEnter: () => {
|
||||
onInteract()
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
onInteractOut()
|
||||
},
|
||||
})}>
|
||||
{createSanitizedDisplayName(
|
||||
joinLinkPreview.owner,
|
||||
true,
|
||||
moderateProfile(joinLinkPreview.owner, moderationOpts).ui(
|
||||
'displayName',
|
||||
),
|
||||
)}
|
||||
</InlineLinkText>
|
||||
</Text>
|
||||
<ProfileBadges
|
||||
profile={data.joinLinkPreviews[0].owner}
|
||||
size="sm"
|
||||
style={{marginTop: -3}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
{sanitizeHandle(joinLinkPreview.owner.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{convoId ? (
|
||||
<Button
|
||||
testID="openButton"
|
||||
onPress={() => {
|
||||
control.close(() => {
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: convoId,
|
||||
})
|
||||
})
|
||||
}}
|
||||
label={l`Open group chat`}
|
||||
accessibilityHint={l`Open this group chat`}
|
||||
size="large"
|
||||
color="primary"
|
||||
disabled={!code}
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>Open chat</ButtonText>
|
||||
<ButtonIcon icon={ArrowRightIcon} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
testID="joinButton"
|
||||
onPress={hasRequested ? handleWithdraw : handleJoin}
|
||||
label={
|
||||
joinLinkPreview.requireApproval
|
||||
? l`Request access to group chat`
|
||||
: l`Join group chat`
|
||||
}
|
||||
accessibilityHint={
|
||||
joinLinkPreview.requireApproval
|
||||
? l`Request access to join this group chat`
|
||||
: l`Join this group chat`
|
||||
}
|
||||
size="large"
|
||||
color={buttonColor}
|
||||
disabled={isJoinPending || isWithdrawPending || !code || !canJoin}
|
||||
style={[a.w_full]}>
|
||||
<ButtonIcon icon={ButtonIconImage} />
|
||||
<ButtonText>{buttonText}</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Context.Provider value={value}>
|
||||
{children}
|
||||
<VerifyEmailIntentDialog />
|
||||
<GroupChatJoinDialog />
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Dialog.Outer control={props.control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
|
||||
@@ -88,4 +88,8 @@ export type ReportDialogProps = {
|
||||
* Called if the report was successfully submitted.
|
||||
*/
|
||||
onAfterSubmit?: () => void
|
||||
/**
|
||||
* Called after the dialog finishes closing.
|
||||
*/
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
+2
-21
@@ -51,16 +51,10 @@ interface PostOpts {
|
||||
langs?: string[]
|
||||
}
|
||||
|
||||
type FeatureFlags = {
|
||||
highResolutionImages?: boolean
|
||||
increasedBlobSizeLimit?: boolean
|
||||
}
|
||||
|
||||
export async function post(
|
||||
agent: BskyAgent,
|
||||
queryClient: QueryClient,
|
||||
opts: PostOpts,
|
||||
featureFlags?: FeatureFlags,
|
||||
) {
|
||||
const thread = opts.thread
|
||||
opts.onStateChange?.(t`Processing...`)
|
||||
@@ -97,7 +91,6 @@ export async function post(
|
||||
queryClient,
|
||||
draft,
|
||||
opts.onStateChange,
|
||||
featureFlags,
|
||||
)
|
||||
let labels: $Typed<ComAtprotoLabelDefs.SelfLabels> | undefined
|
||||
if (draft.labels.length) {
|
||||
@@ -257,7 +250,6 @@ async function resolveEmbed(
|
||||
queryClient: QueryClient,
|
||||
draft: PostDraft,
|
||||
onStateChange: ((state: string) => void) | undefined,
|
||||
featureFlags?: FeatureFlags,
|
||||
): Promise<
|
||||
| $Typed<AppBskyEmbedImages.Main>
|
||||
| $Typed<AppBskyEmbedVideo.Main>
|
||||
@@ -268,13 +260,7 @@ async function resolveEmbed(
|
||||
> {
|
||||
if (draft.embed.quote) {
|
||||
const [resolvedMedia, resolvedQuote] = await Promise.all([
|
||||
resolveMedia(
|
||||
agent,
|
||||
queryClient,
|
||||
draft.embed,
|
||||
onStateChange,
|
||||
featureFlags,
|
||||
),
|
||||
resolveMedia(agent, queryClient, draft.embed, onStateChange),
|
||||
resolveRecord(agent, queryClient, draft.embed.quote.uri),
|
||||
])
|
||||
if (resolvedMedia) {
|
||||
@@ -297,7 +283,6 @@ async function resolveEmbed(
|
||||
queryClient,
|
||||
draft.embed,
|
||||
onStateChange,
|
||||
featureFlags,
|
||||
)
|
||||
if (resolvedMedia) {
|
||||
return resolvedMedia
|
||||
@@ -323,7 +308,6 @@ async function resolveMedia(
|
||||
queryClient: QueryClient,
|
||||
embedDraft: EmbedDraft,
|
||||
onStateChange: ((state: string) => void) | undefined,
|
||||
featureFlags?: FeatureFlags,
|
||||
): Promise<
|
||||
| $Typed<AppBskyEmbedExternal.Main>
|
||||
| $Typed<AppBskyEmbedImages.Main>
|
||||
@@ -339,10 +323,7 @@ async function resolveMedia(
|
||||
const images: AppBskyEmbedImages.Image[] = await Promise.all(
|
||||
imagesDraft.map(async (image, i) => {
|
||||
logger.debug(`Compressing image #${i}`)
|
||||
const {path, width, height, mime} = await compressImage(image, {
|
||||
highResolution: featureFlags?.highResolutionImages,
|
||||
increasedBlobSizeLimit: featureFlags?.increasedBlobSizeLimit,
|
||||
})
|
||||
const {path, width, height, mime} = await compressImage(image)
|
||||
logger.debug(`Uploading image #${i}`)
|
||||
const res = await uploadBlob(agent, path, mime)
|
||||
return {
|
||||
|
||||
@@ -246,6 +246,10 @@ async function resolveExternal(
|
||||
title: result.title ?? '',
|
||||
description: result.description ?? '',
|
||||
thumb: result.image ? await imageToThumb(result.image) : undefined,
|
||||
/*
|
||||
* New fields from Standard Site integration. Other fields are derived from
|
||||
* opengraph/oembed as before.
|
||||
*/
|
||||
associatedRefs: result.associatedRefs,
|
||||
view: result.view,
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -188,8 +189,8 @@ export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds
|
||||
* Maximum size of a video in megabytes, _not_ mebibytes. Backend uses
|
||||
* ISO megabytes.
|
||||
*/
|
||||
export const VIDEO_MAX_SIZE_REDUCED = 1000 * 1000 * 100 // 100mb
|
||||
export const VIDEO_MAX_SIZE = 3000 * 1000 * 100 // 300mb
|
||||
export const VIDEO_MAX_SIZE_MB = 300
|
||||
export const VIDEO_MAX_SIZE = VIDEO_MAX_SIZE_MB * 1000 * 1000 // 300mb
|
||||
|
||||
export const SUPPORTED_MIME_TYPES = [
|
||||
'video/mp4',
|
||||
|
||||
@@ -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} =
|
||||
|
||||
@@ -12,7 +12,6 @@ export async function compressVideo(
|
||||
opts?: {
|
||||
signal?: AbortSignal
|
||||
onProgress?: (progress: number) => void
|
||||
TEMP_enableLargeVideoUploads?: boolean
|
||||
},
|
||||
): Promise<CompressedVideo> {
|
||||
const {onProgress, signal} = opts || {}
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||
|
||||
import {VIDEO_MAX_SIZE, VIDEO_MAX_SIZE_REDUCED} from '#/lib/constants'
|
||||
import {VIDEO_MAX_SIZE} from '#/lib/constants'
|
||||
import {VideoTooLargeError} from '#/lib/media/video/errors'
|
||||
import {type CompressedVideo} from './types'
|
||||
|
||||
// doesn't actually compress, converts to ArrayBuffer
|
||||
export async function compressVideo(
|
||||
asset: ImagePickerAsset,
|
||||
opts?: {
|
||||
_opts?: {
|
||||
signal?: AbortSignal
|
||||
onProgress?: (progress: number) => void
|
||||
TEMP_enableLargeVideoUploads?: number
|
||||
},
|
||||
): Promise<CompressedVideo> {
|
||||
const {mimeType, base64} = parseDataUrl(asset.uri)
|
||||
const blob = base64ToBlob(base64, mimeType)
|
||||
const uri = URL.createObjectURL(blob)
|
||||
|
||||
if (
|
||||
blob.size >
|
||||
(opts?.TEMP_enableLargeVideoUploads
|
||||
? VIDEO_MAX_SIZE
|
||||
: VIDEO_MAX_SIZE_REDUCED)
|
||||
) {
|
||||
if (blob.size > VIDEO_MAX_SIZE) {
|
||||
throw new VideoTooLargeError()
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -178,6 +178,28 @@ export function isBskyStarterPackUrl(url: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
export const CHAT_INVITE_CODE_REGEX = /^\/c\/([a-zA-Z0-9]{7,10})$/
|
||||
|
||||
export function getChatInviteCodeFromUrl(url: string): string | undefined {
|
||||
let pathname: string
|
||||
if (isBskyAppUrl(url)) {
|
||||
try {
|
||||
pathname = new URL(url).pathname
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
} else if (url.startsWith('/')) {
|
||||
pathname = url.split('?')[0].split('#')[0]
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
return pathname.match(CHAT_INVITE_CODE_REGEX)?.[1]
|
||||
}
|
||||
|
||||
export function isBskyChatInviteUrl(url: string): boolean {
|
||||
return getChatInviteCodeFromUrl(url) !== undefined
|
||||
}
|
||||
|
||||
export function isBskyDownloadUrl(url: string): boolean {
|
||||
if (isExternalUrl(url)) {
|
||||
return false
|
||||
|
||||
+207
-206
@@ -1,16 +1,16 @@
|
||||
// Don't remove -force from these because detection is VERY slow on low-end Android.
|
||||
// https://github.com/formatjs/formatjs/issues/4463#issuecomment-2176070577
|
||||
import '@formatjs/intl-locale/polyfill-force'
|
||||
import '@formatjs/intl-pluralrules/polyfill-force'
|
||||
import '@formatjs/intl-numberformat/polyfill-force'
|
||||
import '@formatjs/intl-displaynames/polyfill-force'
|
||||
import '@formatjs/intl-pluralrules/locale-data/en'
|
||||
import '@formatjs/intl-numberformat/locale-data/en'
|
||||
import '@formatjs/intl-displaynames/locale-data/en'
|
||||
import '@formatjs/intl-locale/polyfill-force.js'
|
||||
import '@formatjs/intl-pluralrules/polyfill-force.js'
|
||||
import '@formatjs/intl-numberformat/polyfill-force.js'
|
||||
import '@formatjs/intl-displaynames/polyfill-force.js'
|
||||
import '@formatjs/intl-pluralrules/locale-data/en.js'
|
||||
import '@formatjs/intl-numberformat/locale-data/en.js'
|
||||
import '@formatjs/intl-displaynames/locale-data/en.js'
|
||||
|
||||
import {useEffect, useState} from 'react'
|
||||
import {i18n} from '@lingui/core'
|
||||
import defaultLocale from 'date-fns/locale/en-US'
|
||||
import {enUS as defaultLocale} from 'date-fns/locale/en-US'
|
||||
|
||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {AppLanguage} from '#/locale/languages'
|
||||
@@ -64,399 +64,398 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
switch (locale) {
|
||||
case AppLanguage.an: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAn})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/es'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/an'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es'),
|
||||
import('@formatjs/intl-displaynames/locale-data/es'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/es').then(m => m.es),
|
||||
import('@formatjs/intl-pluralrules/locale-data/an.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/an.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/an.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ast: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAst})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/es'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ast'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ast'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ast'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/es').then(m => m.es),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ast.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ast.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ast.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ca: {
|
||||
i18n.loadAndActivate({locale, messages: messagesCa})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/ca'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ca'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ca'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ca'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/ca').then(m => m.ca),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ca.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ca.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ca.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.cy: {
|
||||
i18n.loadAndActivate({locale, messages: messagesCy})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/cy'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/cy'),
|
||||
import('@formatjs/intl-numberformat/locale-data/cy'),
|
||||
import('@formatjs/intl-displaynames/locale-data/cy'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/cy').then(m => m.cy),
|
||||
import('@formatjs/intl-pluralrules/locale-data/cy.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/cy.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/cy.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDa})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/da'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/da'),
|
||||
import('@formatjs/intl-numberformat/locale-data/da'),
|
||||
import('@formatjs/intl-displaynames/locale-data/da'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/da').then(m => m.da),
|
||||
import('@formatjs/intl-pluralrules/locale-data/da.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/da.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/da.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.de: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/de'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/de'),
|
||||
import('@formatjs/intl-numberformat/locale-data/de'),
|
||||
import('@formatjs/intl-displaynames/locale-data/de'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/de').then(m => m.de),
|
||||
import('@formatjs/intl-pluralrules/locale-data/de.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/de.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/de.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.el: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEl})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/el'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/el'),
|
||||
import('@formatjs/intl-numberformat/locale-data/el'),
|
||||
import('@formatjs/intl-displaynames/locale-data/el'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/el').then(m => m.el),
|
||||
import('@formatjs/intl-pluralrules/locale-data/el.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/el.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/el.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.en_GB: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEn_GB})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/en-GB'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/en'),
|
||||
import('@formatjs/intl-numberformat/locale-data/en-GB'),
|
||||
import('@formatjs/intl-displaynames/locale-data/en-GB'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/en-GB').then(m => m.enGB),
|
||||
import('@formatjs/intl-pluralrules/locale-data/en.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/en-GB.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/en-GB.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.eo: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEo})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/eo'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/eo'),
|
||||
import('@formatjs/intl-numberformat/locale-data/eo'),
|
||||
// borked, see https://github.com/bluesky-social/social-app/pull/9574
|
||||
// import('@formatjs/intl-displaynames/locale-data/eo'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/eo').then(m => m.eo),
|
||||
import('@formatjs/intl-pluralrules/locale-data/eo.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/eo.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/eo.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.es: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEs})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/es'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/es'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es'),
|
||||
import('@formatjs/intl-displaynames/locale-data/es'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/es').then(m => m.es),
|
||||
import('@formatjs/intl-pluralrules/locale-data/es.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/es.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.eu: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEu})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/eu'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/eu'),
|
||||
import('@formatjs/intl-numberformat/locale-data/eu'),
|
||||
import('@formatjs/intl-displaynames/locale-data/eu'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/eu').then(m => m.eu),
|
||||
import('@formatjs/intl-pluralrules/locale-data/eu.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/eu.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/eu.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.fi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFi})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/fi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fi'),
|
||||
import('@formatjs/intl-displaynames/locale-data/fi'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/fi').then(m => m.fi),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fi.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fi.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/fi.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.fr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFr})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/fr'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fr'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fr'),
|
||||
import('@formatjs/intl-displaynames/locale-data/fr'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/fr').then(m => m.fr),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fr.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fr.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/fr.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.fy: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFy})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/fy'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fy'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fy'),
|
||||
import('@formatjs/intl-displaynames/locale-data/fy'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/fy').then(m => m.fy),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fy.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fy.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/fy.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ga: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/ga'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ga'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ga'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ga.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ga.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ga.js'),
|
||||
])
|
||||
return undefined
|
||||
}
|
||||
case AppLanguage.gd: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGd})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/gd'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/gd'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gd'),
|
||||
import('@formatjs/intl-displaynames/locale-data/gd'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/gd').then(m => m.gd),
|
||||
import('@formatjs/intl-pluralrules/locale-data/gd.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gd.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/gd.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.gl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGl})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/gl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/gl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gl'),
|
||||
import('@formatjs/intl-displaynames/locale-data/gl'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/gl').then(m => m.gl),
|
||||
import('@formatjs/intl-pluralrules/locale-data/gl.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gl.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/gl.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.hi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHi})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/hi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hi'),
|
||||
import('@formatjs/intl-displaynames/locale-data/hi'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/hi').then(m => m.hi),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hi.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hi.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/hi.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.hu: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHu})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/hu'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hu'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hu'),
|
||||
import('@formatjs/intl-displaynames/locale-data/hu'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/hu').then(m => m.hu),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hu.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hu.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/hu.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ia: {
|
||||
i18n.loadAndActivate({locale, messages: messagesIa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/ia'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ia'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ia'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ia.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ia.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ia.js'),
|
||||
])
|
||||
return undefined
|
||||
}
|
||||
case AppLanguage.id: {
|
||||
i18n.loadAndActivate({locale, messages: messagesId})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/id'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/id'),
|
||||
import('@formatjs/intl-numberformat/locale-data/id'),
|
||||
import('@formatjs/intl-displaynames/locale-data/id'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/id').then(m => m.id),
|
||||
import('@formatjs/intl-pluralrules/locale-data/id.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/id.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/id.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.it: {
|
||||
i18n.loadAndActivate({locale, messages: messagesIt})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/it'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/it'),
|
||||
import('@formatjs/intl-numberformat/locale-data/it'),
|
||||
import('@formatjs/intl-displaynames/locale-data/it'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/it').then(m => m.it),
|
||||
import('@formatjs/intl-pluralrules/locale-data/it.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/it.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/it.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ja: {
|
||||
i18n.loadAndActivate({locale, messages: messagesJa})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/ja'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ja'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ja'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ja'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/ja').then(m => m.ja),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ja.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ja.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ja.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.km: {
|
||||
i18n.loadAndActivate({locale, messages: messagesKm})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/km'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/km'),
|
||||
import('@formatjs/intl-numberformat/locale-data/km'),
|
||||
import('@formatjs/intl-displaynames/locale-data/km'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/km').then(m => m.km),
|
||||
import('@formatjs/intl-pluralrules/locale-data/km.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/km.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/km.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ko: {
|
||||
i18n.loadAndActivate({locale, messages: messagesKo})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/ko'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ko'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ko'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ko'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/ko').then(m => m.ko),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ko.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ko.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ko.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ne: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNe})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/ne'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ne'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ne'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ne.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ne.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ne.js'),
|
||||
])
|
||||
return undefined
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNl})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/nl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/nl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/nl'),
|
||||
import('@formatjs/intl-displaynames/locale-data/nl'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/nl').then(m => m.nl),
|
||||
import('@formatjs/intl-pluralrules/locale-data/nl.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/nl.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/nl.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.pl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPl})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/pl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pl'),
|
||||
import('@formatjs/intl-displaynames/locale-data/pl'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/pl').then(m => m.pl),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pl.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pl.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/pl.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.pt_BR: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPt_BR})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/pt-BR'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pt'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pt'),
|
||||
import('@formatjs/intl-displaynames/locale-data/pt'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/pt-BR').then(m => m.ptBR),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pt.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pt.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/pt.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.pt_PT: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPt_PT})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/pt'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pt-PT'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pt-PT'),
|
||||
import('@formatjs/intl-displaynames/locale-data/pt-PT'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/pt').then(m => m.pt),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pt-PT.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pt-PT.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/pt-PT.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ro: {
|
||||
i18n.loadAndActivate({locale, messages: messagesRo})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/ro'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ro'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ro'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ro'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/ro').then(m => m.ro),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ro.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ro.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ro.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.ru: {
|
||||
i18n.loadAndActivate({locale, messages: messagesRu})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/ru'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ru'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ru'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ru'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/ru').then(m => m.ru),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ru.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ru.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/ru.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.sv: {
|
||||
i18n.loadAndActivate({locale, messages: messagesSv})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/sv'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/sv'),
|
||||
import('@formatjs/intl-numberformat/locale-data/sv'),
|
||||
import('@formatjs/intl-displaynames/locale-data/sv'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/sv').then(m => m.sv),
|
||||
import('@formatjs/intl-pluralrules/locale-data/sv.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/sv.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/sv.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.th: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTh})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/th'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/th'),
|
||||
import('@formatjs/intl-numberformat/locale-data/th'),
|
||||
import('@formatjs/intl-displaynames/locale-data/th'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/th').then(m => m.th),
|
||||
import('@formatjs/intl-pluralrules/locale-data/th.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/th.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/th.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.tr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTr})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/tr'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/tr'),
|
||||
import('@formatjs/intl-numberformat/locale-data/tr'),
|
||||
import('@formatjs/intl-displaynames/locale-data/tr'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/tr').then(m => m.tr),
|
||||
import('@formatjs/intl-pluralrules/locale-data/tr.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/tr.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/tr.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.uk: {
|
||||
i18n.loadAndActivate({locale, messages: messagesUk})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/uk'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/uk'),
|
||||
import('@formatjs/intl-numberformat/locale-data/uk'),
|
||||
import('@formatjs/intl-displaynames/locale-data/uk'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/uk').then(m => m.uk),
|
||||
import('@formatjs/intl-pluralrules/locale-data/uk.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/uk.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/uk.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.vi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesVi})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/vi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/vi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/vi'),
|
||||
import('@formatjs/intl-displaynames/locale-data/vi'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/vi').then(m => m.vi),
|
||||
import('@formatjs/intl-pluralrules/locale-data/vi.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/vi.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/vi.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.zh_CN: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_CN})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/zh-CN'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
import('@formatjs/intl-displaynames/locale-data/zh'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/zh-CN').then(m => m.zhCN),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/zh.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.zh_HK: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_HK})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/zh-HK'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
import('@formatjs/intl-displaynames/locale-data/zh'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/zh-HK').then(m => m.zhHK),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/yue-Hant.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/yue-Hant.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
case AppLanguage.zh_TW: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_TW})
|
||||
const [{default: dateLocale}] = await Promise.all([
|
||||
import('date-fns/locale/zh-TW'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
import('@formatjs/intl-displaynames/locale-data/zh'),
|
||||
const [dateLocale] = await Promise.all([
|
||||
import('date-fns/locale/zh-TW').then(m => m.zhTW),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh.js'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh-Hant.js'),
|
||||
import('@formatjs/intl-displaynames/locale-data/zh-Hant.js'),
|
||||
])
|
||||
return dateLocale
|
||||
}
|
||||
@@ -472,9 +471,11 @@ export function useLocaleLanguage() {
|
||||
const [dateLocale, setDateLocale] = useState(defaultLocale)
|
||||
|
||||
useEffect(() => {
|
||||
dynamicActivate(sanitizeAppLanguageSetting(appLanguage)).then(locale => {
|
||||
setDateLocale(locale ?? defaultLocale)
|
||||
})
|
||||
void dynamicActivate(sanitizeAppLanguageSetting(appLanguage)).then(
|
||||
locale => {
|
||||
setDateLocale(locale ?? defaultLocale)
|
||||
},
|
||||
)
|
||||
}, [appLanguage])
|
||||
|
||||
return dateLocale
|
||||
|
||||
+121
-120
@@ -1,6 +1,7 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {i18n} from '@lingui/core'
|
||||
import defaultLocale from 'date-fns/locale/en-US'
|
||||
import {i18n, type Messages} from '@lingui/core'
|
||||
import {type Locale} from 'date-fns/locale'
|
||||
import {enUS as defaultLocale} from 'date-fns/locale/en-US'
|
||||
|
||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {AppLanguage} from '#/locale/languages'
|
||||
@@ -10,288 +11,288 @@ import {useLanguagePrefs} from '#/state/preferences'
|
||||
* We do a dynamic import of just the catalog that we need
|
||||
*/
|
||||
export async function dynamicActivate(locale: AppLanguage) {
|
||||
let mod: any
|
||||
let messages: Messages
|
||||
let dateLocale: Locale = defaultLocale
|
||||
|
||||
switch (locale) {
|
||||
case AppLanguage.an: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/an/messages`),
|
||||
import('date-fns/locale/es'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/an/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/es').then(m => m.es),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ast: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/ast/messages`),
|
||||
import('date-fns/locale/es'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/ast/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/es').then(m => m.es),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ca: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/ca/messages`),
|
||||
import('date-fns/locale/ca'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/ca/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/ca').then(m => m.ca),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.cy: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/cy/messages`),
|
||||
import('date-fns/locale/cy'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/cy/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/cy').then(m => m.cy),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/da/messages`),
|
||||
import('date-fns/locale/da'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/da/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/da').then(m => m.da),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.de: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/de/messages`),
|
||||
import('date-fns/locale/de'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/de/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/de').then(m => m.de),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.el: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/el/messages`),
|
||||
import('date-fns/locale/el'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/el/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/el').then(m => m.el),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.en_GB: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/en-GB/messages`),
|
||||
import('date-fns/locale/en-GB'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/en-GB/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/en-GB').then(m => m.enGB),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.eo: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/eo/messages`),
|
||||
import('date-fns/locale/eo'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/eo/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/eo').then(m => m.eo),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.es: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/es/messages`),
|
||||
import('date-fns/locale/es'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/es/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/es').then(m => m.es),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.eu: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/eu/messages`),
|
||||
import('date-fns/locale/eu'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/eu/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/eu').then(m => m.eu),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.fi: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/fi/messages`),
|
||||
import('date-fns/locale/fi'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/fi/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/fi').then(m => m.fi),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.fr: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/fr/messages`),
|
||||
import('date-fns/locale/fr'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/fr/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/fr').then(m => m.fr),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.fy: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/fy/messages`),
|
||||
import('date-fns/locale/fy'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/fy/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/fy').then(m => m.fy),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ga: {
|
||||
mod = await import(`./locales/ga/messages`)
|
||||
messages = await import(`./locales/ga/messages`).then(m => m.messages)
|
||||
break
|
||||
}
|
||||
case AppLanguage.gd: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/gd/messages`),
|
||||
import('date-fns/locale/gd'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/gd/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/gd').then(m => m.gd),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.gl: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/gl/messages`),
|
||||
import('date-fns/locale/gl'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/gl/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/gl').then(m => m.gl),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.hi: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/hi/messages`),
|
||||
import('date-fns/locale/hi'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/hi/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/hi').then(m => m.hi),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.hu: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/hu/messages`),
|
||||
import('date-fns/locale/hu'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/hu/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/hu').then(m => m.hu),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ia: {
|
||||
mod = await import(`./locales/ia/messages`)
|
||||
messages = await import(`./locales/ia/messages`).then(m => m.messages)
|
||||
break
|
||||
}
|
||||
case AppLanguage.id: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/id/messages`),
|
||||
import('date-fns/locale/id'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/id/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/id').then(m => m.id),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.it: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/it/messages`),
|
||||
import('date-fns/locale/it'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/it/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/it').then(m => m.it),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ja: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/ja/messages`),
|
||||
import('date-fns/locale/ja'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/ja/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/ja').then(m => m.ja),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.km: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/km/messages`),
|
||||
import('date-fns/locale/km'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/km/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/km').then(m => m.km),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ko: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/ko/messages`),
|
||||
import('date-fns/locale/ko'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/ko/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/ko').then(m => m.ko),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ne: {
|
||||
mod = await import(`./locales/ne/messages`)
|
||||
messages = await import(`./locales/ne/messages`).then(m => m.messages)
|
||||
break
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/nl/messages`),
|
||||
import('date-fns/locale/nl'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/nl/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/nl').then(m => m.nl),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.pl: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/pl/messages`),
|
||||
import('date-fns/locale/pl'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/pl/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/pl').then(m => m.pl),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.pt_BR: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/pt-BR/messages`),
|
||||
import('date-fns/locale/pt-BR'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/pt-BR/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/pt-BR').then(m => m.ptBR),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.pt_PT: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/pt-PT/messages`),
|
||||
import('date-fns/locale/pt'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/pt-PT/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/pt').then(m => m.pt),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ro: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/ro/messages`),
|
||||
import('date-fns/locale/ro'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/ro/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/ro').then(m => m.ro),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ru: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/ru/messages`),
|
||||
import('date-fns/locale/ru'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/ru/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/ru').then(m => m.ru),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.sv: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/sv/messages`),
|
||||
import('date-fns/locale/sv'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/sv/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/sv').then(m => m.sv),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.th: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/th/messages`),
|
||||
import('date-fns/locale/th'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/th/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/th').then(m => m.th),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.tr: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/tr/messages`),
|
||||
import('date-fns/locale/tr'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/tr/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/tr').then(m => m.tr),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.uk: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/uk/messages`),
|
||||
import('date-fns/locale/uk'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/uk/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/uk').then(m => m.uk),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.vi: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/vi/messages`),
|
||||
import('date-fns/locale/vi'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/vi/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/vi').then(m => m.vi),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_CN: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/zh-CN/messages`),
|
||||
import('date-fns/locale/zh-CN'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/zh-CN/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/zh-CN').then(m => m.zhCN),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_HK: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/zh-HK/messages`),
|
||||
import('date-fns/locale/zh-HK'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/zh-HK/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/zh-HK').then(m => m.zhHK),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_TW: {
|
||||
;[mod, {default: dateLocale}] = await Promise.all([
|
||||
import(`./locales/zh-TW/messages`),
|
||||
import('date-fns/locale/zh-TW'),
|
||||
;[messages, dateLocale] = await Promise.all([
|
||||
import(`./locales/zh-TW/messages`).then(m => m.messages),
|
||||
import('date-fns/locale/zh-TW').then(m => m.zhTW),
|
||||
])
|
||||
break
|
||||
}
|
||||
default: {
|
||||
mod = await import(`./locales/en/messages`)
|
||||
messages = await import(`./locales/en/messages`).then(m => m.messages)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i18n.load(locale, mod.messages)
|
||||
i18n.load(locale, messages)
|
||||
i18n.activate(locale)
|
||||
|
||||
return dateLocale
|
||||
@@ -305,7 +306,7 @@ export function useLocaleLanguage() {
|
||||
const sanitizedLanguage = sanitizeAppLanguageSetting(appLanguage)
|
||||
|
||||
document.documentElement.lang = sanitizedLanguage
|
||||
dynamicActivate(sanitizedLanguage).then(locale => {
|
||||
void dynamicActivate(sanitizedLanguage).then(locale => {
|
||||
setDateLocale(locale)
|
||||
})
|
||||
}, [appLanguage])
|
||||
|
||||
+603
-317
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import format from 'date-fns/format'
|
||||
import {format} from 'date-fns/format'
|
||||
|
||||
import {LogLevel, type Transport} from '#/logger/types'
|
||||
import {prepareMetadata} from '#/logger/util'
|
||||
|
||||
@@ -25,7 +25,8 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
thrownErrors.add(err)
|
||||
throw err
|
||||
} else if (!thrownErrors.has(msgOrError)) {
|
||||
return realConsoleError.apply(this, arguments as any)
|
||||
// @ts-expect-error
|
||||
return realConsoleError.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
||||
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',
|
||||
|
||||
@@ -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<ConvoWithDetails, {kind: 'group'}>
|
||||
requestCount: number
|
||||
hasMoreRequests: boolean
|
||||
isOwner: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const memberCount = convo.details.memberCount
|
||||
const memberLimit = convo.details.memberLimit
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.justify_between, a.px_xl, a.pt_xl, a.pb_sm]}>
|
||||
@@ -42,13 +47,16 @@ export function MembersAndRequests({
|
||||
<InlineLinkText
|
||||
label={l`View incoming group chat requests`}
|
||||
style={[a.text_sm, a.text_right, a.font_semi_bold]}
|
||||
// TODO Need to implement this. -dsb
|
||||
to="#">
|
||||
{...createStaticClick(() => {
|
||||
navigation.navigate('MessagesJoinRequests', {
|
||||
conversation: convo.view.id,
|
||||
})
|
||||
})}>
|
||||
{hasMoreRequests
|
||||
? l({
|
||||
message: `${requestCount}+ requests`,
|
||||
comment:
|
||||
'Displayed when there are more than 50 requests to join a group chat',
|
||||
'Displayed when there are more than 20 requests to join a group chat',
|
||||
})
|
||||
: l({
|
||||
message: plural(requestCount, {
|
||||
|
||||
@@ -247,8 +247,7 @@ function GroupSettings({
|
||||
case 'MEMBERS_AND_REQUESTS':
|
||||
return (
|
||||
<MembersAndRequests
|
||||
memberCount={convo.details.memberCount}
|
||||
memberLimit={convo.details.memberLimit}
|
||||
convo={convo}
|
||||
requestCount={requestCount}
|
||||
hasMoreRequests={!!hasMoreRequests}
|
||||
isOwner={isOwner}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import {View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links'
|
||||
import {useActiveGroupChatJoinRequest} from '#/state/shell/landing'
|
||||
import {LoggedOutScreenState} from '#/view/com/auth/LoggedOut'
|
||||
import {LogomarkWithType} from '#/view/icons/LogomarkWithType'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon} from '#/components/icons/ChainLink'
|
||||
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const desktopDarkBg = require('../../../assets/images/chat-desktop-bg-dark.webp')
|
||||
const desktopLightBg = require('../../../assets/images/chat-desktop-bg-light.webp')
|
||||
const mobileDarkBg = require('../../../assets/images/chat-mobile-bg-dark.webp')
|
||||
const mobileLightBg = require('../../../assets/images/chat-mobile-bg-light.webp')
|
||||
|
||||
type Props = {
|
||||
setScreenState: (state: LoggedOutScreenState) => 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 (
|
||||
<View style={[a.util_screen_outer, a.w_full, t.atoms.bg_contrast_25]}>
|
||||
<ImageBackground
|
||||
source={background}
|
||||
style={[a.util_screen_outer, a.w_full, a.flex_1, a.justify_center]}
|
||||
contentFit={gtTablet ? 'contain' : 'cover'}>
|
||||
<View
|
||||
style={[
|
||||
a.util_screen_outer,
|
||||
a.w_full,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
{error ? (
|
||||
<Wrapper>
|
||||
<ChainLinkBrokenIcon fill={t.palette.primary_500} size="3xl" />
|
||||
<Text
|
||||
style={[
|
||||
a.mb_sm,
|
||||
a.text_center,
|
||||
a.text_lg,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text,
|
||||
]}>
|
||||
{l`This invite link has expired`}
|
||||
</Text>
|
||||
<ActionButtons setScreenState={setScreenState} />
|
||||
</Wrapper>
|
||||
) : data && moderationOpts ? (
|
||||
<Wrapper>
|
||||
<AvatarBubbles
|
||||
profiles={[
|
||||
data.joinLinkPreviews[0].owner,
|
||||
...Array(
|
||||
Math.min(
|
||||
3,
|
||||
Math.max(0, data.joinLinkPreviews[0].memberCount - 1),
|
||||
),
|
||||
).fill(undefined),
|
||||
]}
|
||||
size={135}
|
||||
/>
|
||||
<View style={[a.gap_2xs]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Group chat</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<PersonGroupIcon
|
||||
size="xs"
|
||||
style={[a.mr_2xs, t.atoms.text_contrast_medium]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans comment="The number of active group chat members out of the total number allowed.">
|
||||
{data.joinLinkPreviews[0].memberCount}/
|
||||
{data.joinLinkPreviews[0].memberLimit}
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.text_4xl,
|
||||
a.leading_tight,
|
||||
a.font_bold,
|
||||
t.atoms.text,
|
||||
]}>
|
||||
{data.joinLinkPreviews[0].name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[a.w_full]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_xs,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.mb_xs,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text,
|
||||
a.max_w_full,
|
||||
]}>
|
||||
<Trans comment="The owner (creator) of a group chat.">
|
||||
By{' '}
|
||||
{createSanitizedDisplayName(
|
||||
data.joinLinkPreviews[0].owner,
|
||||
true,
|
||||
moderateProfile(
|
||||
data.joinLinkPreviews[0].owner,
|
||||
moderationOpts,
|
||||
).ui('displayName'),
|
||||
)}
|
||||
</Trans>
|
||||
</Text>
|
||||
<ProfileBadges
|
||||
profile={data.joinLinkPreviews[0].owner}
|
||||
size="sm"
|
||||
style={{marginTop: -4}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
a.font_medium,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.max_w_full,
|
||||
]}>
|
||||
{sanitizeHandle(data.joinLinkPreviews[0].owner.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
{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.`}
|
||||
</Text>
|
||||
<ActionButtons setScreenState={setScreenState} />
|
||||
</Wrapper>
|
||||
) : null}
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Wrapper({children}: React.PropsWithChildren<unknown>) {
|
||||
const t = useTheme()
|
||||
|
||||
const isDarkMode = t.name !== 'light'
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogomarkWithType
|
||||
width={136}
|
||||
fill={t.palette.primary_500}
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
top: 40,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.zoom_fade_in,
|
||||
a.align_center,
|
||||
a.gap_lg,
|
||||
a.p_2xl,
|
||||
a.pt_4xl,
|
||||
t.atoms.bg,
|
||||
t.atoms.shadow_xl,
|
||||
isDarkMode ? [a.border, t.atoms.border_contrast_low] : null,
|
||||
{
|
||||
borderRadius: 48,
|
||||
maxWidth: 320,
|
||||
width: '90%',
|
||||
},
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionButtons({
|
||||
setScreenState,
|
||||
}: {
|
||||
setScreenState: (state: LoggedOutScreenState) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const isDarkMode = t.name !== 'light'
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.gap_md]}>
|
||||
<Button
|
||||
testID="signInButton"
|
||||
onPress={() => {
|
||||
setScreenState(LoggedOutScreenState.S_Login)
|
||||
}}
|
||||
label={l`Sign in`}
|
||||
accessibilityHint={l`Opens flow to sign in to your existing Bluesky account`}
|
||||
size="large"
|
||||
color="primary"
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Sign in</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
testID="createAccountButton"
|
||||
onPress={() => {
|
||||
setScreenState(LoggedOutScreenState.S_CreateAccount)
|
||||
}}
|
||||
label={l`Create new account`}
|
||||
accessibilityHint={l`Opens flow to create a new Bluesky account`}
|
||||
size="large"
|
||||
color={isDarkMode ? 'secondary_inverted' : 'secondary'}
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Create account</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
ChatBskyGroupApproveJoinRequest,
|
||||
type ChatBskyGroupListJoinRequests,
|
||||
ChatBskyGroupRejectJoinRequest,
|
||||
} from '@atproto/api'
|
||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {type InfiniteData, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||
import {isNetworkError} from '#/lib/hooks/useCleanError'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {logger} from '#/logger'
|
||||
import {ConvoProvider, useConvo} from '#/state/messages/convo'
|
||||
import {ConvoStatus} from '#/state/messages/convo/types'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useJoinRequestMutation} from '#/state/queries/messages/join-requests'
|
||||
import {
|
||||
createListJoinRequestsQueryKey,
|
||||
useListJoinRequestsQuery,
|
||||
} from '#/state/queries/messages/list-join-requests'
|
||||
import {useSession} from '#/state/session'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
||||
import {Error} from '#/components/Error'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||
import {KnownFollowers} from '#/components/KnownFollowers'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {InviteLinkDialog} from './components/InviteLinkDialog'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
'MessagesJoinRequests'
|
||||
>
|
||||
|
||||
export function MessagesJoinRequestsScreen(props: Props) {
|
||||
const {t: l} = useLingui()
|
||||
const aaCopy = useAgeAssuranceCopy()
|
||||
return (
|
||||
<AgeRestrictedScreen
|
||||
screenTitle={l`Requests to join`}
|
||||
infoText={aaCopy.chatsInfoText}>
|
||||
<MessagesJoinRequestsScreenInner {...props} />
|
||||
</AgeRestrictedScreen>
|
||||
)
|
||||
}
|
||||
|
||||
function MessagesJoinRequestsScreenInner({route}: Props) {
|
||||
const convoId = route.params.conversation
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<ConvoProvider key={convoId} convoId={convoId}>
|
||||
<JoinRequestsInner />
|
||||
</ConvoProvider>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function JoinRequestsInner() {
|
||||
const {t: l} = useLingui()
|
||||
const convoState = useConvo()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
if (convoState.status === ConvoStatus.Error) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Error
|
||||
title={l`Something went wrong`}
|
||||
message={l`We couldn’t load this conversation’s join requests`}
|
||||
onRetry={() => convoState.error.retry()}
|
||||
sideBorders={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (!convoState.convo) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (convoState.convo.kind !== 'group') {
|
||||
return (
|
||||
<Error
|
||||
title={l`Wrong kind of conversation`}
|
||||
message={l`This screen is only available for group conversations.`}
|
||||
onGoBack={() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.replace('Messages', {animation: 'pop'})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <JoinRequestsList convo={convoState.convo} />
|
||||
}
|
||||
|
||||
function JoinRequestsList({
|
||||
convo,
|
||||
}: {
|
||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const bottomBarOffset = useBottomBarOffset()
|
||||
const {currentAccount} = useSession()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const queryClient = useQueryClient()
|
||||
const inviteLinkControl = Dialog.useDialogControl()
|
||||
|
||||
const getRemainingRequestCount = () => {
|
||||
const data = queryClient.getQueryData<
|
||||
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
|
||||
>(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 (
|
||||
<View style={[a.relative, a.flex_1, a.p_lg]}>
|
||||
<View style={[a.flex_row, a.align_start, a.gap_md]}>
|
||||
<ProfileCard.Link profile={item}>
|
||||
<ProfileCard.Avatar
|
||||
profile={item}
|
||||
moderationOpts={moderationOpts}
|
||||
size={44}
|
||||
disabledPreview
|
||||
/>
|
||||
</ProfileCard.Link>
|
||||
<View>
|
||||
<ProfileCard.Name profile={item} moderationOpts={moderationOpts} />
|
||||
<ProfileCard.Handle profile={item} />
|
||||
<View style={[a.mt_xs]}>
|
||||
<KnownFollowers
|
||||
profile={item}
|
||||
moderationOpts={moderationOpts}
|
||||
minimal
|
||||
showIfEmpty
|
||||
/>
|
||||
</View>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm, a.mt_md]}>
|
||||
<AcceptButton
|
||||
disabled={isMutating}
|
||||
onPress={() => approveJoinRequest({member: item.did})}
|
||||
/>
|
||||
<RejectButton
|
||||
disabled={isMutating}
|
||||
onPress={() => rejectJoinRequest({member: item.did})}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const footer = (
|
||||
<View
|
||||
onLayout={evt => 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,
|
||||
},
|
||||
]}>
|
||||
<Button
|
||||
label={l`Edit invite link`}
|
||||
size="large"
|
||||
color="primary"
|
||||
onPress={() => inviteLinkControl.open()}
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>
|
||||
<Trans context="button">Edit invite link</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Header count={requestCount} hasMoreRequests={hasNextPage} />
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.gap_sm,
|
||||
a.p_lg,
|
||||
]}>
|
||||
<ErrorIcon size="3xl" fill={t.atoms.text_contrast_high.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
a.px_lg,
|
||||
a.text_md,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Unable to fetch join requests.</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
color="primary"
|
||||
label={l`Press to retry`}
|
||||
onPress={() => void onRefresh()}
|
||||
disabled={isPTRing}
|
||||
size="large"
|
||||
style={[a.mt_md]}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={isPTRing ? Loader : RetryIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const showFooter = isOwner
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header count={requestCount} hasMoreRequests={hasNextPage} />
|
||||
<List
|
||||
data={items}
|
||||
keyExtractor={(item: bsky.profile.AnyProfileView) => item.did}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={
|
||||
isPending ? (
|
||||
<View
|
||||
style={[a.flex_1, a.align_center, a.justify_center, a.py_4xl]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : 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 && (
|
||||
<InviteLinkDialog
|
||||
convo={convo}
|
||||
control={inviteLinkControl}
|
||||
owner={owner}
|
||||
isOwner={isOwner}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Header({
|
||||
count,
|
||||
hasMoreRequests,
|
||||
}: {
|
||||
count?: number
|
||||
hasMoreRequests?: boolean
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
return (
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
{count === undefined ? (
|
||||
<Trans>Requests to join</Trans>
|
||||
) : hasMoreRequests ? (
|
||||
l({
|
||||
message: `${count}+ requests to join`,
|
||||
comment:
|
||||
'Displayed when there are more requests to join a group chat than have been loaded',
|
||||
})
|
||||
) : (
|
||||
<Plural
|
||||
value={count}
|
||||
zero="No requests to join"
|
||||
one="# request to join"
|
||||
other="# requests to join"
|
||||
/>
|
||||
)}
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function AcceptButton({
|
||||
disabled,
|
||||
onPress,
|
||||
}: {
|
||||
disabled?: boolean
|
||||
onPress: () => void
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={l`Accept join request`}
|
||||
size="small"
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
onPress={onPress}>
|
||||
<ButtonText>
|
||||
<Trans comment="Accept a request to join a chat" context="button">
|
||||
Accept
|
||||
</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function RejectButton({
|
||||
disabled,
|
||||
onPress,
|
||||
}: {
|
||||
disabled?: boolean
|
||||
onPress: () => void
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={l`Ignore join request`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
disabled={disabled}
|
||||
onPress={onPress}>
|
||||
<ButtonText>
|
||||
<Trans comment="Ignore a request to join a chat" context="button">
|
||||
Ignore
|
||||
</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type ModerationDecision,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -213,6 +214,20 @@ function GroupChatItem({
|
||||
primaryProfileModeration={moderation}
|
||||
isBlockedAccount={false}
|
||||
isDeletedAccount={false}
|
||||
subtitle={
|
||||
convo.details.joinRequestCount
|
||||
? convo.details.joinRequestCount > 20
|
||||
? l({
|
||||
message: '20+ new join requests',
|
||||
context:
|
||||
'Displayed when there are more than 20 requests to join a group chat',
|
||||
})
|
||||
: plural(convo.details.joinRequestCount, {
|
||||
one: '# new join request',
|
||||
other: '# new join requests',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
showProfileBadges={false}
|
||||
selected={selected}
|
||||
showMenu={showMenu}>
|
||||
|
||||
@@ -35,13 +35,14 @@ export function EditTextButton({
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.px_md,
|
||||
a.pl_lg,
|
||||
a.pr_sm,
|
||||
a.py_sm,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
{typeof children === 'function' ? children(context) : children}
|
||||
<View
|
||||
style={[
|
||||
a.ml_sm,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_50,
|
||||
{paddingHorizontal: 10, paddingVertical: 8},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||
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'
|
||||
@@ -64,14 +69,9 @@ export function InviteLinkDialog({
|
||||
)
|
||||
|
||||
const {joinLink} = convo.details
|
||||
const enabledStatus = joinLink?.enabledStatus
|
||||
|
||||
const defaultStep = joinLink ? Step.MANAGE : Step.INFO
|
||||
const defaultWhoCanJoin = joinLink
|
||||
? [
|
||||
`${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}`,
|
||||
]
|
||||
: ['anyone']
|
||||
const defaultWhoCanJoin = joinLink ? joinLinkToKey(joinLink) : 'anyone'
|
||||
|
||||
const [step, setStep] = useState(defaultStep)
|
||||
const [whoCanJoin, setWhoCanJoin] = useState(defaultWhoCanJoin)
|
||||
@@ -79,13 +79,13 @@ export function InviteLinkDialog({
|
||||
// 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
|
||||
? `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}`
|
||||
: null
|
||||
useEffect(() => {
|
||||
const joinLinkRuleKey = joinLink ? joinLinkToKey(joinLink) : null
|
||||
const [prevKey, setPrevKey] = useState(joinLinkRuleKey)
|
||||
if (joinLinkRuleKey !== prevKey) {
|
||||
setStep(joinLinkRuleKey ? Step.MANAGE : Step.INFO)
|
||||
setWhoCanJoin([joinLinkRuleKey ?? 'anyone'])
|
||||
}, [joinLinkRuleKey])
|
||||
setWhoCanJoin(joinLinkRuleKey ?? 'anyone')
|
||||
setPrevKey(joinLinkRuleKey)
|
||||
}
|
||||
|
||||
const {openComposer} = useOpenComposer()
|
||||
|
||||
@@ -163,22 +163,32 @@ export function InviteLinkDialog({
|
||||
let content: React.ReactNode = null
|
||||
let header: string | null = null
|
||||
switch (step) {
|
||||
case Step.INFO:
|
||||
case Step.INFO: {
|
||||
header = l`Invite link`
|
||||
content = (
|
||||
<>
|
||||
<View>
|
||||
<Text style={[a.text_md, t.atoms.text]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
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.
|
||||
added directly. You control who can join the chat. You can
|
||||
disable the link at any time.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.mt_lg, a.text_md, t.atoms.text]}>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
Your name, avatar, and the name of the group chat will be
|
||||
visible to everyone.
|
||||
Group chats can only have a maximum of{' '}
|
||||
{plural(convo.details.memberLimit, {
|
||||
one: '# person',
|
||||
other: '# people',
|
||||
})}
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
Your name, avatar, the name of the group chat, and the number of
|
||||
members will be visible to everyone.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -199,15 +209,16 @@ export function InviteLinkDialog({
|
||||
</>
|
||||
)
|
||||
break
|
||||
case Step.GENERATE:
|
||||
header =
|
||||
joinLink && enabledStatus === 'enabled'
|
||||
? l`Update invite link`
|
||||
: l`Generate invite link`
|
||||
}
|
||||
case Step.GENERATE: {
|
||||
const linkEnabled = joinLink?.enabledStatus === 'enabled'
|
||||
const linkHasChanged = linkEnabled && joinLinkRuleKey !== whoCanJoin
|
||||
|
||||
header = linkEnabled ? l`Update invite link` : l`Generate invite link`
|
||||
content = (
|
||||
<>
|
||||
<View>
|
||||
<Text style={[a.text_md, t.atoms.text]}>
|
||||
<Text style={[a.text_md]}>
|
||||
<Trans>Choose who can join this group chat and how.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -215,9 +226,9 @@ export function InviteLinkDialog({
|
||||
<Toggle.Group
|
||||
label={l`Who can join this group chat and how`}
|
||||
type="radio"
|
||||
values={whoCanJoin}
|
||||
onChange={setWhoCanJoin}>
|
||||
<View style={[a.gap_sm]}>
|
||||
values={[whoCanJoin]}
|
||||
onChange={([value]) => setWhoCanJoin(value)}>
|
||||
<View style={[a.gap_xs]}>
|
||||
{whoCanJoinOptions.map(option => (
|
||||
<Toggle.Item
|
||||
key={option.name}
|
||||
@@ -239,18 +250,22 @@ export function InviteLinkDialog({
|
||||
<View style={[a.mt_4xl]}>
|
||||
<Button
|
||||
label={
|
||||
joinLink && enabledStatus === 'enabled'
|
||||
? l`Update invite link`
|
||||
linkEnabled
|
||||
? linkHasChanged
|
||||
? l`Update invite link`
|
||||
: l`Back`
|
||||
: l`Generate invite link`
|
||||
}
|
||||
color="primary"
|
||||
color={linkEnabled && !linkHasChanged ? 'secondary' : 'primary'}
|
||||
size="large"
|
||||
disabled={isSaving}
|
||||
onPress={() => {
|
||||
const parts = whoCanJoin[0].split(':')
|
||||
const joinRule = parts[0]
|
||||
const requireApproval = parts[1] === 'requireApproval'
|
||||
if (joinLink && enabledStatus === 'enabled') {
|
||||
const {joinRule, requireApproval} = keyToJoinLink(whoCanJoin)
|
||||
if (linkEnabled) {
|
||||
if (!linkHasChanged) {
|
||||
setStep(Step.MANAGE)
|
||||
return
|
||||
}
|
||||
editJoinLink({
|
||||
joinRule,
|
||||
requireApproval,
|
||||
@@ -263,36 +278,40 @@ export function InviteLinkDialog({
|
||||
}
|
||||
}}>
|
||||
<ButtonText>
|
||||
{joinLink && enabledStatus === 'enabled'
|
||||
? l`Update invite link`
|
||||
{linkEnabled
|
||||
? linkHasChanged
|
||||
? l`Update invite link`
|
||||
: l`Back`
|
||||
: l`Generate invite link`}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={isSaving ? Loader : ArrowRightIcon} />
|
||||
{linkHasChanged && (
|
||||
<ButtonIcon icon={isSaving ? Loader : ArrowRightIcon} />
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
case Step.MANAGE: {
|
||||
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 currentOptionName = joinLink
|
||||
? `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}`
|
||||
: whoCanJoinOptions[0].name
|
||||
const currentOption = whoCanJoinOptions.find(
|
||||
o => o.name === currentOptionName,
|
||||
)
|
||||
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 =
|
||||
enabledStatus === 'enabled' ? l`Invite link` : l`Invite link disabled`
|
||||
header = linkEnabled ? l`Invite link` : l`Invite link disabled`
|
||||
content = (
|
||||
<>
|
||||
<View style={[a.mt_lg]}>
|
||||
<CopyTextButton
|
||||
disabled={enabledStatus === 'disabled' || !joinLink?.code}
|
||||
disabled={linkDisabled || !joinLink?.code}
|
||||
label={l`Invite link`}
|
||||
value={joinLinkURI}>
|
||||
<Text
|
||||
@@ -300,9 +319,7 @@ export function InviteLinkDialog({
|
||||
style={[
|
||||
a.mr_xs,
|
||||
a.text_md,
|
||||
enabledStatus === 'disabled'
|
||||
? t.atoms.text_contrast_low
|
||||
: t.atoms.text,
|
||||
linkDisabled ? t.atoms.text_contrast_low : t.atoms.text,
|
||||
]}>
|
||||
{joinLinkURI}
|
||||
</Text>
|
||||
@@ -319,25 +336,25 @@ export function InviteLinkDialog({
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{enabledStatus === 'enabled' ? (
|
||||
{linkEnabled ? (
|
||||
<View style={[a.mt_lg]}>
|
||||
{isOwner ? (
|
||||
<EditTextButton
|
||||
label={l`Edit link settings`}
|
||||
value={ownerValue}
|
||||
onPress={() => setStep(Step.GENERATE)}>
|
||||
<View style={[a.flex_1, a.mr_xs]}>
|
||||
<Text numberOfLines={1} style={[a.text_md, t.atoms.text]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<Text numberOfLines={1} style={[a.text_sm]}>
|
||||
{ownerValue}
|
||||
</Text>
|
||||
</View>
|
||||
</EditTextButton>
|
||||
) : (
|
||||
<Text style={[a.text_sm, t.atoms.text]}>{memberValue}</Text>
|
||||
<Text style={[a.text_sm]}>{memberValue}</Text>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
{enabledStatus === 'enabled' ? (
|
||||
{linkEnabled ? (
|
||||
<View style={[a.flex_row, a.justify_between, a.gap_sm, a.mt_lg]}>
|
||||
{isOwner ? (
|
||||
<StackedButton
|
||||
@@ -350,7 +367,7 @@ export function InviteLinkDialog({
|
||||
</StackedButton>
|
||||
) : null}
|
||||
<StackedButton
|
||||
disabled={enabledStatus === 'disabled'}
|
||||
disabled={linkDisabled}
|
||||
label={l`Post link`}
|
||||
icon={EditIcon}
|
||||
color="primary_subtle"
|
||||
@@ -366,7 +383,7 @@ export function InviteLinkDialog({
|
||||
<Trans>Post link</Trans>
|
||||
</StackedButton>
|
||||
<StackedButton
|
||||
disabled={enabledStatus === 'disabled'}
|
||||
disabled={linkDisabled}
|
||||
label={l`Share`}
|
||||
icon={ArrowShareRightIcon}
|
||||
color="primary_subtle"
|
||||
@@ -408,7 +425,7 @@ export function InviteLinkDialog({
|
||||
)
|
||||
break
|
||||
}
|
||||
case Step.CONFIRM_DISABLE:
|
||||
case Step.CONFIRM_DISABLE: {
|
||||
content = (
|
||||
<>
|
||||
<View style={[a.align_center, a.justify_center, a.mb_lg]}>
|
||||
@@ -425,14 +442,7 @@ export function InviteLinkDialog({
|
||||
]}>
|
||||
<Trans>Disable this invite link?</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.pb_2xl,
|
||||
a.text_center,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text,
|
||||
]}>
|
||||
<Text style={[a.pb_2xl, a.text_center, a.text_sm, a.leading_snug]}>
|
||||
<Trans>
|
||||
Anyone who has it will no longer be able to join or request to
|
||||
join. You can always create a new one.
|
||||
@@ -465,6 +475,7 @@ export function InviteLinkDialog({
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOwner && (!joinLink || joinLink.enabledStatus === 'disabled')) {
|
||||
@@ -472,7 +483,7 @@ export function InviteLinkDialog({
|
||||
content = (
|
||||
<>
|
||||
<View style={[a.mt_lg]}>
|
||||
<Text style={[a.text_sm, t.atoms.text]}>
|
||||
<Text style={[a.text_sm]}>
|
||||
<Trans>There is no invite link for this group chat.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -503,9 +514,7 @@ export function InviteLinkDialog({
|
||||
header={
|
||||
<View>
|
||||
<View style={[IS_WEB ? [a.px_2xl, a.pt_xl] : {paddingTop: 10}]}>
|
||||
<Text style={[a.font_bold, a.text_2xl, a.mb_sm, t.atoms.text]}>
|
||||
{header}
|
||||
</Text>
|
||||
<Text style={[a.font_bold, a.text_2xl, a.mb_sm]}>{header}</Text>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</View>
|
||||
@@ -517,3 +526,17 @@ export function InviteLinkDialog({
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function joinLinkToKey(joinLink: ChatBskyGroupDefs.JoinLinkView): string {
|
||||
return `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}`
|
||||
}
|
||||
|
||||
function keyToJoinLink(
|
||||
key: string,
|
||||
): Pick<ChatBskyGroupDefs.JoinLinkView, 'joinRule' | 'requireApproval'> {
|
||||
const [joinRule, requireApproval] = key.split(':')
|
||||
return {
|
||||
joinRule,
|
||||
requireApproval: requireApproval === 'requireApproval',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -240,7 +241,11 @@ export function MessagesList({
|
||||
|
||||
// Initial scroll to bottom — unconditional, not gated on isAtBottom. This is separated because contentInset
|
||||
// can cause an early onScroll with a negative offset that sets isAtBottom to false before we get here.
|
||||
if (!hasInitiallyScrolled.current && renderItems.length > 0) {
|
||||
// Empty convos take this path too (once history is done) so hasScrolled gets set without an animated scroll.
|
||||
if (
|
||||
!hasInitiallyScrolled.current &&
|
||||
(renderItems.length > 0 || !convoState.isFetchingHistory)
|
||||
) {
|
||||
hasInitiallyScrolled.current = true
|
||||
flatListRef.current?.scrollToOffset({offset: height, animated: false})
|
||||
// If history is already done loading, mark ready after a frame for the scroll to settle.
|
||||
@@ -494,127 +499,133 @@ export function MessagesList({
|
||||
|
||||
return (
|
||||
<InviteLinkDialogProvider convo={convoState.convo}>
|
||||
<KeyboardGestureArea
|
||||
interpolator="ios"
|
||||
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
|
||||
offset={Math.round(inputHeightJS)}
|
||||
// slightly too buggy unfortunately, enable when possible
|
||||
// textInputNativeID={textInputId}
|
||||
style={[a.flex_1]}>
|
||||
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
|
||||
<Animated.View style={[a.flex_1, animatedListStyle]}>
|
||||
<ScrollProvider onScroll={onScroll}>
|
||||
<List
|
||||
ref={flatListRef}
|
||||
data={renderItems}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
disableFullWindowScroll={true}
|
||||
disableVirtualization={true}
|
||||
// The extra two items account for the header and the footer components
|
||||
initialNumToRender={IS_NATIVE ? 32 : 62}
|
||||
maxToRenderPerBatch={IS_WEB ? 32 : 62}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
maintainVisibleContentPosition={{minIndexForVisible: 0}}
|
||||
removeClippedSubviews={false}
|
||||
sideBorders={false}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
onStartReached={onStartReached}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
showsVerticalScrollIndicator={!IS_ANDROID}
|
||||
scrollEventThrottle={100}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
||||
{convoState.hasAllHistory ? (
|
||||
convoState.convo?.kind === 'group' ? (
|
||||
<MessagesListGroupInfoPanel convo={convoState.convo} />
|
||||
) : (
|
||||
<MessagesListInfoPanel convo={convoState.convo} />
|
||||
)
|
||||
) : 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={
|
||||
<View
|
||||
style={web({height: tokens.space.md + inputHeightJS})}
|
||||
onLayout={onFooterLayout}
|
||||
/>
|
||||
}
|
||||
style={[
|
||||
web({
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||
scrollbarGutter: 'stable',
|
||||
}),
|
||||
]}
|
||||
pointerEvents={!hasScrolled ? 'none' : 'auto'}
|
||||
contentInset={{top: transparentHeaderHeight}}
|
||||
scrollIndicatorInsets={{top: transparentHeaderHeight}}
|
||||
/>
|
||||
</ScrollProvider>
|
||||
</Animated.View>
|
||||
<KeyboardStickyView
|
||||
style={[a.absolute, a.bottom_0, a.left_0, a.right_0]}
|
||||
onLayout={onInputLayout}
|
||||
minimumOffset={bottomInset}
|
||||
offset={{
|
||||
closed: platform({
|
||||
ios: tokens.space.lg, // hide bottom padding when closed
|
||||
default: 0,
|
||||
}),
|
||||
opened: 0,
|
||||
}}>
|
||||
{footer ?? (
|
||||
<ConversationFooter
|
||||
convoState={convoState}
|
||||
hasAcceptOverride={hasAcceptOverride}>
|
||||
{({loading}) =>
|
||||
ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
|
||||
<MessageComposer
|
||||
textInputId={textInputId}
|
||||
onSendMessage={(message: string) =>
|
||||
void onSendMessage(message)
|
||||
}
|
||||
hasEmbed={!!embedUri}
|
||||
setEmbed={setEmbed}
|
||||
loading={loading}>
|
||||
<MessageInputEmbed
|
||||
embedUri={embedUri}
|
||||
<MessageOverlays>
|
||||
<KeyboardGestureArea
|
||||
interpolator="ios"
|
||||
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
|
||||
offset={Math.round(inputHeightJS)}
|
||||
// slightly too buggy unfortunately, enable when possible
|
||||
// textInputNativeID={textInputId}
|
||||
style={[a.flex_1]}>
|
||||
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
|
||||
<Animated.View style={[a.flex_1, animatedListStyle]}>
|
||||
<ScrollProvider onScroll={onScroll}>
|
||||
<List
|
||||
ref={flatListRef}
|
||||
data={renderItems}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
disableFullWindowScroll={true}
|
||||
disableVirtualization={true}
|
||||
// The extra two items account for the header and the footer components
|
||||
initialNumToRender={IS_NATIVE ? 32 : 62}
|
||||
maxToRenderPerBatch={IS_WEB ? 32 : 62}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
maintainVisibleContentPosition={{minIndexForVisible: 0}}
|
||||
removeClippedSubviews={false}
|
||||
sideBorders={false}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
onStartReached={onStartReached}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
showsVerticalScrollIndicator={!IS_ANDROID}
|
||||
scrollEventThrottle={100}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
||||
{convoState.hasAllHistory ? (
|
||||
convoState.convo?.kind === 'group' ? (
|
||||
<MessagesListGroupInfoPanel convo={convoState.convo} />
|
||||
) : (
|
||||
<MessagesListInfoPanel convo={convoState.convo} />
|
||||
)
|
||||
) : 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={
|
||||
<View
|
||||
style={web({height: tokens.space.md + inputHeightJS})}
|
||||
onLayout={onFooterLayout}
|
||||
/>
|
||||
}
|
||||
style={[
|
||||
web({
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||
scrollbarGutter: 'stable',
|
||||
}),
|
||||
]}
|
||||
pointerEvents={!hasScrolled ? 'none' : 'auto'}
|
||||
contentInset={{top: transparentHeaderHeight}}
|
||||
scrollIndicatorInsets={{top: transparentHeaderHeight}}
|
||||
/>
|
||||
</ScrollProvider>
|
||||
</Animated.View>
|
||||
<KeyboardStickyView
|
||||
style={[a.absolute, a.bottom_0, a.left_0, a.right_0]}
|
||||
onLayout={onInputLayout}
|
||||
minimumOffset={bottomInset}
|
||||
offset={{
|
||||
closed: platform({
|
||||
ios: tokens.space.lg, // hide bottom padding when closed
|
||||
default: 0,
|
||||
}),
|
||||
opened: 0,
|
||||
}}>
|
||||
{footer ?? (
|
||||
<ConversationFooter
|
||||
convoState={convoState}
|
||||
hasAcceptOverride={hasAcceptOverride}>
|
||||
{({loading}) =>
|
||||
ax.features.enabled(
|
||||
ax.features.DmsNewMessageComposerEnable,
|
||||
) ? (
|
||||
<MessageComposer
|
||||
textInputId={textInputId}
|
||||
onSendMessage={(message: string) =>
|
||||
void onSendMessage(message)
|
||||
}
|
||||
hasEmbed={!!embedUri}
|
||||
setEmbed={setEmbed}
|
||||
/>
|
||||
</MessageComposer>
|
||||
) : (
|
||||
<MessageInput
|
||||
textInputId={textInputId}
|
||||
onSendMessage={onSendMessage}
|
||||
hasEmbed={!!embedUri}
|
||||
setEmbed={setEmbed}
|
||||
loading={loading}>
|
||||
<MessageInputEmbed
|
||||
embedUri={embedUri}
|
||||
loading={loading}>
|
||||
<MessageInputEmbed
|
||||
embedUri={embedUri}
|
||||
setEmbed={setEmbed}
|
||||
/>
|
||||
</MessageComposer>
|
||||
) : (
|
||||
<MessageInput
|
||||
textInputId={textInputId}
|
||||
onSendMessage={onSendMessage}
|
||||
hasEmbed={!!embedUri}
|
||||
setEmbed={setEmbed}
|
||||
/>
|
||||
</MessageInput>
|
||||
)
|
||||
}
|
||||
</ConversationFooter>
|
||||
)}
|
||||
</KeyboardStickyView>
|
||||
</KeyboardGestureArea>
|
||||
loading={loading}>
|
||||
<MessageInputEmbed
|
||||
embedUri={embedUri}
|
||||
setEmbed={setEmbed}
|
||||
/>
|
||||
</MessageInput>
|
||||
)
|
||||
}
|
||||
</ConversationFooter>
|
||||
)}
|
||||
</KeyboardStickyView>
|
||||
</KeyboardGestureArea>
|
||||
|
||||
{newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
|
||||
{newMessagesPill.show && (
|
||||
<NewMessagesPill onPress={scrollToEndOnPress} />
|
||||
)}
|
||||
</MessageOverlays>
|
||||
</InviteLinkDialogProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -123,7 +123,11 @@ export function AutomationLabelSettingsScreen({}: Props) {
|
||||
paddingRight: 20, // helps visually center
|
||||
},
|
||||
]}>
|
||||
<UserAvatar size={42} avatar={profile.avatar} type="user" />
|
||||
<UserAvatar
|
||||
size={42}
|
||||
avatar={profile.avatar}
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<View>
|
||||
<View style={[a.flex_row, a.align_baseline]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
+3
-11
@@ -13,7 +13,6 @@ import {
|
||||
} from 'expo-image-manipulator'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
import {getImageDim} from '#/lib/media/manip'
|
||||
import {openCropper} from '#/lib/media/picker'
|
||||
import {type PickerImage} from '#/lib/media/picker.shared'
|
||||
@@ -202,19 +201,12 @@ export function resetImageManipulation(
|
||||
return img
|
||||
}
|
||||
|
||||
export async function compressImage(
|
||||
img: ComposerImage,
|
||||
options?: {
|
||||
highResolution?: boolean
|
||||
increasedBlobSizeLimit?: boolean
|
||||
},
|
||||
): Promise<PickerImage> {
|
||||
export async function compressImage(img: ComposerImage): Promise<PickerImage> {
|
||||
const source = img.transformed || img.source
|
||||
const highResolution = options?.highResolution ?? false
|
||||
|
||||
let attempts = 0
|
||||
let maxDimension = highResolution ? 4000 : POST_IMG_MAX.width
|
||||
let maxBytes = options?.increasedBlobSizeLimit ? 2000000 : POST_IMG_MAX.size
|
||||
let maxDimension = 4000
|
||||
let maxBytes = 2000000
|
||||
|
||||
let minQualityPercentage = 0
|
||||
let maxQualityPercentage = 101 // exclusive
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import {AtpAgent} from '@atproto/api'
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries/index'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const joinLinkPreviewQueryKeyRoot = 'join-link-preview'
|
||||
|
||||
export const createJoinLinkPreviewQueryKey = (args: {
|
||||
codes: string[]
|
||||
hasSession: boolean
|
||||
}) =>
|
||||
createQueryKey(joinLinkPreviewQueryKeyRoot, args, {
|
||||
persistedVersion: 1,
|
||||
})
|
||||
|
||||
export function useJoinLinkPreviewsQuery({
|
||||
codes,
|
||||
hasSession,
|
||||
}: {
|
||||
codes?: string[]
|
||||
hasSession: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: createJoinLinkPreviewQueryKey({codes: codes ?? [], hasSession}),
|
||||
queryFn: async () => {
|
||||
if (!codes) throw new Error('No invite code')
|
||||
try {
|
||||
const previewAgent = new AtpAgent({service: CHAT_SERVICE})
|
||||
const res = hasSession
|
||||
? await agent.chat.bsky.group.getJoinLinkPreviews(
|
||||
{codes},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
: await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes})
|
||||
return res.data
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch join link preview', {safeMessage: error})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
enabled: codes != null && codes.length > 0,
|
||||
staleTime: STALE.SECONDS.FIFTEEN,
|
||||
})
|
||||
}
|
||||
|
||||
export function usePrefetchJoinLinkPreviews() {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => {
|
||||
return queryClient.prefetchQuery({
|
||||
queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}),
|
||||
queryFn: async () => {
|
||||
const previewAgent = new AtpAgent({service: CHAT_SERVICE})
|
||||
const res = hasSession
|
||||
? await agent.chat.bsky.group.getJoinLinkPreviews(
|
||||
{codes},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
: await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes})
|
||||
return res.data
|
||||
},
|
||||
staleTime: STALE.SECONDS.FIFTEEN,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 JoinRequestAction> = A extends 'approve'
|
||||
? ChatBskyGroupApproveJoinRequest.OutputSchema
|
||||
: ChatBskyGroupRejectJoinRequest.OutputSchema
|
||||
|
||||
export function useJoinRequestMutation<A extends JoinRequestAction>(
|
||||
action: A,
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: JoinRequestOutput<A>) => 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<A>
|
||||
},
|
||||
onMutate: ({member}) => {
|
||||
if (!convoId) return
|
||||
|
||||
const requestsKey = createListJoinRequestsQueryKey({convoId})
|
||||
const prevRequests =
|
||||
queryClient.getQueryData<
|
||||
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
|
||||
>(requestsKey)
|
||||
|
||||
const requestedByProfile = prevRequests?.pages
|
||||
.flatMap(page => page.requests)
|
||||
.find(request => request.requestedBy.did === member)?.requestedBy
|
||||
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<ChatBskyGroupListJoinRequests.OutputSchema>
|
||||
>(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<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
membersKey,
|
||||
)
|
||||
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
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)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -489,12 +489,22 @@ export function ListConvosProviderInner({
|
||||
// Join link data not included in the log event, trigger refetch to get it
|
||||
debouncedRefetch()
|
||||
} else if (
|
||||
ChatBskyConvoDefs.isLogIncomingJoinRequest(log) ||
|
||||
ChatBskyConvoDefs.isLogApproveJoinRequest(log) ||
|
||||
ChatBskyConvoDefs.isLogRejectJoinRequest(log) ||
|
||||
ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)
|
||||
ChatBskyConvoDefs.isLogRejectJoinRequest(log)
|
||||
) {
|
||||
// TODO Update join request count here when available. -dsb
|
||||
queryClient.setQueriesData(
|
||||
{queryKey: [RQKEY_ROOT]},
|
||||
(old?: ConvoListQueryData) =>
|
||||
updateGroupConvoJoinRequestCount(log, old, -1),
|
||||
)
|
||||
} else if (ChatBskyConvoDefs.isLogIncomingJoinRequest(log)) {
|
||||
queryClient.setQueriesData(
|
||||
{queryKey: [RQKEY_ROOT]},
|
||||
(old?: ConvoListQueryData) =>
|
||||
updateGroupConvoJoinRequestCount(log, old, 1),
|
||||
)
|
||||
} else if (ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)) {
|
||||
// Viewer isn't in the chat yet, no need to do anything
|
||||
} else if (ChatBskyConvoDefs.isLogAddReaction(log)) {
|
||||
queryClient.setQueriesData(
|
||||
{queryKey: [RQKEY_ROOT]},
|
||||
@@ -762,6 +772,29 @@ function optimisticUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
function updateGroupConvoJoinRequestCount(
|
||||
log: {convoId: string; rev: string},
|
||||
old: ConvoListQueryData | undefined,
|
||||
delta: 1 | -1,
|
||||
) {
|
||||
return optimisticUpdate(log.convoId, old, convo => {
|
||||
// Join requests are only meaningful for group convos.
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
|
||||
return {...convo, rev: log.rev}
|
||||
}
|
||||
const current = convo.kind.joinRequestCount ?? 0
|
||||
const next = Math.max(0, current + delta)
|
||||
return {
|
||||
...convo,
|
||||
kind: {
|
||||
...convo.kind,
|
||||
joinRequestCount: next === 0 ? undefined : next,
|
||||
},
|
||||
rev: log.rev,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeMemberFromConvoView(
|
||||
convo: ChatBskyConvoDefs.ConvoView,
|
||||
did: string,
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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<LandingType>(undefined)
|
||||
stateContext.displayName = 'ActiveLandingStateContext'
|
||||
const setContext = createContext<SetContext>((_: LandingType) => {})
|
||||
setContext.displayName = 'ActiveLandingSetContext'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [state, setState] = useState<LandingType>()
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
<setContext.Provider value={setState}>{children}</setContext.Provider>
|
||||
</stateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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<Controls>({
|
||||
})
|
||||
ControlsContext.displayName = 'LoggedOutControlsContext'
|
||||
|
||||
function getRequestedAccountFromLanding(
|
||||
landing: ReturnType<typeof useActiveLanding>,
|
||||
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<State>({
|
||||
showLoggedOut: shouldShowStarterPack,
|
||||
requestedAccountSwitchTo: shouldShowStarterPack
|
||||
? IS_WEB
|
||||
? 'starterpack'
|
||||
: 'new'
|
||||
: undefined,
|
||||
showLoggedOut: Boolean(requestedAccount),
|
||||
requestedAccountSwitchTo: requestedAccount,
|
||||
})
|
||||
|
||||
const controls = useMemo<Controls>(
|
||||
|
||||
@@ -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<ScreenState>(() => {
|
||||
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<ScreenState>(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 (
|
||||
<View
|
||||
@@ -107,6 +121,8 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
||||
|
||||
{screenState === ScreenState.S_StarterPack ? (
|
||||
<LandingScreen setScreenState={setScreenState} />
|
||||
) : screenState === ScreenState.S_GroupChatJoinRequest ? (
|
||||
<JoinRequest setScreenState={setScreenState} />
|
||||
) : screenState === ScreenState.S_LoginOrCreateAccount ? (
|
||||
<SplashScreen
|
||||
onPressSignin={() => {
|
||||
@@ -122,17 +138,12 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
||||
{screenState === ScreenState.S_Login ? (
|
||||
<Login
|
||||
onPressBack={() => {
|
||||
setScreenState(ScreenState.S_LoginOrCreateAccount)
|
||||
clearRequestedAccount()
|
||||
setScreenState(initialScreenState)
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
{screenState === ScreenState.S_CreateAccount ? (
|
||||
<Signup
|
||||
onPressBack={() =>
|
||||
setScreenState(ScreenState.S_LoginOrCreateAccount)
|
||||
}
|
||||
/>
|
||||
<Signup onPressBack={() => setScreenState(initialScreenState)} />
|
||||
) : undefined}
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user