Merge branch 'main' into @tomekzaw/nano-icons-feed-icons
This commit is contained in:
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: 🔧 Setup Docker buildx
|
- name: 🔧 Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||||
|
|
||||||
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
- name: ⬇️ Checkout repository
|
- name: ⬇️ Checkout repository
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- name: 🔧 Setup Docker buildx
|
- name: 🔧 Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||||
- name: Build ${{ matrix.image }}
|
- name: Build ${{ matrix.image }}
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: 🔧 Setup Docker buildx
|
- name: 🔧 Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||||
|
|
||||||
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: 🔧 Setup Docker buildx
|
- name: 🔧 Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||||
|
|
||||||
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: 🔧 Setup Docker buildx
|
- name: 🔧 Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||||
|
|
||||||
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: 🔧 Setup Docker buildx
|
- name: 🔧 Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||||
|
|
||||||
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
- name: 🔑 Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||||
|
|||||||
@@ -225,31 +225,6 @@ jobs:
|
|||||||
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
|
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
|
||||||
pnpm export
|
pnpm export
|
||||||
|
|
||||||
# Pin ONE bundle version for both publishes below. Each script used to call
|
|
||||||
# `date +%s` itself, so the same bytes reached denis and ota1 under versions
|
|
||||||
# seconds apart (observed: 1785102575 vs 1785102614). The version is part of
|
|
||||||
# the asset URL path, so each origin then served a manifest referencing a
|
|
||||||
# path only it had -- meaning a manifest fetched from one origin and assets
|
|
||||||
# fetched from the other 404. Both scripts fall back to `date +%s` when this
|
|
||||||
# is unset, so single-publisher callers are unaffected.
|
|
||||||
- name: 🔢 Pin bundle version
|
|
||||||
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
|
||||||
!steps.version.outputs.version-changed }}
|
|
||||||
run: echo "BUNDLE_VERSION=$(date +%s)" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
# denis on EKS has been the sole origin for updates.bsky.app since
|
|
||||||
# 2026-07-26, so it publishes FIRST: it is the path that actually serves
|
|
||||||
# clients. The legacy ota1 upload runs after it, and exists only so that
|
|
||||||
# rolling the Bunny origin back to ota1 would find current bundles there.
|
|
||||||
#
|
|
||||||
# The ordering is load-bearing, not cosmetic. While the legacy step ran
|
|
||||||
# first, its failure skipped these steps and nothing reached EITHER origin
|
|
||||||
# -- the dual-write took down the working path with it. Both steps are
|
|
||||||
# still required to pass, so a stale ota1 remains a loud failure, but the
|
|
||||||
# publish that serves users has already landed before the legacy one can
|
|
||||||
# fail.
|
|
||||||
#
|
|
||||||
# Both halves are removed together when ota1 is decommissioned (Phase 5).
|
|
||||||
- name: ☁️ Configure AWS credentials (denis)
|
- name: ☁️ Configure AWS credentials (denis)
|
||||||
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
||||||
!steps.version.outputs.version-changed }}
|
!steps.version.outputs.version-changed }}
|
||||||
@@ -279,19 +254,6 @@ jobs:
|
|||||||
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
||||||
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
||||||
|
|
||||||
- name: 📦 Package Bundle and 🚀 Deploy (legacy ota1)
|
|
||||||
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
|
||||||
!steps.version.outputs.version-changed }}
|
|
||||||
run: pnpm use-build-number bash scripts/bundleUpdate.sh
|
|
||||||
env:
|
|
||||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
|
||||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
|
||||||
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
|
||||||
# When set (required for production), these take precedence over the
|
|
||||||
# global EAS counters inside the use-build-number wrapper
|
|
||||||
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
|
||||||
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
|
||||||
|
|
||||||
buildIfNecessaryIOS:
|
buildIfNecessaryIOS:
|
||||||
name: Build and Submit iOS
|
name: Build and Submit iOS
|
||||||
needs: [bundleDeploy]
|
needs: [bundleDeploy]
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: 🤖 Claude
|
- name: 🤖 Claude
|
||||||
uses: anthropics/claude-code-action@5ef2e550a465a721f4f45e4a7d3c340c873e1dcc # v1.0.190
|
uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
|
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: 🤖 Claude review
|
- name: 🤖 Claude review
|
||||||
uses: anthropics/claude-code-action@5ef2e550a465a721f4f45e4a7d3c340c873e1dcc # v1.0.190
|
uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
|
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ Bluesky Social is a cross-platform social media application built with React Nat
|
|||||||
|
|
||||||
**Tech Stack:**
|
**Tech Stack:**
|
||||||
|
|
||||||
- React 19.1
|
- React 19.2
|
||||||
- React Native 0.81 with Expo 54
|
- React Native 0.86 with Expo 57
|
||||||
- TypeScript 7
|
- TypeScript 7
|
||||||
- React Navigation 7 for routing
|
- React Navigation 7 for routing
|
||||||
- TanStack Query (React Query) for data fetching
|
- TanStack Query (React Query) for data fetching
|
||||||
@@ -561,7 +561,7 @@ Only use `useMemo`/`useCallback` when you have a specific reason, such as:
|
|||||||
|
|
||||||
1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful
|
1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful
|
||||||
|
|
||||||
2. **Translations**: Wrap ALL user-facing strings with ` `l` `` or `<Trans>`
|
2. **Translations**: Wrap ALL user-facing strings with the `` l`…` `` macro or the `<Trans>` component
|
||||||
|
|
||||||
3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles
|
3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ jest.mock('expo-media-library', () => ({
|
|||||||
__esModule: true, // this property makes it work
|
__esModule: true, // this property makes it work
|
||||||
default: jest.fn(),
|
default: jest.fn(),
|
||||||
usePermissions: jest.fn(() => [true]),
|
usePermissions: jest.fn(() => [true]),
|
||||||
|
requestPermissionsAsync: jest.fn().mockResolvedValue({granted: true}),
|
||||||
|
saveToLibraryAsync: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('expo-media-library/legacy', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
usePermissions: jest.fn(() => [true]),
|
||||||
|
requestPermissionsAsync: jest.fn().mockResolvedValue({granted: true}),
|
||||||
|
saveToLibraryAsync: jest.fn().mockResolvedValue(undefined),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('@bsky.app/expo-guess-language', () => ({
|
jest.mock('@bsky.app/expo-guess-language', () => ({
|
||||||
|
|||||||
+2
-2
@@ -426,11 +426,11 @@
|
|||||||
},
|
},
|
||||||
"app.bsky.embed.video": {
|
"app.bsky.embed.video": {
|
||||||
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.video",
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.video",
|
||||||
"cid": "bafyreie3nug4ezpwodl6yrpgv5edkazzn22t7ea4yaeuun4rctyekkngai"
|
"cid": "bafyreiaqos23yv3t4ptrxily6s6qea5fcxfjzjlm42zq46xweby2mkgr4m"
|
||||||
},
|
},
|
||||||
"app.bsky.feed.defs": {
|
"app.bsky.feed.defs": {
|
||||||
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.defs",
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.defs",
|
||||||
"cid": "bafyreibg2zvz4n25vtd5yfqlodqra72s4h4lz7vwdexoutv3523wbw6jca"
|
"cid": "bafyreihxsdr5eig2gntbwn5duiskdae6lb7xyrtvump6nzzuz7sspyqv3e"
|
||||||
},
|
},
|
||||||
"app.bsky.feed.describeFeedGenerator": {
|
"app.bsky.feed.describeFeedGenerator": {
|
||||||
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.describeFeedGenerator",
|
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.describeFeedGenerator",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
"accept": [
|
"accept": [
|
||||||
"video/mp4"
|
"video/mp4"
|
||||||
],
|
],
|
||||||
"maxSize": 100000000,
|
"maxSize": 300000000,
|
||||||
"description": "The mp4 video file. May be up to 100mb, formerly limited to 50mb."
|
"description": "The mp4 video file. May be up to 300mb, formerly limited to 100mb."
|
||||||
},
|
},
|
||||||
"captions": {
|
"captions": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|||||||
@@ -202,10 +202,37 @@
|
|||||||
},
|
},
|
||||||
"embeddingDisabled": {
|
"embeddingDisabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"knownLikers": {
|
||||||
|
"description": "This property is present only in selected cases, as an optimization.",
|
||||||
|
"type": "ref",
|
||||||
|
"ref": "#knownLikers"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests."
|
"description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests."
|
||||||
},
|
},
|
||||||
|
"knownLikers": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The post's likers whom you also follow",
|
||||||
|
"required": [
|
||||||
|
"count",
|
||||||
|
"actors"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"actors": {
|
||||||
|
"type": "array",
|
||||||
|
"minLength": 0,
|
||||||
|
"maxLength": 5,
|
||||||
|
"items": {
|
||||||
|
"type": "ref",
|
||||||
|
"ref": "app.bsky.actor.defs#profileViewBasic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"feedViewPost": {
|
"feedViewPost": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
+15
-190
@@ -60,11 +60,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/Navigation.tsx": {
|
|
||||||
"typescript/no-floating-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/Splash.android.tsx": {
|
"src/Splash.android.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -80,14 +75,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/alf/util/flatten.ts": {
|
|
||||||
"typescript/no-explicit-any": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
|
||||||
"count": 9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/alf/util/systemUI.ts": {
|
"src/alf/util/systemUI.ts": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@@ -167,9 +154,6 @@
|
|||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 4
|
"count": 4
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-call": {
|
"typescript/no-unsafe-call": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
},
|
||||||
@@ -224,9 +208,6 @@
|
|||||||
"src/components/Lists.tsx": {
|
"src/components/Lists.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/Menu/types.ts": {
|
"src/components/Menu/types.ts": {
|
||||||
@@ -261,9 +242,6 @@
|
|||||||
"src/components/PostControls/BookmarkButton.tsx": {
|
"src/components/PostControls/BookmarkButton.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/PostControls/DiscoverDebug.tsx": {
|
"src/components/PostControls/DiscoverDebug.tsx": {
|
||||||
@@ -335,9 +313,6 @@
|
|||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 5
|
"count": 5
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"typescript/require-await": {
|
"typescript/require-await": {
|
||||||
"count": 4
|
"count": 4
|
||||||
}
|
}
|
||||||
@@ -346,9 +321,6 @@
|
|||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"typescript/require-await": {
|
"typescript/require-await": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
@@ -363,16 +335,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/ageAssurance/AgeAssuranceErrors.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/ageAssurance/AgeAssuranceInitDialog.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx": {
|
"src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx": {
|
||||||
"typescript/require-await": {
|
"typescript/require-await": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -388,18 +350,10 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/contacts/screens/ViewMatches.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/dialogs/DeviceLocationRequestDialog.tsx": {
|
"src/components/dialogs/DeviceLocationRequestDialog.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
"typescript/no-unsafe-member-access": {
|
||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
@@ -409,26 +363,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/dialogs/EmailDialog/screens/Update.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/dialogs/EmailDialog/screens/Verify.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/dialogs/LanguageSelectDialog.tsx": {
|
"src/components/dialogs/LanguageSelectDialog.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -447,7 +381,7 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 3
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-unsafe-member-access": {
|
"typescript/no-unsafe-member-access": {
|
||||||
"count": 2
|
"count": 2
|
||||||
@@ -490,9 +424,6 @@
|
|||||||
"src/components/dialogs/lists/CreateOrEditListDialog.tsx": {
|
"src/components/dialogs/lists/CreateOrEditListDialog.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": {
|
"src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": {
|
||||||
@@ -580,16 +511,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/intents/VerifyEmailIntentDialog.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/verification/VerificationCreatePrompt.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/verification/VerificationRemovePrompt.tsx": {
|
"src/components/verification/VerificationRemovePrompt.tsx": {
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -696,13 +617,10 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 3
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/lib/hooks/usePermissions.ts": {
|
"src/lib/hooks/usePermissions.ts": {
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-enum-comparison": {
|
"typescript/no-unsafe-enum-comparison": {
|
||||||
"count": 5
|
"count": 5
|
||||||
}
|
}
|
||||||
@@ -845,15 +763,9 @@
|
|||||||
},
|
},
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/E2E/SharedPreferencesTesterScreen.tsx": {
|
"src/screens/E2E/SharedPreferencesTesterScreen.tsx": {
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 6
|
|
||||||
},
|
|
||||||
"typescript/require-await": {
|
"typescript/require-await": {
|
||||||
"count": 6
|
"count": 6
|
||||||
}
|
}
|
||||||
@@ -863,22 +775,9 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Feeds/NoSavedFeedsOfAnyType.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/Home/NoFeedsPinned.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/List/ListHiddenScreen.tsx": {
|
"src/screens/List/ListHiddenScreen.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 3
|
"count": 3
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Log.tsx": {
|
"src/screens/Log.tsx": {
|
||||||
@@ -886,20 +785,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Moderation/index.tsx": {
|
|
||||||
"typescript/no-explicit-any": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"typescript/no-floating-promises": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/ModerationInteractionSettings/index.tsx": {
|
"src/screens/ModerationInteractionSettings/index.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -922,19 +807,6 @@
|
|||||||
"src/screens/Onboarding/StepProfile/index.tsx": {
|
"src/screens/Onboarding/StepProfile/index.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": {
|
"src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": {
|
||||||
@@ -962,18 +834,12 @@
|
|||||||
"src/screens/Profile/Header/EditProfileDialog.tsx": {
|
"src/screens/Profile/Header/EditProfileDialog.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 3
|
"count": 3
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Profile/Header/ProfileHeaderLabeler.tsx": {
|
"src/screens/Profile/Header/ProfileHeaderLabeler.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
"typescript/no-unsafe-member-access": {
|
||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
@@ -1004,11 +870,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/ProfileList/components/Header.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/ProfileList/components/MoreOptionsMenu.tsx": {
|
"src/screens/ProfileList/components/MoreOptionsMenu.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -1027,11 +888,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/SavedFeeds.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/Search/modules/ExploreSuggestedAccounts.tsx": {
|
"src/screens/Search/modules/ExploreSuggestedAccounts.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -1093,9 +949,6 @@
|
|||||||
"src/screens/Settings/components/ChangePasswordDialog.tsx": {
|
"src/screens/Settings/components/ChangePasswordDialog.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Settings/components/CopyButton.tsx": {
|
"src/screens/Settings/components/CopyButton.tsx": {
|
||||||
@@ -1107,9 +960,6 @@
|
|||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
"typescript/no-unsafe-member-access": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
@@ -1119,11 +969,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Settings/components/DisableEmail2FADialog.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/Settings/components/OTAInfo.tsx": {
|
"src/screens/Settings/components/OTAInfo.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -1147,7 +992,7 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 2
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-unsafe-call": {
|
"typescript/no-unsafe-call": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -1168,9 +1013,6 @@
|
|||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"typescript/require-await": {
|
"typescript/require-await": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
@@ -1474,9 +1316,6 @@
|
|||||||
"src/view/com/composer/SelectMediaButton.tsx": {
|
"src/view/com/composer/SelectMediaButton.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/composer/drafts/DraftsButton.tsx": {
|
"src/view/com/composer/drafts/DraftsButton.tsx": {
|
||||||
@@ -1500,11 +1339,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/composer/photos/EditImageDialog.web.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/view/com/composer/photos/Gallery.tsx": {
|
"src/view/com/composer/photos/Gallery.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@@ -1513,9 +1347,6 @@
|
|||||||
"src/view/com/composer/photos/OpenCameraBtn.tsx": {
|
"src/view/com/composer/photos/OpenCameraBtn.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/composer/text-input/TextInput.web.tsx": {
|
"src/view/com/composer/text-input/TextInput.web.tsx": {
|
||||||
@@ -1560,7 +1391,7 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 3
|
"count": 2
|
||||||
},
|
},
|
||||||
"typescript/no-unsafe-member-access": {
|
"typescript/no-unsafe-member-access": {
|
||||||
"count": 6
|
"count": 6
|
||||||
@@ -1598,12 +1429,6 @@
|
|||||||
"src/view/com/pager/TabBar.web.tsx": {
|
"src/view/com/pager/TabBar.web.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-unsafe-call": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
|
||||||
"count": 4
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/post-thread/PostLikedBy.tsx": {
|
"src/view/com/post-thread/PostLikedBy.tsx": {
|
||||||
@@ -1627,9 +1452,6 @@
|
|||||||
"src/view/com/posts/FeedShutdownMsg.tsx": {
|
"src/view/com/posts/FeedShutdownMsg.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/posts/PostFeedErrorMessage.tsx": {
|
"src/view/com/posts/PostFeedErrorMessage.tsx": {
|
||||||
@@ -1693,9 +1515,6 @@
|
|||||||
"src/view/com/util/Views.tsx": {
|
"src/view/com/util/Views.tsx": {
|
||||||
"react/display-name": {
|
"react/display-name": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-explicit-any": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/util/Views.web.tsx": {
|
"src/view/com/util/Views.web.tsx": {
|
||||||
@@ -1703,11 +1522,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/util/forms/Button.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/view/screens/Feeds.tsx": {
|
"src/view/screens/Feeds.tsx": {
|
||||||
"typescript/no-floating-promises": {
|
"typescript/no-floating-promises": {
|
||||||
"count": 3
|
"count": 3
|
||||||
@@ -1770,5 +1584,16 @@
|
|||||||
"typescript/no-unsafe-member-access": {
|
"typescript/no-unsafe-member-access": {
|
||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"webpack.config.js": {
|
||||||
|
"import/no-nodejs-modules": {
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
"typescript/no-unsafe-call": {
|
||||||
|
"count": 12
|
||||||
|
},
|
||||||
|
"typescript/no-unsafe-member-access": {
|
||||||
|
"count": 33
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-5
@@ -93,17 +93,16 @@
|
|||||||
"update-extensions": "bash scripts/updateExtensions.sh",
|
"update-extensions": "bash scripts/updateExtensions.sh",
|
||||||
"export": "expo export --dump-sourcemap && pnpm upload-native-sourcemaps",
|
"export": "expo export --dump-sourcemap && pnpm upload-native-sourcemaps",
|
||||||
"upload-native-sourcemaps": "pnpm exec sentry-expo-upload-sourcemaps dist",
|
"upload-native-sourcemaps": "pnpm exec sentry-expo-upload-sourcemaps dist",
|
||||||
"make-deploy-bundle": "bash scripts/bundleUpdate.sh",
|
|
||||||
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 pnpm build-web",
|
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 pnpm build-web",
|
||||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=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 ."
|
"prettier": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/common-web": "0.5.9",
|
"@atproto/common-web": "0.5.10",
|
||||||
"@atproto/lex": "0.3.6",
|
"@atproto/lex": "0.3.8",
|
||||||
"@atproto/lex-password-session": "0.2.0",
|
"@atproto/lex-password-session": "0.2.2",
|
||||||
"@atproto/syntax": "0.7.4",
|
"@atproto/syntax": "0.7.5",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@bsky.app/alf": "^0.1.15",
|
"@bsky.app/alf": "^0.1.15",
|
||||||
|
|||||||
Generated
+74
-74
@@ -243,17 +243,17 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web':
|
'@atproto/common-web':
|
||||||
specifier: 0.5.9
|
specifier: 0.5.10
|
||||||
version: 0.5.9
|
version: 0.5.10
|
||||||
'@atproto/lex':
|
'@atproto/lex':
|
||||||
specifier: 0.3.6
|
specifier: 0.3.8
|
||||||
version: 0.3.6
|
version: 0.3.8
|
||||||
'@atproto/lex-password-session':
|
'@atproto/lex-password-session':
|
||||||
specifier: 0.2.0
|
specifier: 0.2.2
|
||||||
version: 0.2.0
|
version: 0.2.2
|
||||||
'@atproto/syntax':
|
'@atproto/syntax':
|
||||||
specifier: 0.7.4
|
specifier: 0.7.5
|
||||||
version: 0.7.4
|
version: 0.7.5
|
||||||
'@bitdrift/react-native':
|
'@bitdrift/react-native':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.6.8
|
||||||
version: 0.6.14(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)
|
version: 0.6.14(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)
|
||||||
@@ -298,7 +298,7 @@ importers:
|
|||||||
version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)
|
version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)
|
||||||
'@bsky/sdk':
|
'@bsky/sdk':
|
||||||
specifier: 1.0.1
|
specifier: 1.0.1
|
||||||
version: 1.0.1(@atproto/lex@0.3.6)
|
version: 1.0.1(@atproto/lex@0.3.8)
|
||||||
'@emoji-mart/data':
|
'@emoji-mart/data':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
@@ -900,12 +900,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-vfvoDhu6ds6BT3Pqe+d2/LBU1WpDRtt69y6zltlfMCoucPm82m1j50/Wb6xTvv+oZ+2j0JyZVXsmXQ12SWYQJg==}
|
resolution: {integrity: sha512-vfvoDhu6ds6BT3Pqe+d2/LBU1WpDRtt69y6zltlfMCoucPm82m1j50/Wb6xTvv+oZ+2j0JyZVXsmXQ12SWYQJg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.9':
|
'@atproto/common-web@0.5.10':
|
||||||
resolution: {integrity: sha512-2c5C6YV8352JJz9Z5fNfcsstllaKGAb3cQW9aO7unMAGh/FzTGFq+xOwNMW2lTeagO6z530uxYm8AiSRM6O+DQ==}
|
resolution: {integrity: sha512-w4JUdsJ3VXt8ewkavYh5m/u0UbxDtFdCKhNZPEpJ0U1vcWIjDaSInIexteETDgD7fBJAhidIEi30MGz1kk49ug==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common@0.8.0':
|
'@atproto/common@0.8.1':
|
||||||
resolution: {integrity: sha512-fBtd08LdouLqNaL8SaGLEIa0l3Ue27yR810jhUu+iBv/xdoXp+1G0pxvPe6JUStH7YGtf/v/v0w/cRGh1xUOaA==}
|
resolution: {integrity: sha512-WvgY0CDgodmTS7B1KDVFt868h1b85iQtQZeoDVI+6oOZmg1jINWUl+bZ1rwf59Tk1PqcMz5OzH6OS2J7Mb9/Iw==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/crypto@0.5.4':
|
'@atproto/crypto@0.5.4':
|
||||||
@@ -916,57 +916,57 @@ packages:
|
|||||||
resolution: {integrity: sha512-BlnwQ+obL+4ZA71KH/EzZ3TY+cpSxnLiUI85mjBJIQwvDF/oN2sQA18wIj9jpduviIt2b/cMtlJuzHjzBkfXvw==}
|
resolution: {integrity: sha512-BlnwQ+obL+4ZA71KH/EzZ3TY+cpSxnLiUI85mjBJIQwvDF/oN2sQA18wIj9jpduviIt2b/cMtlJuzHjzBkfXvw==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-builder@0.1.12':
|
'@atproto/lex-builder@0.1.13':
|
||||||
resolution: {integrity: sha512-uo3mcdoOgqBfQnf+PhYYLNEBdUTgoNe2ARYb3cUaiec8O0oritOeZndUDgmths06ACrlqFkZNTV1YJX6CsH71g==}
|
resolution: {integrity: sha512-YHSONY88fWdMcN6Xa/Noap98DNW2m5zVZGspQWaROsX2lFx1mLf7g/XKLBcPRVDpE3OWRZ98QkkSZN3SgbOXrg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-cbor@0.1.6':
|
'@atproto/lex-cbor@0.1.6':
|
||||||
resolution: {integrity: sha512-PxLQy7jzV8u9zTGURNF8ZczLPk+ZH+WNx/2z/z7RacJbu7sWNpx26U2pC7cIkWtt4jyDaruaLhssw3w3atLotA==}
|
resolution: {integrity: sha512-PxLQy7jzV8u9zTGURNF8ZczLPk+ZH+WNx/2z/z7RacJbu7sWNpx26U2pC7cIkWtt4jyDaruaLhssw3w3atLotA==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-client@0.3.3':
|
'@atproto/lex-client@0.3.5':
|
||||||
resolution: {integrity: sha512-sWChe7Jukm9uxDfxT6ElICp6vMTqAFmwcHdSLdMDtsKBZSrwO7vWL9F6rpoMPyxS29Ob9nNAhMg30kKfZrb+uw==}
|
resolution: {integrity: sha512-IOYjgQ2H+pw8QhV0vMMG9PTRg+oh5In9APbGBvotNZlbF4SCZNDVd7dONFY4rU2Dg6BHSkiqbqoNUHNgTC+ITA==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-data@0.1.7':
|
'@atproto/lex-data@0.1.7':
|
||||||
resolution: {integrity: sha512-kW/dPLqo/WgCLV+XESR4JKwV6c1rZWJGOfuPupZGTjEDAKoBbKXdaEzX9/1vKQYbZ9U3j0DS/n7OFFK7wBugyQ==}
|
resolution: {integrity: sha512-kW/dPLqo/WgCLV+XESR4JKwV6c1rZWJGOfuPupZGTjEDAKoBbKXdaEzX9/1vKQYbZ9U3j0DS/n7OFFK7wBugyQ==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-document@0.1.9':
|
'@atproto/lex-document@0.1.10':
|
||||||
resolution: {integrity: sha512-TmpwttQ9pKCXlvwUlJ/5HMZPtJMFBvgZ1bunEeQD5JfU6n/sgK/fob8tfjhTdL2QrpZMtM6opMEk3w5STm1RDQ==}
|
resolution: {integrity: sha512-7ivrxWttSxInDksR2rtM24xGdntY/3Ew5j08W5SiG45B+T2nyARL5EOk2xMXEm3MLjPrpNYAybA21ZEDYhGG0Q==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-installer@0.1.15':
|
'@atproto/lex-installer@0.1.17':
|
||||||
resolution: {integrity: sha512-4/xDyY7yxxs66vF0WZB+7fcuEy4I5IRDyDoQFzLQx7C6PlQ76/e/PxvYTff7x0/N6hknmOlANODnnxo3rtdnCA==}
|
resolution: {integrity: sha512-XSXiFLoDT7YEe+GuUdwczKQmhuLrA2CHVVlL8s5U8FLtWC5Cr3ELmz1t1b/Y8dV+R31hWyvdsk/t992B6+WW3Q==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.6':
|
'@atproto/lex-json@0.1.6':
|
||||||
resolution: {integrity: sha512-mvrAd0lbyuecIHjyld8QN6MN6CBf4j0GCxLzegsvLh0SvDf+GbYWklkcQqmITL44yFQOwmA/QNIQj0Uvh7+R/g==}
|
resolution: {integrity: sha512-mvrAd0lbyuecIHjyld8QN6MN6CBf4j0GCxLzegsvLh0SvDf+GbYWklkcQqmITL44yFQOwmA/QNIQj0Uvh7+R/g==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-password-session@0.2.0':
|
'@atproto/lex-password-session@0.2.2':
|
||||||
resolution: {integrity: sha512-u958Etax/bPVr9FWko+/B5nRcU1klQHvrdiVtYb4lkgrM6uvV7BvaFJZYEu5DzlEeqNAZ9a65PmbC1F5oyUBjQ==}
|
resolution: {integrity: sha512-lifxp9sMdOGP1AfPhjNUMson6yzqEbS0PS8Q7UCVQucXbBlM89r5ff9DMBJF2sTiH5Qt8yXXsthI543OT9FXgg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-resolver@0.2.8':
|
'@atproto/lex-resolver@0.2.10':
|
||||||
resolution: {integrity: sha512-j3dIxeEAStp/RzJS4MECRDPuOFX41mX9fnteGgrQhe9053eRuTnl2oTyJbV7rKgzy+hEI4jH6X6Y4wOStjUb/Q==}
|
resolution: {integrity: sha512-0Ml37emPqi6zr7hBAVR8fi7XDgG6xWsksBotoMU4Doh/PNFNt/H79Zopt/jnmDRcl8DGj9PNqZOZjzcAr2IuFw==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-schema@0.2.5':
|
'@atproto/lex-schema@0.2.6':
|
||||||
resolution: {integrity: sha512-/tP3dRqaZu0EIoq0BSVzhvFGL6u0uu2KFDV21i73DpFsg6TXOTddKJ9bvgK83XdAJYL7PWi703XsaR31v9u5Qg==}
|
resolution: {integrity: sha512-8fe/gmhjMImBsy073jMANkbHHQLe/w4s9XLATpAz+Qd6WrR9RYTqc2DU8uZDL13F5bfdGTj8KECt/Xs4CoPXdQ==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex@0.3.6':
|
'@atproto/lex@0.3.8':
|
||||||
resolution: {integrity: sha512-8Hpc6MbfkPJKaO5CvvIerEylbihI5GfrY5DzapZD4rV2TWfm3RU+DzeRpsxQJ0W9WOZRYvp1JnBcq/gZ48Qb0g==}
|
resolution: {integrity: sha512-8X7f6wxVDm4IVNR80Yg3BV3IXb89sKo/KIZeYb4or/EHP7ljazVU4qU+RPXmlELnWiIcw+PiJE+hPSkGLnMPOA==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@atproto/repo@0.10.11':
|
'@atproto/repo@0.10.12':
|
||||||
resolution: {integrity: sha512-QqDzEv8d6NUuqZ0xvWG+n8DfWd4Vuka04nyuD9oglKsj5avB1zbEsWx3Oit1oYtZFUf9JSVWMDqMXhu3UbB+lA==}
|
resolution: {integrity: sha512-SnDSoFi1bRAfN0IcDjSPFcefknDCIIjKgJXgFsd5jvktCkopmzml8BpQEP5t2/mcZ7NvEn5onQ0kaWkXhgL+5g==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/syntax@0.7.4':
|
'@atproto/syntax@0.7.5':
|
||||||
resolution: {integrity: sha512-EHsEHtasH/DGPljBYecVDjweGMQ5eTu6Ns0GZ5z0qdqpOI8ipBvldWnMIxWkA+5HfZ9Dsu4V1MX0WP7wgL8cuA==}
|
resolution: {integrity: sha512-6vnLQK8OAzg0dO6z/xnvuXn5zMV0UMI54zbxk7G7BXhGlIlLMb1yo+JVYtAlv8Nxzr+AaFdNZ8AJt+L/QhJMFQ==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@babel/code-frame@7.10.4':
|
'@babel/code-frame@7.10.4':
|
||||||
@@ -9758,16 +9758,16 @@ snapshots:
|
|||||||
|
|
||||||
'@atproto-labs/simple-store@0.5.1': {}
|
'@atproto-labs/simple-store@0.5.1': {}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.9':
|
'@atproto/common-web@0.5.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
'@atproto/lex-json': 0.1.6
|
'@atproto/lex-json': 0.1.6
|
||||||
'@atproto/syntax': 0.7.4
|
'@atproto/syntax': 0.7.5
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@atproto/common@0.8.0':
|
'@atproto/common@0.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.5.9
|
'@atproto/common-web': 0.5.10
|
||||||
'@atproto/lex-cbor': 0.1.6
|
'@atproto/lex-cbor': 0.1.6
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
multiformats: 13.4.2
|
multiformats: 13.4.2
|
||||||
@@ -9783,10 +9783,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@atproto/lex-builder@0.1.12':
|
'@atproto/lex-builder@0.1.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-document': 0.1.9
|
'@atproto/lex-document': 0.1.10
|
||||||
'@atproto/lex-schema': 0.2.5
|
'@atproto/lex-schema': 0.2.6
|
||||||
prettier: 3.9.6
|
prettier: 3.9.6
|
||||||
ts-morph: 27.0.2
|
ts-morph: 27.0.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -9797,11 +9797,11 @@ snapshots:
|
|||||||
cborg: 4.5.8
|
cborg: 4.5.8
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-client@0.3.3':
|
'@atproto/lex-client@0.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
'@atproto/lex-json': 0.1.6
|
'@atproto/lex-json': 0.1.6
|
||||||
'@atproto/lex-schema': 0.2.5
|
'@atproto/lex-schema': 0.2.6
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-data@0.1.7':
|
'@atproto/lex-data@0.1.7':
|
||||||
@@ -9810,21 +9810,21 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
unicode-segmenter: 0.14.5
|
unicode-segmenter: 0.14.5
|
||||||
|
|
||||||
'@atproto/lex-document@0.1.9':
|
'@atproto/lex-document@0.1.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-schema': 0.2.5
|
'@atproto/lex-schema': 0.2.6
|
||||||
core-js: 3.50.0
|
core-js: 3.50.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-installer@0.1.15':
|
'@atproto/lex-installer@0.1.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-builder': 0.1.12
|
'@atproto/lex-builder': 0.1.13
|
||||||
'@atproto/lex-cbor': 0.1.6
|
'@atproto/lex-cbor': 0.1.6
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
'@atproto/lex-document': 0.1.9
|
'@atproto/lex-document': 0.1.10
|
||||||
'@atproto/lex-resolver': 0.2.8
|
'@atproto/lex-resolver': 0.2.10
|
||||||
'@atproto/lex-schema': 0.2.5
|
'@atproto/lex-schema': 0.2.6
|
||||||
'@atproto/syntax': 0.7.4
|
'@atproto/syntax': 0.7.5
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.6':
|
'@atproto/lex-json@0.1.6':
|
||||||
@@ -9832,54 +9832,54 @@ snapshots:
|
|||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-password-session@0.2.0':
|
'@atproto/lex-password-session@0.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-client': 0.3.3
|
'@atproto/lex-client': 0.3.5
|
||||||
'@atproto/lex-schema': 0.2.5
|
'@atproto/lex-schema': 0.2.6
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-resolver@0.2.8':
|
'@atproto/lex-resolver@0.2.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto-labs/did-resolver': 0.3.7
|
'@atproto-labs/did-resolver': 0.3.7
|
||||||
'@atproto/crypto': 0.5.4
|
'@atproto/crypto': 0.5.4
|
||||||
'@atproto/lex-client': 0.3.3
|
'@atproto/lex-client': 0.3.5
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
'@atproto/lex-document': 0.1.9
|
'@atproto/lex-document': 0.1.10
|
||||||
'@atproto/lex-schema': 0.2.5
|
'@atproto/lex-schema': 0.2.6
|
||||||
'@atproto/repo': 0.10.11
|
'@atproto/repo': 0.10.12
|
||||||
'@atproto/syntax': 0.7.4
|
'@atproto/syntax': 0.7.5
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-schema@0.2.5':
|
'@atproto/lex-schema@0.2.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
'@atproto/syntax': 0.7.4
|
'@atproto/syntax': 0.7.5
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex@0.3.6':
|
'@atproto/lex@0.3.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-builder': 0.1.12
|
'@atproto/lex-builder': 0.1.13
|
||||||
'@atproto/lex-client': 0.3.3
|
'@atproto/lex-client': 0.3.5
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
'@atproto/lex-installer': 0.1.15
|
'@atproto/lex-installer': 0.1.17
|
||||||
'@atproto/lex-json': 0.1.6
|
'@atproto/lex-json': 0.1.6
|
||||||
'@atproto/lex-schema': 0.2.5
|
'@atproto/lex-schema': 0.2.6
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
yargs: 18.1.0
|
yargs: 18.1.0
|
||||||
|
|
||||||
'@atproto/repo@0.10.11':
|
'@atproto/repo@0.10.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common': 0.8.0
|
'@atproto/common': 0.8.1
|
||||||
'@atproto/common-web': 0.5.9
|
'@atproto/common-web': 0.5.10
|
||||||
'@atproto/crypto': 0.5.4
|
'@atproto/crypto': 0.5.4
|
||||||
'@atproto/lex-cbor': 0.1.6
|
'@atproto/lex-cbor': 0.1.6
|
||||||
'@atproto/lex-data': 0.1.7
|
'@atproto/lex-data': 0.1.7
|
||||||
'@atproto/syntax': 0.7.4
|
'@atproto/syntax': 0.7.5
|
||||||
varint: 6.0.0
|
varint: 6.0.0
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@atproto/syntax@0.7.4':
|
'@atproto/syntax@0.7.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
iso-datestring-validator: 2.2.2
|
iso-datestring-validator: 2.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -10842,11 +10842,11 @@ snapshots:
|
|||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)
|
react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)
|
||||||
|
|
||||||
'@bsky/sdk@1.0.1(@atproto/lex@0.3.6)':
|
'@bsky/sdk@1.0.1(@atproto/lex@0.3.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto-labs/handle-resolver': 0.4.8
|
'@atproto-labs/handle-resolver': 0.4.8
|
||||||
'@atproto/lex': 0.3.6
|
'@atproto/lex': 0.3.8
|
||||||
'@atproto/syntax': 0.7.4
|
'@atproto/syntax': 0.7.5
|
||||||
tlds: 1.261.0
|
tlds: 1.261.0
|
||||||
|
|
||||||
'@crowdin/cli@4.14.2':
|
'@crowdin/cli@4.14.2':
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
rm -rf bundleTempDir
|
|
||||||
rm -rf bundle.tar.gz
|
|
||||||
|
|
||||||
echo "Creating tarball..."
|
|
||||||
node scripts/bundleUpdate.js
|
|
||||||
|
|
||||||
if [ -z "$RUNTIME_VERSION" ]; then
|
|
||||||
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd bundleTempDir || exit
|
|
||||||
|
|
||||||
# Shared with denisPublish.sh when both run in one job -- see the note there.
|
|
||||||
# Both origins must receive the same bundle version for the same bytes, because
|
|
||||||
# the version is part of the asset URL path.
|
|
||||||
BUNDLE_VERSION="${BUNDLE_VERSION:-$(date +%s)}"
|
|
||||||
|
|
||||||
# This MUST address ota1's own origin hostname, never updates.bsky.app.
|
|
||||||
#
|
|
||||||
# Since the 2026-07-26 cutover updates.bsky.app resolves to denis on EKS, which
|
|
||||||
# deliberately has no /v1/upload route -- publishing there is out-of-band via
|
|
||||||
# `denis publish` (see denisPublish.sh). Posting to the CDN hostname therefore
|
|
||||||
# returns 404, which is what broke this step the first time it ran after the
|
|
||||||
# flip. The dual-write was never independent of the cutover precisely because it
|
|
||||||
# addressed the hostname being cut over.
|
|
||||||
#
|
|
||||||
# This upload exists only to keep ota1 carrying current bundles so a rollback of
|
|
||||||
# the Bunny origin remains useful. It goes away with this whole script when ota1
|
|
||||||
# is decommissioned (Phase 5).
|
|
||||||
OTA1_ORIGIN="${OTA1_ORIGIN:-https://ota1.us-east.updates.bsky.network}"
|
|
||||||
DEPLOYMENT_URL="$OTA1_ORIGIN/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
|
|
||||||
|
|
||||||
tar czvf bundle.tar.gz ./*
|
|
||||||
|
|
||||||
echo "Deploying to $DEPLOYMENT_URL..."
|
|
||||||
echo " runtime-version: $RUNTIME_VERSION"
|
|
||||||
echo " bundle-version: $BUNDLE_VERSION"
|
|
||||||
echo " channel: $CHANNEL_NAME"
|
|
||||||
echo " ios-build-number: $BSKY_IOS_BUILD_NUMBER"
|
|
||||||
echo " android-build-number: $BSKY_ANDROID_VERSION_CODE"
|
|
||||||
|
|
||||||
curl --fail-with-body -o - --form "bundle=@./bundle.tar.gz" --user "bsky:$DENIS_API_KEY" --basic "$DEPLOYMENT_URL"
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
rm -rf bundleTempDir
|
|
||||||
rm -rf bundle.tar.gz
|
|
||||||
+2
-12
@@ -4,8 +4,7 @@ set -o pipefail
|
|||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
# Publishes the just-exported Expo bundle to the denis OTA service (S3) via the
|
# Publishes the just-exported Expo bundle to the denis OTA service (S3) via the
|
||||||
# `denis publish` CLI. Mirrors bundleUpdate.sh's inputs (runtime version, bundle
|
# `denis publish` CLI.
|
||||||
# version, build numbers) but targets denis instead of the legacy ota1 upload.
|
|
||||||
# Expects: the `denis` binary on PATH (setup-denis action), ambient AWS creds
|
# Expects: the `denis` binary on PATH (setup-denis action), ambient AWS creds
|
||||||
# (configure-aws-credentials OIDC), and BSKY_IOS_BUILD_NUMBER /
|
# (configure-aws-credentials OIDC), and BSKY_IOS_BUILD_NUMBER /
|
||||||
# BSKY_ANDROID_VERSION_CODE from the use-build-number wrapper.
|
# BSKY_ANDROID_VERSION_CODE from the use-build-number wrapper.
|
||||||
@@ -19,16 +18,7 @@ if [ -z "$RUNTIME_VERSION" ]; then
|
|||||||
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
|
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Accept a caller-supplied bundle version so that a dual-write publishes the SAME
|
BUNDLE_VERSION=$(date +%s)
|
||||||
# version to every origin. When this script and bundleUpdate.sh each called
|
|
||||||
# `date +%s` independently they produced versions seconds apart for identical
|
|
||||||
# bytes -- observed 1785102575 (denis) vs 1785102614 (ota1) for one commit. Since
|
|
||||||
# the version is part of the asset URL path, the two origins then served
|
|
||||||
# manifests pointing at paths only one of them had, so the manifest and its
|
|
||||||
# assets had to come from the same origin or the fetch 404s. Falling back to
|
|
||||||
# `date +%s` keeps standalone callers (PR previews, `pnpm make-deploy-bundle`)
|
|
||||||
# working unchanged.
|
|
||||||
BUNDLE_VERSION="${BUNDLE_VERSION:-$(date +%s)}"
|
|
||||||
DENIS_CDN_DOMAIN="${DENIS_CDN_DOMAIN:-updates.bsky.app}"
|
DENIS_CDN_DOMAIN="${DENIS_CDN_DOMAIN:-updates.bsky.app}"
|
||||||
DENIS_S3_BUCKET="${DENIS_S3_BUCKET:-bsky-denis-ota-prod}"
|
DENIS_S3_BUCKET="${DENIS_S3_BUCKET:-bsky-denis-ota-prod}"
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -87,6 +87,7 @@ import {MessagesJoinRequestsScreen} from '#/screens/Messages/JoinRequests'
|
|||||||
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||||
import {ModerationScreen} from '#/screens/Moderation'
|
import {ModerationScreen} from '#/screens/Moderation'
|
||||||
import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
|
import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
|
||||||
|
import {ModerationInboxScreen} from '#/screens/ModerationInbox'
|
||||||
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
||||||
import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'
|
import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'
|
||||||
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
||||||
@@ -178,6 +179,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => ModerationScreen}
|
getComponent={() => ModerationScreen}
|
||||||
options={{title: title(msg`Moderation`), requireAuth: true}}
|
options={{title: title(msg`Moderation`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ModerationInbox"
|
||||||
|
getComponent={() => ModerationInboxScreen}
|
||||||
|
options={{title: title(msg`Moderation inbox`), requireAuth: true}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ModerationModlists"
|
name="ModerationModlists"
|
||||||
getComponent={() => ModerationModlistsScreen}
|
getComponent={() => ModerationModlistsScreen}
|
||||||
@@ -919,7 +925,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
// chat-removed-from-group, chat-join-request-rejected: the convo is
|
// chat-removed-from-group, chat-join-request-rejected: the convo is
|
||||||
// no longer accessible to the recipient, so just open the list.
|
// no longer accessible to the recipient, so just open the list.
|
||||||
// @ts-expect-error nested navigators aren't typed -sfn
|
// @ts-expect-error nested navigators aren't typed -sfn
|
||||||
navigate('MessagesTab', {screen: 'Messages'})
|
void navigate('MessagesTab', {screen: 'Messages'})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1006,7 +1012,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
ax.metric('init', {
|
ax.metric('init', {
|
||||||
initMs: Math.round(
|
initMs: Math.round(
|
||||||
// @ts-ignore Emitted by Metro in the bundle prelude
|
// @ts-expect-error Emitted by Metro in the bundle prelude
|
||||||
performance.now() - global.__BUNDLE_START_TIME__,
|
performance.now() - global.__BUNDLE_START_TIME__,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -1081,7 +1087,7 @@ function navigate<K extends keyof AllNavigatorParams>(
|
|||||||
}
|
}
|
||||||
navigationRef.addListener('state', handler)
|
navigationRef.addListener('state', handler)
|
||||||
|
|
||||||
// @ts-ignore I don't know what would make typescript happy but I have a life -prf
|
// @ts-expect-error I don't know what would make typescript happy but I have a life -prf
|
||||||
navigationRef.navigate(name, params)
|
navigationRef.navigate(name, params)
|
||||||
}),
|
}),
|
||||||
timeout(1e3),
|
timeout(1e3),
|
||||||
|
|||||||
+6
-5
@@ -20,14 +20,15 @@ import * as SplashScreen from 'expo-splash-screen'
|
|||||||
|
|
||||||
import {Logotype} from '#/view/icons/Logotype'
|
import {Logotype} from '#/view/icons/Logotype'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
import splashImagePointer from '../assets/splash/splash.png'
|
import splashImagePointer from '../assets/splash/splash.png'
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
import darkSplashImagePointer from '../assets/splash/splash-dark.png'
|
import darkSplashImagePointer from '../assets/splash/splash-dark.png'
|
||||||
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
|
||||||
|
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer)!.uri
|
||||||
const darkSplashImageUri = RNImage.resolveAssetSource(
|
const darkSplashImageUri = RNImage.resolveAssetSource(
|
||||||
darkSplashImagePointer,
|
darkSplashImagePointer,
|
||||||
).uri
|
)!.uri
|
||||||
|
|
||||||
export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
|
export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
|
||||||
const width = 1000
|
const width = 1000
|
||||||
@@ -35,7 +36,7 @@ export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
|
|||||||
return (
|
return (
|
||||||
<Svg
|
<Svg
|
||||||
fill="none"
|
fill="none"
|
||||||
// @ts-ignore it's fiiiiine
|
// @ts-expect-error it's fiiiiine
|
||||||
ref={ref}
|
ref={ref}
|
||||||
viewBox="0 0 64 66"
|
viewBox="0 0 64 66"
|
||||||
style={[{width, height}, props.style]}>
|
style={[{width, height}, props.style]}>
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@
|
|||||||
import {useEffect, useRef, useState} from 'react'
|
import {useEffect, useRef, useState} from 'react'
|
||||||
import Svg, {Path} from 'react-native-svg'
|
import Svg, {Path} from 'react-native-svg'
|
||||||
|
|
||||||
import {atoms as a, flatten} from '#/alf'
|
import {atoms as a, flattenToCSS} from '#/alf'
|
||||||
|
|
||||||
const size = 100
|
const size = 100
|
||||||
const ratio = 57 / 64
|
const ratio = 57 / 64
|
||||||
@@ -72,7 +72,7 @@ export function Splash({
|
|||||||
{!isAnimationComplete && (
|
{!isAnimationComplete && (
|
||||||
<div
|
<div
|
||||||
ref={splashRef}
|
ref={splashRef}
|
||||||
style={flatten([
|
style={flattenToCSS([
|
||||||
a.fixed,
|
a.fixed,
|
||||||
a.inset_0,
|
a.inset_0,
|
||||||
a.flex,
|
a.flex,
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ export function RedirectOverlay() {
|
|||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
gtMobile ? a.p_2xl : a.p_xl,
|
gtMobile ? a.p_2xl : a.p_xl,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
// @ts-ignore
|
|
||||||
platform({
|
platform({
|
||||||
web: {
|
web: {
|
||||||
paddingTop: '35vh',
|
paddingTop: '35vh',
|
||||||
|
|||||||
+6
-1
@@ -3,6 +3,8 @@ import {type FontVariant, type TextStyle} from 'react-native'
|
|||||||
import {IS_ANDROID, IS_WEB} from '#/env'
|
import {IS_ANDROID, IS_WEB} from '#/env'
|
||||||
import {type Device, device} from '#/storage'
|
import {type Device, device} from '#/storage'
|
||||||
|
|
||||||
|
export type MutableTextStyle = {-readonly [K in keyof TextStyle]: TextStyle[K]}
|
||||||
|
|
||||||
const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
|
const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
|
||||||
|
|
||||||
const factor = 0.0625 // 1 - (15/16)
|
const factor = 0.0625 // 1 - (15/16)
|
||||||
@@ -37,7 +39,10 @@ export function setFontFamily(fontFamily: Device['fontFamily']) {
|
|||||||
/*
|
/*
|
||||||
* Unused fonts are commented out, but the files are there if we need them.
|
* Unused fonts are commented out, but the files are there if we need them.
|
||||||
*/
|
*/
|
||||||
export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
|
export function applyFonts(
|
||||||
|
style: MutableTextStyle,
|
||||||
|
fontFamily: 'system' | 'theme',
|
||||||
|
) {
|
||||||
if (fontFamily === 'theme') {
|
if (fontFamily === 'theme') {
|
||||||
if (IS_ANDROID) {
|
if (IS_ANDROID) {
|
||||||
style.fontFamily =
|
style.fontFamily =
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import {
|
|||||||
import {UITextView} from '@bsky.app/react-native-uitextview'
|
import {UITextView} from '@bsky.app/react-native-uitextview'
|
||||||
import createEmojiRegex from 'emoji-regex'
|
import createEmojiRegex from 'emoji-regex'
|
||||||
|
|
||||||
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
|
import {
|
||||||
|
type Alf,
|
||||||
|
applyFonts,
|
||||||
|
atoms,
|
||||||
|
flatten,
|
||||||
|
type MutableTextStyle,
|
||||||
|
} from '#/alf'
|
||||||
import {IS_IOS, IS_NATIVE} from '#/env'
|
import {IS_IOS, IS_NATIVE} from '#/env'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +33,7 @@ export function normalizeTextStyles(
|
|||||||
fontFamily: Alf['fonts']['family']
|
fontFamily: Alf['fonts']['family']
|
||||||
} & Pick<Alf, 'flags'>,
|
} & Pick<Alf, 'flags'>,
|
||||||
) {
|
) {
|
||||||
const s = flatten(styles) ?? {}
|
const s: MutableTextStyle = {...flatten(styles)}
|
||||||
|
|
||||||
// should always be defined on these components
|
// should always be defined on these components
|
||||||
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
|
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import {Dimensions} from 'react-native'
|
import {Dimensions, type DimensionsPayload} from 'react-native'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as `useWindowDimensions().fontScale`, but avoids rerendering
|
* Same as `useWindowDimensions().fontScale`, but avoids rerendering
|
||||||
@@ -9,9 +9,12 @@ export function useNativeFontScale() {
|
|||||||
const [fontScale, setFontScale] = useState(Dimensions.get('window').fontScale)
|
const [fontScale, setFontScale] = useState(Dimensions.get('window').fontScale)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = Dimensions.addEventListener('change', evt => {
|
const sub = Dimensions.addEventListener(
|
||||||
setFontScale(evt.window.fontScale)
|
'change',
|
||||||
})
|
(evt: DimensionsPayload) => {
|
||||||
|
if (evt.window) setFontScale(evt.window.fontScale)
|
||||||
|
},
|
||||||
|
)
|
||||||
return () => sub.remove()
|
return () => sub.remove()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -1,6 +1,19 @@
|
|||||||
import {type DimensionValue, StyleSheet} from 'react-native'
|
import {type DimensionValue, type StyleProp, StyleSheet} from 'react-native'
|
||||||
|
|
||||||
export const flatten = StyleSheet.flatten
|
export function flatten<T>(
|
||||||
|
style?: StyleProp<T>,
|
||||||
|
): T extends (infer U)[] ? U : T {
|
||||||
|
return (StyleSheet.flatten(
|
||||||
|
style as unknown as Parameters<typeof StyleSheet.flatten>[0],
|
||||||
|
) ?? {}) as T extends (infer U)[] ? U : T
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flatten React Native styles passed directly to a web-only DOM component. */
|
||||||
|
export function flattenToCSS(style: unknown): React.CSSProperties {
|
||||||
|
return (StyleSheet.flatten(
|
||||||
|
style as Parameters<typeof StyleSheet.flatten>[0],
|
||||||
|
) ?? {}) as React.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coerce a style value to a number. Padding values are typed as
|
* Coerce a style value to a number. Padding values are typed as
|
||||||
@@ -28,7 +41,7 @@ interface PaddingStyle {
|
|||||||
* non-numeric `DimensionValue` (e.g. percentages) is treated as 0.
|
* non-numeric `DimensionValue` (e.g. percentages) is treated as 0.
|
||||||
*/
|
*/
|
||||||
export function extractPadding(style: PaddingStyle | PaddingStyle[]) {
|
export function extractPadding(style: PaddingStyle | PaddingStyle[]) {
|
||||||
const s = flatten(style as any) ?? {}
|
const s = flatten(style)
|
||||||
const base = num(s.padding)
|
const base = num(s.padding)
|
||||||
return {
|
return {
|
||||||
paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base,
|
paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function useThemeName(): ThemeName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getThemeName(
|
function getThemeName(
|
||||||
colorScheme: ColorSchemeName,
|
colorScheme: ColorSchemeName | null | undefined,
|
||||||
colorMode: 'system' | 'light' | 'dark',
|
colorMode: 'system' | 'light' | 'dark',
|
||||||
darkTheme?: ThemeName,
|
darkTheme?: ThemeName,
|
||||||
) {
|
) {
|
||||||
@@ -39,11 +39,8 @@ function getThemeName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDocument(theme: ThemeName) {
|
function updateDocument(theme: ThemeName) {
|
||||||
// @ts-ignore web only
|
|
||||||
if (IS_WEB && typeof window !== 'undefined') {
|
if (IS_WEB && typeof window !== 'undefined') {
|
||||||
// @ts-ignore web only
|
|
||||||
const html = window.document.documentElement
|
const html = window.document.documentElement
|
||||||
// @ts-ignore web only
|
|
||||||
const meta = window.document.querySelector('meta[name="theme-color"]')
|
const meta = window.document.querySelector('meta[name="theme-color"]')
|
||||||
|
|
||||||
// remove any other color mode classes
|
// remove any other color mode classes
|
||||||
|
|||||||
@@ -77,22 +77,6 @@ export function getFeatures() {
|
|||||||
|
|
||||||
export function getFeatureDescription(feature: Features, i18n: I18n) {
|
export function getFeatureDescription(feature: Features, i18n: I18n) {
|
||||||
switch (feature) {
|
switch (feature) {
|
||||||
case Features.VideoAllow10MinuteEnable:
|
|
||||||
return {
|
|
||||||
key: feature,
|
|
||||||
name: i18n._(
|
|
||||||
msg({
|
|
||||||
message: 'Longer videos',
|
|
||||||
comment: 'Name for a feature flag (longer videos)',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
description: i18n._(
|
|
||||||
msg({
|
|
||||||
message: 'Enable 10-minute video uploads.',
|
|
||||||
comment: 'Description of a feature flag (10-minute video uploads)',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
case Features.CanonicalPostNumberingEnable:
|
case Features.CanonicalPostNumberingEnable:
|
||||||
return {
|
return {
|
||||||
key: feature,
|
key: feature,
|
||||||
|
|||||||
@@ -17,15 +17,13 @@ export enum Features {
|
|||||||
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
||||||
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
||||||
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
||||||
PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable',
|
|
||||||
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
||||||
VideoAllow10MinuteEnable = 'video:allow-10-minute:enable',
|
|
||||||
VideoMultipartUploadEnable = 'video:multipart_upload:enable',
|
|
||||||
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
||||||
FollowSortEnable = 'follow_sort:enable',
|
FollowSortEnable = 'follow_sort:enable',
|
||||||
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
|
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
|
||||||
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
|
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
|
||||||
ContentVisibilitySettingsEnable = 'content_visibility_settings:enable',
|
ContentVisibilitySettingsEnable = 'content_visibility_settings:enable',
|
||||||
|
ModerationInboxEnable = 'moderation_inbox:enable',
|
||||||
|
|
||||||
// values
|
// values
|
||||||
TrendingDiscoverValues = 'trending_discover:values',
|
TrendingDiscoverValues = 'trending_discover:values',
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ import {Platform} from 'react-native'
|
|||||||
import {type Result, type WidenPrimitives} from '@growthbook/growthbook-react'
|
import {type Result, type WidenPrimitives} from '@growthbook/growthbook-react'
|
||||||
|
|
||||||
import {Logger} from '#/logger'
|
import {Logger} from '#/logger'
|
||||||
import {
|
|
||||||
getCachedIsBetaUser,
|
|
||||||
subscribeToCachedIsBetaUser,
|
|
||||||
} from '#/state/preferences/beta-user-cache'
|
|
||||||
import {
|
import {
|
||||||
Features,
|
Features,
|
||||||
features as feats,
|
features as feats,
|
||||||
@@ -37,7 +33,7 @@ import {type Metrics, metrics} from '#/analytics/metrics'
|
|||||||
import * as refParams from '#/analytics/misc/refParams'
|
import * as refParams from '#/analytics/misc/refParams'
|
||||||
import * as env from '#/env'
|
import * as env from '#/env'
|
||||||
import {useGeolocationServiceResponse} from '#/geolocation/service'
|
import {useGeolocationServiceResponse} from '#/geolocation/service'
|
||||||
import {device} from '#/storage'
|
import {account, device} from '#/storage'
|
||||||
|
|
||||||
export * as utils from '#/analytics/utils'
|
export * as utils from '#/analytics/utils'
|
||||||
export const features = {init, refresh}
|
export const features = {init, refresh}
|
||||||
@@ -135,7 +131,7 @@ export const setupDeviceId = getAndMigrateDeviceId()
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the per-account cached `isBetaUser` flag for `did`, kept in sync with
|
* Reads the per-account cached `isBetaUser` flag for `did`, kept in sync with
|
||||||
* PDS preference query results and the beta settings toggle.
|
* writes from `BetaUserStorageSync` and the beta settings toggle.
|
||||||
*
|
*
|
||||||
* This deliberately does not use `useStorage`, whose `useState` seeds once and
|
* This deliberately does not use `useStorage`, whose `useState` seeds once and
|
||||||
* only updates via the change listener. The consuming `AnalyticsContext` lives
|
* only updates via the change listener. The consuming `AnalyticsContext` lives
|
||||||
@@ -150,13 +146,17 @@ function useAccountIsBetaUser(did: string | undefined): boolean | undefined {
|
|||||||
const subscribe = useCallback(
|
const subscribe = useCallback(
|
||||||
(onChange: () => void) => {
|
(onChange: () => void) => {
|
||||||
if (!did) return () => {}
|
if (!did) return () => {}
|
||||||
return subscribeToCachedIsBetaUser(did, onChange)
|
const sub = account.addOnValueChangedListener(
|
||||||
|
[did, 'isBetaUser'],
|
||||||
|
onChange,
|
||||||
|
)
|
||||||
|
return () => sub.remove()
|
||||||
},
|
},
|
||||||
[did],
|
[did],
|
||||||
)
|
)
|
||||||
const getSnapshot = useCallback(() => {
|
const getSnapshot = useCallback(() => {
|
||||||
if (!did) return undefined
|
if (!did) return undefined
|
||||||
return getCachedIsBetaUser(did)
|
return account.get([did, 'isBetaUser'])
|
||||||
}, [did])
|
}, [did])
|
||||||
return useSyncExternalStore(subscribe, getSnapshot)
|
return useSyncExternalStore(subscribe, getSnapshot)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,7 @@
|
|||||||
import {type Platform} from 'react-native'
|
import {type Platform} from 'react-native'
|
||||||
|
|
||||||
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
||||||
import {
|
import {type VideoCompressSkipReason} from '#/lib/media/video/types'
|
||||||
type VideoCompressSkipReason,
|
|
||||||
type VideoUploadTransport,
|
|
||||||
} from '#/lib/media/video/types'
|
|
||||||
import {type NotificationType} from '#/state/queries/notifications/types'
|
import {type NotificationType} from '#/state/queries/notifications/types'
|
||||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||||
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
||||||
@@ -1473,7 +1470,6 @@ export type Events = {
|
|||||||
bytes: number
|
bytes: number
|
||||||
elapsedMs: number
|
elapsedMs: number
|
||||||
throughputBytesPerSec: number
|
throughputBytesPerSec: number
|
||||||
transport: VideoUploadTransport
|
|
||||||
}
|
}
|
||||||
'video:upload:uploadFailed': {
|
'video:upload:uploadFailed': {
|
||||||
uploadId: string
|
uploadId: string
|
||||||
@@ -1481,7 +1477,6 @@ export type Events = {
|
|||||||
bytes: number
|
bytes: number
|
||||||
errorClass: string
|
errorClass: string
|
||||||
elapsedMs: number
|
elapsedMs: number
|
||||||
transport: VideoUploadTransport
|
|
||||||
}
|
}
|
||||||
'video:upload:processingStarted': {
|
'video:upload:processingStarted': {
|
||||||
uploadId: string
|
uploadId: string
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
export function useMeta(metadata?: MergeableMetadata) {
|
export function useMeta(metadata?: MergeableMetadata) {
|
||||||
const m = useMemo(() => metadata, [metadata])
|
const m = useMemo(() => metadata, [metadata])
|
||||||
if (!m) return
|
if (!m) return
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
m.__meta = true
|
m.__meta = true
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {Pressable} from 'react-native'
|
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Pressable} from '#/components/Pressable'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export function AltBadgeWithDialog({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
hitSlop={HITSLOP_20}
|
hitSlop={HITSLOP_20}
|
||||||
onPress={control.open}
|
onPress={control.open}
|
||||||
style={s => [
|
style={({pressed, hovered}) => [
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
a.p_xs,
|
a.p_xs,
|
||||||
@@ -62,7 +62,7 @@ export function AltBadgeWithDialog({
|
|||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
pos,
|
pos,
|
||||||
s.hovered || s.pressed
|
hovered || pressed
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {type Insets, Pressable, View} from 'react-native'
|
import {type Insets, View} from 'react-native'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
|
import {Pressable} from '#/components/Pressable'
|
||||||
import * as Tooltip from '#/components/Tooltip'
|
import * as Tooltip from '#/components/Tooltip'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export type ButtonProps = Pick<
|
|||||||
| 'onPressOut'
|
| 'onPressOut'
|
||||||
| 'onFocus'
|
| 'onFocus'
|
||||||
| 'onBlur'
|
| 'onBlur'
|
||||||
|
| 'onAccessibilityAction'
|
||||||
|
| 'onAccessibilityEscape'
|
||||||
> &
|
> &
|
||||||
AccessibilityProps &
|
AccessibilityProps &
|
||||||
VariantProps & {
|
VariantProps & {
|
||||||
@@ -131,7 +133,7 @@ export function useButtonContext() {
|
|||||||
return useContext(Context)
|
return useContext(Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = forwardRef<View, ButtonProps>(
|
export const Button = forwardRef<React.ComponentRef<typeof View>, ButtonProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
@@ -575,7 +577,6 @@ export const Button = forwardRef<View, ButtonProps>(
|
|||||||
role="button"
|
role="button"
|
||||||
accessibilityHint={undefined} // optional
|
accessibilityHint={undefined} // optional
|
||||||
{...rest}
|
{...rest}
|
||||||
// @ts-ignore - this will always be a pressable
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-pressed={state.pressed}
|
aria-pressed={state.pressed}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ import {
|
|||||||
import {Span, Text} from '#/components/Typography'
|
import {Span, Text} from '#/components/Typography'
|
||||||
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||||
|
|
||||||
|
type TextInputInstance = React.ComponentRef<typeof TextInput>
|
||||||
|
type ViewInstance = React.ComponentRef<typeof View>
|
||||||
|
|
||||||
export type SubmitRequest =
|
export type SubmitRequest =
|
||||||
| {
|
| {
|
||||||
platform: 'web'
|
platform: 'web'
|
||||||
@@ -60,7 +63,7 @@ export type ComposerInternalApi = {
|
|||||||
input?: ReturnType<typeof useTapper>['input']
|
input?: ReturnType<typeof useTapper>['input']
|
||||||
clear: () => void
|
clear: () => void
|
||||||
insert(text: string): void
|
insert(text: string): void
|
||||||
setAutocompleteAnchor: (node: View | null) => void
|
setAutocompleteAnchor: (node: ViewInstance | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useComposerInternalApiRef() {
|
export function useComposerInternalApiRef() {
|
||||||
@@ -82,7 +85,7 @@ export type ComposerProps = Omit<
|
|||||||
| 'onSubmitEditing'
|
| 'onSubmitEditing'
|
||||||
> & {
|
> & {
|
||||||
label: string
|
label: string
|
||||||
ref?: React.RefObject<TextInput>
|
ref?: React.RefObject<TextInputInstance>
|
||||||
internalApiRef?: React.Ref<ComposerInternalApi>
|
internalApiRef?: React.Ref<ComposerInternalApi>
|
||||||
outerStyle?: ViewStyleProp['style']
|
outerStyle?: ViewStyleProp['style']
|
||||||
contentTextStyle?: TextStyleProp['style']
|
contentTextStyle?: TextStyleProp['style']
|
||||||
@@ -139,6 +142,11 @@ export function Composer({
|
|||||||
placement: autocompletePlacement,
|
placement: autocompletePlacement,
|
||||||
dynamicWidth: IS_WEB,
|
dynamicWidth: IS_WEB,
|
||||||
})
|
})
|
||||||
|
const inputRef = mergeRefs<TextInputInstance>([
|
||||||
|
ref,
|
||||||
|
tapper.inputProps.ref as React.Ref<TextInputInstance>,
|
||||||
|
sift.targetProps.ref as React.Ref<TextInputInstance>,
|
||||||
|
])
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Active facet state for controlling the visibility of the Autocomplete.
|
* Active facet state for controlling the visibility of the Autocomplete.
|
||||||
@@ -306,7 +314,7 @@ export function Composer({
|
|||||||
style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]}
|
style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]}
|
||||||
ref={node => {
|
ref={node => {
|
||||||
if (IS_WEB && node) {
|
if (IS_WEB && node) {
|
||||||
// @ts-ignore web only a11y
|
// @ts-expect-error web only a11y
|
||||||
node.setAttribute('inert', '')
|
node.setAttribute('inert', '')
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
@@ -345,7 +353,7 @@ export function Composer({
|
|||||||
{...rest}
|
{...rest}
|
||||||
{...tapper.inputProps}
|
{...tapper.inputProps}
|
||||||
{...sift.targetProps}
|
{...sift.targetProps}
|
||||||
ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])}
|
ref={inputRef}
|
||||||
rawValue={tapper.state.text}
|
rawValue={tapper.state.text}
|
||||||
onBlur={e => {
|
onBlur={e => {
|
||||||
rest.onBlur?.(e)
|
rest.onBlur?.(e)
|
||||||
@@ -359,11 +367,10 @@ export function Composer({
|
|||||||
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
|
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// @ts-ignore web only
|
// @ts-expect-error web only
|
||||||
onCompositionStart={() => {
|
onCompositionStart={() => {
|
||||||
isComposing.current = true
|
isComposing.current = true
|
||||||
}}
|
}}
|
||||||
// @ts-ignore web only
|
|
||||||
onCompositionEnd={() => {
|
onCompositionEnd={() => {
|
||||||
isComposing.current = false
|
isComposing.current = false
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export function Trigger({
|
|||||||
const context = useContextMenuContext()
|
const context = useContextMenuContext()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const ref = useRef<View>(null)
|
const ref = useRef<React.ComponentRef<typeof View>>(null)
|
||||||
const isFocused = useIsFocused()
|
const isFocused = useIsFocused()
|
||||||
const [image, setImage] = useState<string | null>(null)
|
const [image, setImage] = useState<string | null>(null)
|
||||||
const [pendingMeasurement, setPendingMeasurement] = useState<{
|
const [pendingMeasurement, setPendingMeasurement] = useState<{
|
||||||
@@ -971,7 +971,10 @@ export function Divider() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function measureView(view: View | null, insets: EdgeInsets) {
|
function measureView(
|
||||||
|
view: React.ComponentRef<typeof View> | null,
|
||||||
|
insets: EdgeInsets,
|
||||||
|
) {
|
||||||
if (!view) return Promise.resolve(null)
|
if (!view) return Promise.resolve(null)
|
||||||
return new Promise<Measurement>(resolve => {
|
return new Promise<Measurement>(resolve => {
|
||||||
view?.measureInWindow((x, y, width, height) =>
|
view?.measureInWindow((x, y, width, height) =>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
Keyboard,
|
Keyboard,
|
||||||
type KeyboardEventListener,
|
type KeyboardEvent,
|
||||||
type LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
type NativeScrollEvent,
|
type NativeScrollEvent,
|
||||||
type NativeSyntheticEvent,
|
type NativeSyntheticEvent,
|
||||||
@@ -203,80 +203,85 @@ export function Inner(props: DialogInnerProps) {
|
|||||||
return <ScrollableInner {...props} />
|
return <ScrollableInner {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>(
|
export function ScrollableInner({
|
||||||
function ScrollableInner(
|
ref,
|
||||||
{children, contentContainerStyle, header, footer, style, ...props},
|
children,
|
||||||
ref,
|
contentContainerStyle,
|
||||||
) {
|
header,
|
||||||
const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} =
|
footer,
|
||||||
useDialogContext()
|
style,
|
||||||
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
|
...props
|
||||||
const insets = useSafeAreaInsets()
|
}: DialogInnerProps & {
|
||||||
const [keyboardHeight, setKeyboardHeight] = useState(() =>
|
ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
|
||||||
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
|
}) {
|
||||||
)
|
const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} =
|
||||||
|
useDialogContext()
|
||||||
|
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const [keyboardHeight, setKeyboardHeight] = useState(() =>
|
||||||
|
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
|
||||||
|
)
|
||||||
|
|
||||||
const keyboardEventHandler = useCallback<KeyboardEventListener>(e => {
|
const keyboardEventHandler = useCallback((e: KeyboardEvent) => {
|
||||||
setKeyboardHeight(e.endCoordinates.height)
|
setKeyboardHeight(e.endCoordinates.height)
|
||||||
}, [])
|
}, [])
|
||||||
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
|
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
|
||||||
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
|
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
|
||||||
|
|
||||||
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
if (!IS_ANDROID) {
|
if (!IS_ANDROID) {
|
||||||
return
|
return
|
||||||
}
|
|
||||||
const {contentOffset} = e.nativeEvent
|
|
||||||
if (contentOffset.y > 0 && !disableDrag) {
|
|
||||||
setDisableDrag(true)
|
|
||||||
} else if (contentOffset.y <= 1 && disableDrag) {
|
|
||||||
setDisableDrag(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const {contentOffset} = e.nativeEvent
|
||||||
|
if (contentOffset.y > 0 && !disableDrag) {
|
||||||
|
setDisableDrag(true)
|
||||||
|
} else if (contentOffset.y <= 1 && disableDrag) {
|
||||||
|
setDisableDrag(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={[isHeightConstrained && a.flex_1, style]}
|
style={[isHeightConstrained && a.flex_1, style]}
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
a.pt_2xl,
|
a.pt_2xl,
|
||||||
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
|
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
|
||||||
platform({
|
platform({
|
||||||
ios: a.pb_2xl,
|
ios: a.pb_2xl,
|
||||||
android: {
|
android: {
|
||||||
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
|
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
]}
|
]}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
|
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
|
||||||
contentInsetAdjustmentBehavior={
|
contentInsetAdjustmentBehavior={
|
||||||
isAtMaxSnapPoint ? 'automatic' : 'never'
|
isAtMaxSnapPoint ? 'automatic' : 'never'
|
||||||
}
|
}
|
||||||
automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint}
|
automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint}
|
||||||
{...props}
|
{...props}
|
||||||
bounces={isAtMaxSnapPoint}
|
bounces={isAtMaxSnapPoint}
|
||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
// set drag state based on scroll on android.
|
// set drag state based on scroll on android.
|
||||||
// we want to detect if it's at the top or not, so watch
|
// we want to detect if it's at the top or not, so watch
|
||||||
// scrollEndDrag and momentumScrollEnd as well
|
// scrollEndDrag and momentumScrollEnd as well
|
||||||
onScroll={android(onScroll)}
|
onScroll={android(onScroll)}
|
||||||
onScrollEndDrag={android(onScroll)}
|
onScrollEndDrag={android(onScroll)}
|
||||||
onMomentumScrollEnd={android(onScroll)}
|
onMomentumScrollEnd={android(onScroll)}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
||||||
// on Android. fine to disable for now, because we don't have any
|
// on Android. fine to disable for now, because we don't have any
|
||||||
// dialogs that use this that actually scroll -sfn
|
// dialogs that use this that actually scroll -sfn
|
||||||
stickyHeaderIndices={ios(header ? [0] : undefined)}>
|
stickyHeaderIndices={ios(header ? [0] : undefined)}>
|
||||||
{header}
|
{header}
|
||||||
{children}
|
{children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{footer}
|
{footer}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const InnerFlatList = forwardRef<
|
export const InnerFlatList = forwardRef<
|
||||||
ListMethods,
|
ListMethods,
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ export function Inner({
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-labelledby={accessibilityLabelledBy}
|
aria-labelledby={accessibilityLabelledBy}
|
||||||
aria-describedby={accessibilityDescribedBy}
|
aria-describedby={accessibilityDescribedBy}
|
||||||
// @ts-expect-error web only -prf
|
|
||||||
onClick={stopPropagation}
|
onClick={stopPropagation}
|
||||||
onStartShouldSetResponder={_ => true}
|
onStartShouldSetResponder={_ => true}
|
||||||
onTouchEnd={stopPropagation}
|
onTouchEnd={stopPropagation}
|
||||||
@@ -239,7 +238,9 @@ export function Inner({
|
|||||||
export function ScrollableInner({
|
export function ScrollableInner({
|
||||||
ref: _ref,
|
ref: _ref,
|
||||||
...props
|
...props
|
||||||
}: DialogInnerProps & {ref?: React.Ref<ScrollView>}) {
|
}: DialogInnerProps & {
|
||||||
|
ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
|
||||||
|
}) {
|
||||||
return <Inner {...props} />
|
return <Inner {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export function ProfileGrid({
|
|||||||
|
|
||||||
// Track seen profiles
|
// Track seen profiles
|
||||||
const seenProfilesRef = useRef<Set<string>>(new Set())
|
const seenProfilesRef = useRef<Set<string>>(new Set())
|
||||||
const containerRef = useRef<View>(null)
|
const containerRef = useRef<React.ComponentRef<typeof View>>(null)
|
||||||
const hasTrackedRef = useRef(false)
|
const hasTrackedRef = useRef(false)
|
||||||
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
|
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
|
||||||
? 'DiscoverInterstitial'
|
? 'DiscoverInterstitial'
|
||||||
@@ -276,7 +276,7 @@ export function ProfileGrid({
|
|||||||
},
|
},
|
||||||
{threshold: 0.5},
|
{threshold: 0.5},
|
||||||
)
|
)
|
||||||
// @ts-ignore - web only
|
// @ts-expect-error - web only
|
||||||
observer.observe(node)
|
observer.observe(node)
|
||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function FocusScope({children}: {children: React.ReactNode}) {
|
|||||||
*/
|
*/
|
||||||
function FocusTrap({children}: {children: React.ReactNode}) {
|
function FocusTrap({children}: {children: React.ReactNode}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const child = useRef<View>(null)
|
const child = useRef<React.ComponentRef<typeof View>>(null)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Here we add a ref to the first child of this component. This currently
|
* Here we add a ref to the first child of this component. This currently
|
||||||
@@ -66,13 +66,16 @@ function FocusTrap({children}: {children: React.ReactNode}) {
|
|||||||
})
|
})
|
||||||
}, [children])
|
}, [children])
|
||||||
|
|
||||||
const focusNode = useCallback((ref: View | null) => {
|
const focusNode = useCallback(
|
||||||
if (!ref) return
|
(ref: React.ComponentRef<typeof View> | null) => {
|
||||||
const node = findNodeHandle(ref)
|
if (!ref) return
|
||||||
if (node) {
|
const node = findNodeHandle(ref)
|
||||||
AccessibilityInfo.setAccessibilityFocus(node)
|
if (node) {
|
||||||
}
|
AccessibilityInfo.setAccessibilityFocus(node)
|
||||||
}, [])
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const IS_GLASS_AVAILABLE =
|
|||||||
*/
|
*/
|
||||||
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
|
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
|
||||||
|
|
||||||
export type GlassViewProps = ExpoGlassViewProps & {
|
export type GlassViewProps = Omit<ExpoGlassViewProps, 'ref'> & {
|
||||||
fallbackStyle?: StyleProp<ViewStyle>
|
fallbackStyle?: StyleProp<ViewStyle>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function InterestTabs({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const listRef = useRef<ScrollView>(null)
|
const listRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
|
||||||
const [totalWidth, setTotalWidth] = useState(0)
|
const [totalWidth, setTotalWidth] = useState(0)
|
||||||
const [scrollX, setScrollX] = useState(0)
|
const [scrollX, setScrollX] = useState(0)
|
||||||
const [contentWidth, setContentWidth] = useState(0)
|
const [contentWidth, setContentWidth] = useState(0)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function Outer({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
noBottomBorder?: boolean
|
noBottomBorder?: boolean
|
||||||
headerRef?: React.RefObject<View | null>
|
headerRef?: React.RefObject<React.ComponentRef<typeof View> | null>
|
||||||
sticky?: boolean
|
sticky?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|||||||
@@ -446,9 +446,7 @@ function LightboxGalleryItem({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
avi: {
|
avi: {
|
||||||
// @ts-ignore web-only
|
|
||||||
maxWidth: `calc(min(400px, 100vw))`,
|
maxWidth: `calc(min(400px, 100vw))`,
|
||||||
// @ts-ignore web-only
|
|
||||||
maxHeight: `calc(min(400px, 100vh))`,
|
maxHeight: `calc(min(400px, 100vh))`,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
@@ -458,7 +456,6 @@ const styles = StyleSheet.create({
|
|||||||
// column via ScrollView's default flexGrow.
|
// column via ScrollView's default flexGrow.
|
||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
// @ts-ignore web-only -sfn
|
|
||||||
maxHeight: '50vh',
|
maxHeight: '50vh',
|
||||||
},
|
},
|
||||||
menuBtn: {
|
menuBtn: {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const TIMING_OUT = {duration: 150}
|
|||||||
|
|
||||||
export function ImageMenu({onPressShare, onPressSave}: Props) {
|
export function ImageMenu({onPressShare, onPressSave}: Props) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const triggerRef = useRef<View>(null)
|
const triggerRef = useRef<React.ComponentRef<typeof View>>(null)
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
const [anchor, setAnchor] = useState<Anchor | null>(null)
|
const [anchor, setAnchor] = useState<Anchor | null>(null)
|
||||||
const progress = useSharedValue(0)
|
const progress = useSharedValue(0)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const ImageItem = ({
|
|||||||
const scrollHandler = useAnimatedScrollHandler({
|
const scrollHandler = useAnimatedScrollHandler({
|
||||||
onScroll(e) {
|
onScroll(e) {
|
||||||
'worklet'
|
'worklet'
|
||||||
const nextIsScaled = e.zoomScale > 1
|
const nextIsScaled = (e.zoomScale ?? 1) > 1
|
||||||
if (scaled !== nextIsScaled) {
|
if (scaled !== nextIsScaled) {
|
||||||
scheduleOnRN(handleZoom, nextIsScaled)
|
scheduleOnRN(handleZoom, nextIsScaled)
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,6 @@ const ImageItem = ({
|
|||||||
height: number
|
height: number
|
||||||
}) {
|
}) {
|
||||||
const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
|
const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()
|
||||||
// @ts-ignore
|
|
||||||
scrollResponderRef?.scrollResponderZoomTo({
|
scrollResponderRef?.scrollResponderZoomTo({
|
||||||
...nextZoomRect, // This rect is in screen coordinates
|
...nextZoomRect, // This rect is in screen coordinates
|
||||||
animated: true,
|
animated: true,
|
||||||
@@ -218,7 +217,6 @@ const ImageItem = ({
|
|||||||
return (
|
return (
|
||||||
<GestureDetector gesture={composedGesture}>
|
<GestureDetector gesture={composedGesture}>
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
// @ts-ignore Something's up with the types here
|
|
||||||
ref={scrollViewRef}
|
ref={scrollViewRef}
|
||||||
pinchGestureEnabled
|
pinchGestureEnabled
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
|
|||||||
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
|
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||||
import {SystemBars} from 'react-native-edge-to-edge'
|
import {SystemBars} from 'react-native-edge-to-edge'
|
||||||
import {Gesture} from 'react-native-gesture-handler'
|
import {Gesture} from 'react-native-gesture-handler'
|
||||||
import PagerView from 'react-native-pager-view'
|
import PagerView, {
|
||||||
|
type PagerViewOnPageSelectedEvent,
|
||||||
|
type PageScrollStateChangedNativeEvent,
|
||||||
|
} from 'react-native-pager-view'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
type AnimatableValue,
|
type AnimatableValue,
|
||||||
type AnimatedRef,
|
type AnimatedRef,
|
||||||
@@ -383,7 +386,7 @@ function ImageView({
|
|||||||
<PagerView
|
<PagerView
|
||||||
scrollEnabled={!isScaled}
|
scrollEnabled={!isScaled}
|
||||||
initialPage={initialImageIndex}
|
initialPage={initialImageIndex}
|
||||||
onPageSelected={e => {
|
onPageSelected={(e: PagerViewOnPageSelectedEvent) => {
|
||||||
const next = e.nativeEvent.position
|
const next = e.nativeEvent.position
|
||||||
setImageIndex(prev => {
|
setImageIndex(prev => {
|
||||||
if (metricsContext && prev !== next) {
|
if (metricsContext && prev !== next) {
|
||||||
@@ -401,7 +404,7 @@ function ImageView({
|
|||||||
})
|
})
|
||||||
setIsScaled(false)
|
setIsScaled(false)
|
||||||
}}
|
}}
|
||||||
onPageScrollStateChanged={e => {
|
onPageScrollStateChanged={(e: PageScrollStateChangedNativeEvent) => {
|
||||||
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
|
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
|
||||||
}}
|
}}
|
||||||
overdrag={true}
|
overdrag={true}
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ export function InlineLinkText({
|
|||||||
onIn: onInteract,
|
onIn: onInteract,
|
||||||
onOut: onInteractOut,
|
onOut: onInteractOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
const flattenedStyle = flatten(style) || {}
|
const flattenedStyle = flatten(style)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
@@ -570,7 +570,7 @@ export function SimpleInlineLinkText({
|
|||||||
onIn: onInteract,
|
onIn: onInteract,
|
||||||
onOut: onInteractOut,
|
onOut: onInteractOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
const flattenedStyle = flatten(style) || {}
|
const flattenedStyle = flatten(style)
|
||||||
const isExternal = isExternalUrl(to)
|
const isExternal = isExternalUrl(to)
|
||||||
|
|
||||||
let href = to
|
let href = to
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {DropdownMenu} from 'radix-ui'
|
import {DropdownMenu} from 'radix-ui'
|
||||||
|
|
||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
import {atoms as a, flatten, useTheme, web} from '#/alf'
|
import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
|
||||||
import type * as Dialog from '#/components/Dialog'
|
import type * as Dialog from '#/components/Dialog'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {
|
import {
|
||||||
@@ -413,7 +413,7 @@ export function Divider() {
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Separator
|
<DropdownMenu.Separator
|
||||||
style={flatten([
|
style={flattenToCSS([
|
||||||
a.my_xs,
|
a.my_xs,
|
||||||
t.atoms.bg_contrast_100,
|
t.atoms.bg_contrast_100,
|
||||||
a.flex_shrink_0,
|
a.flex_shrink_0,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export type RadixPassThroughTriggerProps = {
|
|||||||
['aria-controls']?: string
|
['aria-controls']?: string
|
||||||
['aria-haspopup']?: boolean
|
['aria-haspopup']?: boolean
|
||||||
['aria-expanded']?: AccessibilityProps['aria-expanded']
|
['aria-expanded']?: AccessibilityProps['aria-expanded']
|
||||||
onKeyDown: (e: React.KeyboardEvent) => void
|
onKeyDown: PressableProps['onKeyDown']
|
||||||
/**
|
/**
|
||||||
* Radix provides this, but we override on web to use `onPress` instead,
|
* Radix provides this, but we override on web to use `onPress` instead,
|
||||||
* which is less sensitive while scrolling.
|
* which is less sensitive while scrolling.
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ function canPlayBskyVideoCodecs(): boolean {
|
|||||||
|
|
||||||
type CachedPromise<T> = Promise<T> & {value: undefined | T}
|
type CachedPromise<T> = Promise<T> & {value: undefined | T}
|
||||||
const promiseForHls = import(
|
const promiseForHls = import(
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
'hls.js/dist/hls.min'
|
'hls.js/dist/hls.min'
|
||||||
// oxlint-disable-next-line typescript/no-unsafe-member-access
|
// oxlint-disable-next-line typescript/no-unsafe-member-access
|
||||||
).then(mod => mod.default) as CachedPromise<typeof HlsTypes.default>
|
).then(mod => mod.default) as CachedPromise<typeof HlsTypes.default>
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ function TranslationResult({
|
|||||||
? codeToLanguageName(resultSourceLanguage, i18n.locale)
|
? codeToLanguageName(resultSourceLanguage, i18n.locale)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const flattenedStyle = flatten(postTextStyle) ?? {}
|
const flattenedStyle = flatten(postTextStyle)
|
||||||
const fontSize = flattenedStyle.fontSize
|
const fontSize = flattenedStyle.fontSize
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function PostControlButton({
|
|||||||
activeColor,
|
activeColor,
|
||||||
...props
|
...props
|
||||||
}: Omit<ButtonProps, 'hitSlop'> & {
|
}: Omit<ButtonProps, 'hitSlop'> & {
|
||||||
ref?: React.Ref<View>
|
ref?: React.Ref<React.ComponentRef<typeof View>>
|
||||||
active?: boolean
|
active?: boolean
|
||||||
big?: boolean
|
big?: boolean
|
||||||
color?: string
|
color?: string
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
Pressable as NativePressable,
|
||||||
|
type PressableStateCallbackType as NativePressableStateCallbackType,
|
||||||
|
type StyleProp,
|
||||||
|
type ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
|
export interface PressableStateCallbackType extends NativePressableStateCallbackType {
|
||||||
|
/** Provided by react-native-web. */
|
||||||
|
readonly focused?: boolean
|
||||||
|
/** Provided by react-native-web. */
|
||||||
|
readonly hovered?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PressableProps = Omit<
|
||||||
|
React.ComponentProps<typeof NativePressable>,
|
||||||
|
'children' | 'style'
|
||||||
|
> & {
|
||||||
|
children?:
|
||||||
|
React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode)
|
||||||
|
style?:
|
||||||
|
| StyleProp<ViewStyle>
|
||||||
|
| ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React Native Pressable with react-native-web's callback state represented in
|
||||||
|
* its types. The web-only fields are optional because native does not provide
|
||||||
|
* them at runtime.
|
||||||
|
*/
|
||||||
|
export function Pressable({children, style, ...props}: PressableProps) {
|
||||||
|
return (
|
||||||
|
<NativePressable {...props} style={style}>
|
||||||
|
{children}
|
||||||
|
</NativePressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -318,11 +318,10 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
// @ts-ignore View is being used as div
|
|
||||||
ref={refs.setReference}
|
ref={refs.setReference}
|
||||||
onPointerMove={onPointerMoveTarget}
|
onPointerMove={onPointerMoveTarget}
|
||||||
onPointerLeave={onPointerLeaveTarget}
|
onPointerLeave={onPointerLeaveTarget}
|
||||||
// @ts-ignore web only prop
|
// @ts-expect-error web only prop
|
||||||
onMouseUp={onPress}
|
onMouseUp={onPress}
|
||||||
style={[a.flex_shrink, props.inline && a.inline]}>
|
style={[a.flex_shrink, props.inline && a.inline]}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import {TextInput, View, type ViewToken} from 'react-native'
|
import {type ListViewToken as ViewToken, TextInput, View} from 'react-native'
|
||||||
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
|||||||
const [searchText, setSearchText] = useState(lastSearchText)
|
const [searchText, setSearchText] = useState(lastSearchText)
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const listRef = useRef<ListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const inputRef = useRef<TextInput>(null)
|
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
const [headerHeight, setHeaderHeight] = useState(0)
|
const [headerHeight, setHeaderHeight] = useState(0)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ let Header = ({
|
|||||||
interestsDisplayNames,
|
interestsDisplayNames,
|
||||||
}: {
|
}: {
|
||||||
guide?: Follow10ProgressGuide
|
guide?: Follow10ProgressGuide
|
||||||
inputRef: React.RefObject<TextInput | null>
|
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||||
listRef: React.RefObject<ListMethods | null>
|
listRef: React.RefObject<ListMethods | null>
|
||||||
onSelectTab: (v: string) => void
|
onSelectTab: (v: string) => void
|
||||||
searchText: string
|
searchText: string
|
||||||
@@ -679,7 +679,7 @@ function SearchInput({
|
|||||||
}: {
|
}: {
|
||||||
onChangeText: (text: string) => void
|
onChangeText: (text: string) => void
|
||||||
onEscape: () => void
|
onEscape: () => void
|
||||||
inputRef: React.RefObject<TextInput | null>
|
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||||
defaultValue: string
|
defaultValue: string
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function RichText({
|
|||||||
|
|
||||||
if (!facets?.length) {
|
if (!facets?.length) {
|
||||||
if (isOnlyEmoji(text)) {
|
if (isOnlyEmoji(text)) {
|
||||||
const flattenedStyle = flatten(style) ?? {}
|
const flattenedStyle = flatten(style)
|
||||||
const fontSize =
|
const fontSize =
|
||||||
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
||||||
return (
|
return (
|
||||||
@@ -104,7 +104,6 @@ export function RichText({
|
|||||||
style={[plainStyles, {fontSize}, suffixStyles]}
|
style={[plainStyles, {fontSize}, suffixStyles]}
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
onTextLayout={onTextLayout}
|
onTextLayout={onTextLayout}
|
||||||
// @ts-ignore web only -prf
|
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}>
|
||||||
{text}
|
{text}
|
||||||
{suffix ? ' ' : null}
|
{suffix ? ' ' : null}
|
||||||
@@ -121,7 +120,6 @@ export function RichText({
|
|||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
onTextLayout={onTextLayout}
|
onTextLayout={onTextLayout}
|
||||||
// @ts-ignore web only -prf
|
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}>
|
||||||
{text}
|
{text}
|
||||||
{suffix ? ' ' : null}
|
{suffix ? ' ' : null}
|
||||||
@@ -150,7 +148,7 @@ export function RichText({
|
|||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
to={`/profile/${mention.did}`}
|
to={`/profile/${mention.did}`}
|
||||||
style={interactiveStyles}
|
style={interactiveStyles}
|
||||||
// @ts-ignore TODO
|
// @ts-expect-error TODO
|
||||||
dataSet={WORD_WRAP}
|
dataSet={WORD_WRAP}
|
||||||
shouldProxy={shouldProxyLinks}
|
shouldProxy={shouldProxyLinks}
|
||||||
onPress={onLinkPress}>
|
onPress={onLinkPress}>
|
||||||
@@ -169,7 +167,7 @@ export function RichText({
|
|||||||
key={key}
|
key={key}
|
||||||
to={link.uri}
|
to={link.uri}
|
||||||
style={interactiveStyles}
|
style={interactiveStyles}
|
||||||
// @ts-ignore TODO
|
// @ts-expect-error TODO
|
||||||
dataSet={WORD_WRAP}
|
dataSet={WORD_WRAP}
|
||||||
shareOnLongPress
|
shareOnLongPress
|
||||||
shouldProxy={shouldProxyLinks}
|
shouldProxy={shouldProxyLinks}
|
||||||
@@ -209,7 +207,6 @@ export function RichText({
|
|||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
onTextLayout={onTextLayout}
|
onTextLayout={onTextLayout}
|
||||||
// @ts-ignore web only -prf
|
|
||||||
dataSet={WORD_WRAP}>
|
dataSet={WORD_WRAP}>
|
||||||
{els}
|
{els}
|
||||||
{suffix ? ' ' : null}
|
{suffix ? ' ' : null}
|
||||||
|
|||||||
@@ -100,7 +100,16 @@ export function Trigger({children, hitSlop, label}: TriggerProps) {
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
hitSlop={hitSlop}
|
hitSlop={
|
||||||
|
typeof hitSlop === 'number'
|
||||||
|
? {
|
||||||
|
top: hitSlop,
|
||||||
|
right: hitSlop,
|
||||||
|
bottom: hitSlop,
|
||||||
|
left: hitSlop,
|
||||||
|
}
|
||||||
|
: (hitSlop ?? undefined)
|
||||||
|
}
|
||||||
label={label}
|
label={label}
|
||||||
onPress={control.open}
|
onPress={control.open}
|
||||||
style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]}
|
style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {View} from 'react-native'
|
|||||||
import {Select as RadixSelect} from 'radix-ui'
|
import {Select as RadixSelect} from 'radix-ui'
|
||||||
|
|
||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
import {atoms as a, flatten, useTheme, web} from '#/alf'
|
import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||||
import {
|
import {
|
||||||
@@ -94,7 +94,7 @@ export function Trigger({children, label}: TriggerProps) {
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
style={flatten([
|
style={flattenToCSS([
|
||||||
a.flex,
|
a.flex,
|
||||||
a.relative,
|
a.relative,
|
||||||
t.atoms.bg_contrast_50,
|
t.atoms.bg_contrast_50,
|
||||||
@@ -196,7 +196,7 @@ export function Content<T>({
|
|||||||
return (
|
return (
|
||||||
<RadixSelect.Portal>
|
<RadixSelect.Portal>
|
||||||
<RadixSelect.Content
|
<RadixSelect.Content
|
||||||
style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
|
style={flattenToCSS([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
|
||||||
position="popper"
|
position="popper"
|
||||||
align="center"
|
align="center"
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
@@ -212,17 +212,17 @@ export function Content<T>({
|
|||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
!reduceMotionEnabled && a.zoom_fade_in,
|
!reduceMotionEnabled && a.zoom_fade_in,
|
||||||
]}>
|
]}>
|
||||||
<RadixSelect.ScrollUpButton style={flatten(up)}>
|
<RadixSelect.ScrollUpButton style={flattenToCSS(up)}>
|
||||||
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
||||||
</RadixSelect.ScrollUpButton>
|
</RadixSelect.ScrollUpButton>
|
||||||
<RadixSelect.Viewport style={flatten([a.p_xs])}>
|
<RadixSelect.Viewport style={flattenToCSS([a.p_xs])}>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<Fragment key={valueExtractor(item)}>
|
<Fragment key={valueExtractor(item)}>
|
||||||
{renderItem(item, index, selectedValue)}
|
{renderItem(item, index, selectedValue)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</RadixSelect.Viewport>
|
</RadixSelect.Viewport>
|
||||||
<RadixSelect.ScrollDownButton style={flatten(down)}>
|
<RadixSelect.ScrollDownButton style={flattenToCSS(down)}>
|
||||||
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
|
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
|
||||||
</RadixSelect.ScrollDownButton>
|
</RadixSelect.ScrollDownButton>
|
||||||
</View>
|
</View>
|
||||||
@@ -273,7 +273,7 @@ export function Item({ref, value, style, children}: ItemProps) {
|
|||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
style={flatten([
|
style={flattenToCSS([
|
||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
a.relative,
|
a.relative,
|
||||||
a.flex,
|
a.flex,
|
||||||
@@ -307,7 +307,7 @@ export const ItemText = function ItemText({children, style}: ItemTextProps) {
|
|||||||
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
|
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
|
||||||
return (
|
return (
|
||||||
<RadixSelect.ItemIndicator
|
<RadixSelect.ItemIndicator
|
||||||
style={flatten([
|
style={flattenToCSS([
|
||||||
a.absolute,
|
a.absolute,
|
||||||
{left: 0, width: 30},
|
{left: 0, width: 30},
|
||||||
a.flex,
|
a.flex,
|
||||||
@@ -324,7 +324,7 @@ export function Separator() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RadixSelect.Separator
|
<RadixSelect.Separator
|
||||||
style={flatten([
|
style={flattenToCSS([
|
||||||
{
|
{
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: t.atoms.border_contrast_low.borderColor,
|
backgroundColor: t.atoms.border_contrast_low.borderColor,
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
|
|||||||
import {type ListRenderItemInfo, View} from 'react-native'
|
import {type ListRenderItemInfo, View} from 'react-native'
|
||||||
import {AtUri} from '@atproto/syntax'
|
import {AtUri} from '@atproto/syntax'
|
||||||
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
import {type ModerationOpts} from '@bsky/sdk/moderation'
|
||||||
import {
|
|
||||||
type InfiniteData,
|
|
||||||
type UseInfiniteQueryResult,
|
|
||||||
} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
@@ -26,9 +22,6 @@ function keyExtractor(item: app.bsky.actor.defs.ProfileView, index: number) {
|
|||||||
|
|
||||||
interface ProfilesListProps {
|
interface ProfilesListProps {
|
||||||
listUri: string
|
listUri: string
|
||||||
listMembersQuery: UseInfiniteQueryResult<
|
|
||||||
InfiniteData<app.bsky.graph.getList.$OutputBody>
|
|
||||||
>
|
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
headerHeight: number
|
headerHeight: number
|
||||||
scrollElRef: ListRef
|
scrollElRef: ListRef
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ import {
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
const TooltipPortal = createPortalGroup()
|
const TooltipPortal = createPortalGroup()
|
||||||
const TooltipProviderContext =
|
const TooltipProviderContext = createContext<React.RefObject<React.ComponentRef<
|
||||||
createContext<React.RefObject<View | null> | null>(null)
|
typeof View
|
||||||
|
> | null> | null>(null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container,
|
* Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container,
|
||||||
@@ -37,7 +38,7 @@ const TooltipProviderContext =
|
|||||||
* Only really necessary on iOS but can work on Android.
|
* Only really necessary on iOS but can work on Android.
|
||||||
*/
|
*/
|
||||||
export function SheetCompatProvider({children}: {children: React.ReactNode}) {
|
export function SheetCompatProvider({children}: {children: React.ReactNode}) {
|
||||||
const ref = useRef<View | null>(null)
|
const ref = useRef<React.ComponentRef<typeof View> | null>(null)
|
||||||
return (
|
return (
|
||||||
<GlobalGestureEventsProvider style={[a.flex_1]}>
|
<GlobalGestureEventsProvider style={[a.flex_1]}>
|
||||||
<TooltipPortal.Provider>
|
<TooltipPortal.Provider>
|
||||||
@@ -154,7 +155,7 @@ export function Outer({
|
|||||||
export function Target({children}: {children: React.ReactNode}) {
|
export function Target({children}: {children: React.ReactNode}) {
|
||||||
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
|
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
|
||||||
const [hasLaidOut, setHasLaidOut] = useState(false)
|
const [hasLaidOut, setHasLaidOut] = useState(false)
|
||||||
const targetRef = useRef<View>(null)
|
const targetRef = useRef<React.ComponentRef<typeof View>>(null)
|
||||||
const containerRef = useContext(TooltipProviderContext)
|
const containerRef = useContext(TooltipProviderContext)
|
||||||
const keyboardIsOpen = useIsKeyboardVisible()
|
const keyboardIsOpen = useIsKeyboardVisible()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {View} from 'react-native'
|
|||||||
import {utils} from '@bsky.app/alf'
|
import {utils} from '@bsky.app/alf'
|
||||||
import {Popover} from 'radix-ui'
|
import {Popover} from 'radix-ui'
|
||||||
|
|
||||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
import {atoms as a, flattenToCSS, useTheme} from '#/alf'
|
||||||
import {
|
import {
|
||||||
ARROW_SIZE,
|
ARROW_SIZE,
|
||||||
BUBBLE_MAX_WIDTH,
|
BUBBLE_MAX_WIDTH,
|
||||||
@@ -85,7 +85,7 @@ export function Content({
|
|||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={flatten([
|
style={flattenToCSS([
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
{
|
{
|
||||||
backgroundColor: style.surface,
|
backgroundColor: style.surface,
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ export function OTPInput({
|
|||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
onChange: (text: string) => void
|
onChange: (text: string) => void
|
||||||
ref?: React.Ref<TextInput>
|
ref?: React.Ref<React.ComponentRef<typeof TextInput>>
|
||||||
numberOfDigits?: number
|
numberOfDigits?: number
|
||||||
onComplete?: (code: string) => void
|
onComplete?: (code: string) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const innerRef = useRef<TextInput>(null)
|
const innerRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
const [selection, setSelection] = useState({start: 0, end: 0})
|
const [selection, setSelection] = useState({start: 0, end: 0})
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ export function ViewMatches({
|
|||||||
ref={listRef}
|
ref={listRef}
|
||||||
data={items}
|
data={items}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
ListFooterComponent={!isEmpty ? <ListFooter height={20} /> : null}
|
ListFooterComponent={!isEmpty ? <ListFooter height={20} /> : undefined}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
keyboardDismissMode="interactive"
|
keyboardDismissMode="interactive"
|
||||||
automaticallyAdjustKeyboardInsets
|
automaticallyAdjustKeyboardInsets
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function SearchablePeopleList({
|
|||||||
const [headerHeight, setHeaderHeight] = useState(0)
|
const [headerHeight, setHeaderHeight] = useState(0)
|
||||||
const listRef = useRef<ListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const inputRef = useRef<TextInput>(null)
|
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
|
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
|
|
||||||
@@ -634,7 +634,7 @@ function SearchInput({
|
|||||||
value: string
|
value: string
|
||||||
onChangeText: (text: string) => void
|
onChangeText: (text: string) => void
|
||||||
onEscape: () => void
|
onEscape: () => void
|
||||||
inputRef: React.RefObject<TextInput | null>
|
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|||||||
@@ -182,9 +182,9 @@ function ListsContent({
|
|||||||
<View style={[a.align_center, a.py_lg]}>
|
<View style={[a.align_center, a.py_lg]}>
|
||||||
<Loader size="lg" />
|
<Loader size="lg" />
|
||||||
</View>
|
</View>
|
||||||
) : null
|
) : undefined
|
||||||
}
|
}
|
||||||
ListEmptyComponent={!isLoading && data ? <Empty /> : null}
|
ListEmptyComponent={!isLoading && data ? <Empty /> : undefined}
|
||||||
webInnerContentContainerStyle={[a.py_0]}
|
webInnerContentContainerStyle={[a.py_0]}
|
||||||
style={platform({
|
style={platform({
|
||||||
web: [a.px_2xl, a.pb_md],
|
web: [a.px_2xl, a.pb_md],
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ function Inner({
|
|||||||
}, [activeNux, setActiveNux])
|
}, [activeNux, setActiveNux])
|
||||||
|
|
||||||
if (__DEV__ && typeof window !== 'undefined') {
|
if (__DEV__ && typeof window !== 'undefined') {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
window.clearNuxDialog = (id: Nux) => {
|
window.clearNuxDialog = (id: Nux) => {
|
||||||
if (!__DEV__ || !id) return
|
if (!__DEV__ || !id) return
|
||||||
resetNuxs([id])
|
resetNuxs([id])
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export function AddMembersFlow({
|
|||||||
const [footerHeight, setFooterHeight] = useState(0)
|
const [footerHeight, setFooterHeight] = useState(0)
|
||||||
|
|
||||||
const listRef = useRef<ListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const inputRef = useRef<TextInput>(null)
|
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
|
|
||||||
const [{groupChatDids, groupChatProfiles, searchText}, dispatch] = useReducer(
|
const [{groupChatDids, groupChatProfiles, searchText}, dispatch] = useReducer(
|
||||||
reducer,
|
reducer,
|
||||||
@@ -502,7 +502,7 @@ export function AddMembersFlow({
|
|||||||
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
||||||
<Loader size="xl" />
|
<Loader size="xl" />
|
||||||
</View>
|
</View>
|
||||||
) : null
|
) : undefined
|
||||||
}
|
}
|
||||||
keyExtractor={(item: Item) => item.key}
|
keyExtractor={(item: Item) => item.key}
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export function InitiateChatFlow({
|
|||||||
const listRef = useRef<ListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const aa = useAgeAssurance()
|
const aa = useAgeAssurance()
|
||||||
const inputRef = useRef<TextInput>(null)
|
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
const accountTooNewPromptControl = Dialog.useDialogControl()
|
const accountTooNewPromptControl = Dialog.useDialogControl()
|
||||||
|
|
||||||
const {data: convos} = useListConvosQuery({
|
const {data: convos} = useListConvosQuery({
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ function ReactionTabs({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
const scrollViewRef = useRef<ScrollView>(null)
|
const scrollViewRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
|
||||||
const scrollState = useRef({x: 0, width: 0})
|
const scrollState = useRef({x: 0, width: 0})
|
||||||
const tabLayouts = useRef<Map<string, {x: number; width: number}>>(new Map())
|
const tabLayouts = useRef<Map<string, {x: number; width: number}>>(new Map())
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function UserSearchInput({
|
|||||||
value: string
|
value: string
|
||||||
onChangeText: (text: string) => void
|
onChangeText: (text: string) => void
|
||||||
onEscape: () => void
|
onEscape: () => void
|
||||||
inputRef: React.RefObject<TextInput | null>
|
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
@@ -38,7 +38,6 @@ export function UserSearchInput({
|
|||||||
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
// @ts-ignore bottom sheet input types issue - esb
|
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={l`Search for people`}
|
placeholder={l`Search for people`}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -8,23 +8,24 @@ import {
|
|||||||
import {mergeRefs} from '#/lib/merge-refs'
|
import {mergeRefs} from '#/lib/merge-refs'
|
||||||
import {atoms as a, extractPadding, useAlf, web} from '#/alf'
|
import {atoms as a, extractPadding, useAlf, web} from '#/alf'
|
||||||
import {normalizeTextStyles} from '#/alf/typography'
|
import {normalizeTextStyles} from '#/alf/typography'
|
||||||
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & {
|
export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & {
|
||||||
ref?: React.Ref<TextInput>
|
ref?: React.Ref<React.ComponentRef<typeof TextInput>>
|
||||||
label: string
|
label: string
|
||||||
minRows?: number
|
minRows?: number
|
||||||
maxRows?: number
|
maxRows?: number
|
||||||
onUpdateHeight?: (height: number) => void
|
onUpdateHeight?: (height: number) => void
|
||||||
/**
|
/**
|
||||||
* In some cases, like on native, we may not pass in an actual `value` prop.
|
* In some cases, like on native, we may not pass in an actual `value` prop.
|
||||||
* This prop allows Android to know if the field was cleared and thereby
|
* This prop allows native platforms to know if the field was cleared and
|
||||||
* reset its height. This is a small hack required because we need to
|
* thereby reset its height. This is a small hack required because Android's
|
||||||
* explicitly set the `{height: nativeHeight}` on Android.
|
* height is driven by state, while iOS needs an explicit minimum height when
|
||||||
|
* cleared programmatically.
|
||||||
*
|
*
|
||||||
* If you notice height calcuation issues after clearing the field on
|
* If you notice height calculation issues after clearing the field on a
|
||||||
* Android, check if this value is being populated and cleared properly in
|
* native platform, check if this value is being populated and cleared
|
||||||
* the parent component.
|
* properly in the parent component.
|
||||||
*/
|
*/
|
||||||
rawValue?: string
|
rawValue?: string
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ export function AutosizedTextarea({
|
|||||||
...rest
|
...rest
|
||||||
}: AutosizedTextareaProps) {
|
}: AutosizedTextareaProps) {
|
||||||
const {theme: t, fonts} = useAlf()
|
const {theme: t, fonts} = useAlf()
|
||||||
const internalRef = useRef<TextInput>(null)
|
const internalRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
const {style, minInputHeight, maxInputHeight, verticalContentPadding} =
|
const {style, minInputHeight, maxInputHeight, verticalContentPadding} =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
const normalizedStyles = normalizeTextStyles(
|
const normalizedStyles = normalizeTextStyles(
|
||||||
@@ -137,13 +138,12 @@ export function AutosizedTextarea({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Manual height clearing required for Android because we're forced to set
|
* Reset native height state after a programmatic clear. Android uses it as
|
||||||
* `{height: nativeHeight}` on Android, and even though `onContentSizeChange`
|
* the explicit input height, while iOS uses it to decide when to scroll.
|
||||||
* fires, the height matches the existing height and we aren't able to reset.
|
|
||||||
*/
|
*/
|
||||||
const prevRawValue = useRef(rawValue || '')
|
const prevRawValue = useRef(rawValue || '')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!IS_ANDROID) return // everything else is fine
|
if (!IS_NATIVE) return
|
||||||
if (rawValue === undefined) return // uncontrolled
|
if (rawValue === undefined) return // uncontrolled
|
||||||
if (prevRawValue.current?.length && rawValue === '') {
|
if (prevRawValue.current?.length && rawValue === '') {
|
||||||
setNativeHeight(minInputHeight)
|
setNativeHeight(minInputHeight)
|
||||||
@@ -176,11 +176,12 @@ export function AutosizedTextarea({
|
|||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
}),
|
}),
|
||||||
style,
|
style,
|
||||||
IS_ANDROID ? {height: nativeHeight} : {},
|
IS_ANDROID && {height: nativeHeight},
|
||||||
|
IS_IOS && rawValue === '' && {height: minInputHeight},
|
||||||
]}
|
]}
|
||||||
{...rest}
|
{...rest}
|
||||||
ref={mergeRefs([
|
ref={mergeRefs([
|
||||||
(node: TextInput | null) => {
|
(node: React.ComponentRef<typeof TextInput> | null) => {
|
||||||
internalRef.current = node
|
internalRef.current = node
|
||||||
// bop resize on first render
|
// bop resize on first render
|
||||||
if (IS_WEB && node) handleResizeWeb()
|
if (IS_WEB && node) handleResizeWeb()
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ export function DateField({
|
|||||||
open
|
open
|
||||||
timeZoneOffsetInMinutes={0}
|
timeZoneOffsetInMinutes={0}
|
||||||
theme={t.scheme}
|
theme={t.scheme}
|
||||||
// @ts-ignore TODO
|
|
||||||
buttonColor={t.name === 'light' ? '#000000' : '#ffffff'}
|
buttonColor={t.name === 'light' ? '#000000' : '#ffffff'}
|
||||||
date={initialDate}
|
date={initialDate}
|
||||||
onConfirm={onChangeInternal}
|
onConfirm={onChangeInternal}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function DateField({
|
|||||||
<TextField.Icon icon={CalendarDays} />
|
<TextField.Icon icon={CalendarDays} />
|
||||||
<Input
|
<Input
|
||||||
value={value === '' ? '' : toSimpleDateString(value)}
|
value={value === '' ? '' : toSimpleDateString(value)}
|
||||||
inputRef={inputRef as React.Ref<TextInput>}
|
inputRef={inputRef as React.Ref<React.ComponentRef<typeof TextInput>>}
|
||||||
label={label}
|
label={label}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Props = Omit<TextField.InputProps, 'label'> & {
|
|||||||
*/
|
*/
|
||||||
onClearText?: () => void
|
onClearText?: () => void
|
||||||
hotkey?: boolean
|
hotkey?: boolean
|
||||||
ref?: React.Ref<TextInput>
|
ref?: React.Ref<React.ComponentRef<typeof TextInput>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchInput({
|
export function SearchInput({
|
||||||
@@ -33,7 +33,7 @@ export function SearchInput({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const showClear = value && value.length > 0
|
const showClear = value && value.length > 0
|
||||||
const internalRef = useRef<TextInput>(null)
|
const internalRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hotkey) return
|
if (!hotkey) return
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {mergeRefs} from '#/lib/merge-refs'
|
|||||||
import {
|
import {
|
||||||
applyFonts,
|
applyFonts,
|
||||||
atoms as a,
|
atoms as a,
|
||||||
|
flatten,
|
||||||
|
type MutableTextStyle,
|
||||||
platform,
|
platform,
|
||||||
type TextStyleProp,
|
type TextStyleProp,
|
||||||
tokens,
|
tokens,
|
||||||
@@ -26,7 +28,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
const Context = createContext<{
|
const Context = createContext<{
|
||||||
inputRef: React.RefObject<TextInput | null> | null
|
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null> | null
|
||||||
isInvalid: boolean
|
isInvalid: boolean
|
||||||
hovered: boolean
|
hovered: boolean
|
||||||
onHoverIn: () => void
|
onHoverIn: () => void
|
||||||
@@ -55,7 +57,7 @@ export type RootProps = React.PropsWithChildren<
|
|||||||
>
|
>
|
||||||
|
|
||||||
export function Root({children, isInvalid = false, style}: RootProps) {
|
export function Root({children, isInvalid = false, style}: RootProps) {
|
||||||
const inputRef = useRef<TextInput>(null)
|
const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
onIn: onHoverIn,
|
onIn: onHoverIn,
|
||||||
@@ -161,7 +163,9 @@ export type InputProps = Omit<
|
|||||||
value?: string
|
value?: string
|
||||||
onChangeText?: (value: string) => void
|
onChangeText?: (value: string) => void
|
||||||
isInvalid?: boolean
|
isInvalid?: boolean
|
||||||
inputRef?: React.RefObject<TextInput | null> | React.ForwardedRef<TextInput>
|
inputRef?:
|
||||||
|
| React.RefObject<React.ComponentRef<typeof TextInput> | null>
|
||||||
|
| React.ForwardedRef<React.ComponentRef<typeof TextInput>>
|
||||||
/**
|
/**
|
||||||
* Note: this currently falls back to the label if not specified. However,
|
* Note: this currently falls back to the label if not specified. However,
|
||||||
* most new designs have no placeholder. We should eventually remove this fallback
|
* most new designs have no placeholder. We should eventually remove this fallback
|
||||||
@@ -208,43 +212,41 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
|
|
||||||
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
|
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
|
||||||
|
|
||||||
const flattened = StyleSheet.flatten<TextStyle>([
|
const flattened: MutableTextStyle = {
|
||||||
a.relative,
|
...flatten([
|
||||||
a.z_20,
|
a.relative,
|
||||||
a.flex_1,
|
a.z_20,
|
||||||
a.text_md,
|
a.flex_1,
|
||||||
t.atoms.text,
|
a.text_md,
|
||||||
a.px_xs,
|
t.atoms.text,
|
||||||
{
|
a.px_xs,
|
||||||
// paddingVertical doesn't work w/multiline - esb
|
{
|
||||||
lineHeight: a.text_md.fontSize * 1.2,
|
// paddingVertical doesn't work w/multiline - esb
|
||||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
lineHeight: a.text_md.fontSize * 1.2,
|
||||||
minHeight: rest.multiline ? 80 : undefined,
|
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||||
minWidth: 0,
|
minHeight: rest.multiline ? 80 : undefined,
|
||||||
paddingTop: 13,
|
minWidth: 0,
|
||||||
paddingBottom: 13,
|
paddingTop: 13,
|
||||||
},
|
paddingBottom: 13,
|
||||||
/*
|
},
|
||||||
* Margins are needed here to avoid autofill background overlapping the
|
/*
|
||||||
* top and bottom borders - esb
|
* Margins are needed here to avoid autofill background overlapping the
|
||||||
*/
|
* top and bottom borders - esb
|
||||||
web({
|
*/
|
||||||
paddingTop: 11,
|
web({
|
||||||
paddingBottom: 11,
|
paddingTop: 11,
|
||||||
marginTop: 2,
|
paddingBottom: 11,
|
||||||
marginBottom: 2,
|
marginTop: 2,
|
||||||
}),
|
marginBottom: 2,
|
||||||
style,
|
}),
|
||||||
])
|
style,
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
applyFonts(flattened, fonts.family)
|
applyFonts(flattened, fonts.family)
|
||||||
|
|
||||||
// should always be defined on `typography`
|
|
||||||
// @ts-ignore
|
|
||||||
if (flattened.fontSize) {
|
if (flattened.fontSize) {
|
||||||
// @ts-ignore
|
|
||||||
flattened.fontSize = Math.round(
|
flattened.fontSize = Math.round(
|
||||||
// @ts-ignore
|
|
||||||
flattened.fontSize * fonts.scaleMultiplier,
|
flattened.fontSize * fonts.scaleMultiplier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import {
|
import {
|
||||||
Keyboard,
|
Keyboard,
|
||||||
type KeyboardEventListener,
|
type KeyboardEvent,
|
||||||
type KeyboardEventName,
|
type KeyboardEventName,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
|
||||||
export function useOnKeyboard(
|
export function useOnKeyboard(
|
||||||
eventName: KeyboardEventName,
|
eventName: KeyboardEventName,
|
||||||
cb: KeyboardEventListener,
|
cb: (event: KeyboardEvent) => unknown,
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = Keyboard.addListener(eventName, cb)
|
const subscription = Keyboard.addListener(eventName, cb)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const IconTemplate_Stroke2_Corner0_Rounded = forwardRef(
|
|||||||
<Svg
|
<Svg
|
||||||
fill="none"
|
fill="none"
|
||||||
{...rest}
|
{...rest}
|
||||||
// @ts-ignore it's fiiiiine
|
// @ts-expect-error it's fiiiiine
|
||||||
ref={ref}
|
ref={ref}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
width={size}
|
width={size}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ interface GalleryProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Context = createContext<{
|
const Context = createContext<{
|
||||||
bleedRef: React.RefObject<View | null>
|
bleedRef: React.RefObject<React.ComponentRef<typeof View> | null>
|
||||||
bleedWidth: number
|
bleedWidth: number
|
||||||
}>({
|
}>({
|
||||||
bleedRef: {current: null},
|
bleedRef: {current: null},
|
||||||
@@ -73,7 +73,7 @@ const Context = createContext<{
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function GalleryBleed({children}: {children: React.ReactNode}) {
|
export function GalleryBleed({children}: {children: React.ReactNode}) {
|
||||||
const ref = useRef<View>(null)
|
const ref = useRef<React.ComponentRef<typeof View>>(null)
|
||||||
const [bleedWidth, setBleedWidth] = useState(0)
|
const [bleedWidth, setBleedWidth] = useState(0)
|
||||||
|
|
||||||
if (!isValidElement(children)) {
|
if (!isValidElement(children)) {
|
||||||
@@ -145,7 +145,7 @@ export function Gallery({
|
|||||||
* scroll position, so it works correctly for off-screen FlatList items.
|
* scroll position, so it works correctly for off-screen FlatList items.
|
||||||
*/
|
*/
|
||||||
const {bleedRef, bleedWidth} = useGalleryBleed()
|
const {bleedRef, bleedWidth} = useGalleryBleed()
|
||||||
const contentRef = useRef<View>(null)
|
const contentRef = useRef<React.ComponentRef<typeof View>>(null)
|
||||||
const [contentDims, setContentDims] = useState<{x: number; width: number}>()
|
const [contentDims, setContentDims] = useState<{x: number; width: number}>()
|
||||||
const measure = () => {
|
const measure = () => {
|
||||||
if (contentRef.current && bleedRef.current) {
|
if (contentRef.current && bleedRef.current) {
|
||||||
@@ -168,7 +168,9 @@ export function Gallery({
|
|||||||
|
|
||||||
const flatListRef = useRef<FlatList>(null)
|
const flatListRef = useRef<FlatList>(null)
|
||||||
const itemWidthsRef = useRef<Map<number, number>>(new Map())
|
const itemWidthsRef = useRef<Map<number, number>>(new Map())
|
||||||
const itemRefsRef = useRef<Map<number, View>>(new Map())
|
const itemRefsRef = useRef<Map<number, React.ComponentRef<typeof View>>>(
|
||||||
|
new Map(),
|
||||||
|
)
|
||||||
const containerRefsRef = useRef<Map<number, AnimatedRef<any>>>(new Map())
|
const containerRefsRef = useRef<Map<number, AnimatedRef<any>>>(new Map())
|
||||||
const thumbDimsRef = useRef<Map<number, Dimensions>>(new Map())
|
const thumbDimsRef = useRef<Map<number, Dimensions>>(new Map())
|
||||||
const currentIndexRef = useRef(0)
|
const currentIndexRef = useRef(0)
|
||||||
@@ -408,7 +410,7 @@ function GalleryImage({
|
|||||||
index: number
|
index: number
|
||||||
imageCount: number
|
imageCount: number
|
||||||
onWidthChange: (index: number, width: number) => void
|
onWidthChange: (index: number, width: number) => void
|
||||||
itemRef: (node: View | null) => void
|
itemRef: (node: React.ComponentRef<typeof View> | null) => void
|
||||||
largeAltBadge?: boolean
|
largeAltBadge?: boolean
|
||||||
onContainerRef: (index: number, ref: AnimatedRef<any>) => void
|
onContainerRef: (index: number, ref: AnimatedRef<any>) => void
|
||||||
onThumbDims: (index: number, dims: Dimensions) => void
|
onThumbDims: (index: number, dims: Dimensions) => void
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
import {useCallback} from 'react'
|
|
||||||
import {ScrollView, View} from 'react-native'
|
|
||||||
import {useLingui} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {
|
|
||||||
useTrendingSettings,
|
|
||||||
useTrendingSettingsApi,
|
|
||||||
} from '#/state/preferences/trending'
|
|
||||||
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
|
|
||||||
import {useTrendingConfig} from '#/state/service-config'
|
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
|
||||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
|
||||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
|
||||||
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
|
|
||||||
import * as Prompt from '#/components/Prompt'
|
|
||||||
import {TrendingTopicLink} from '#/components/TrendingTopics'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {useAnalytics} from '#/analytics'
|
|
||||||
|
|
||||||
const TRENDING_LIMIT = 14
|
|
||||||
|
|
||||||
export function TrendingInterstitial() {
|
|
||||||
const {enabled} = useTrendingConfig()
|
|
||||||
const {trendingDisabled} = useTrendingSettings()
|
|
||||||
return enabled && !trendingDisabled ? <Inner /> : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Inner() {
|
|
||||||
const t = useTheme()
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
const ax = useAnalytics()
|
|
||||||
const gutters = useGutters([0, 'base', 0, 'base'])
|
|
||||||
const trendingPrompt = Prompt.usePromptControl()
|
|
||||||
const {setTrendingDisabled} = useTrendingSettingsApi()
|
|
||||||
const {
|
|
||||||
data: trending,
|
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
} = useGetTrendsQuery({
|
|
||||||
limit: TRENDING_LIMIT,
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
})
|
|
||||||
const noTopics = !isLoading && !error && !trending?.trends?.length
|
|
||||||
|
|
||||||
const onConfirmHide = useCallback(() => {
|
|
||||||
ax.metric('trendingTopics:hide', {context: 'interstitial'})
|
|
||||||
setTrendingDisabled(true)
|
|
||||||
}, [ax, setTrendingDisabled])
|
|
||||||
|
|
||||||
return error || noTopics ? null : (
|
|
||||||
<View style={[t.atoms.border_contrast_low, a.border_t, a.border_b]}>
|
|
||||||
<BlockDrawerGesture>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
decelerationRate="fast">
|
|
||||||
<View style={[gutters, a.flex_row, a.align_center, a.gap_lg]}>
|
|
||||||
<View style={{paddingLeft: 4, paddingRight: 2}}>
|
|
||||||
<Graph size="sm" />
|
|
||||||
</View>
|
|
||||||
{isLoading ? (
|
|
||||||
<View style={[a.py_lg, a.flex_row, a.gap_lg, a.align_center]}>
|
|
||||||
<LoadingPlaceholder
|
|
||||||
width={80}
|
|
||||||
height={undefined}
|
|
||||||
style={{alignSelf: 'stretch'}}
|
|
||||||
/>
|
|
||||||
<LoadingPlaceholder
|
|
||||||
width={50}
|
|
||||||
height={undefined}
|
|
||||||
style={{alignSelf: 'stretch'}}
|
|
||||||
/>
|
|
||||||
<LoadingPlaceholder
|
|
||||||
width={120}
|
|
||||||
height={undefined}
|
|
||||||
style={{alignSelf: 'stretch'}}
|
|
||||||
/>
|
|
||||||
<LoadingPlaceholder
|
|
||||||
width={30}
|
|
||||||
height={undefined}
|
|
||||||
style={{alignSelf: 'stretch'}}
|
|
||||||
/>
|
|
||||||
<LoadingPlaceholder
|
|
||||||
width={180}
|
|
||||||
height={undefined}
|
|
||||||
style={{alignSelf: 'stretch'}}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.text_sm,
|
|
||||||
a.font_semi_bold,
|
|
||||||
]}>
|
|
||||||
{' '}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : !trending?.trends ? null : (
|
|
||||||
<>
|
|
||||||
{trending.trends.map((topic, index) => {
|
|
||||||
const rank = index + 1
|
|
||||||
return (
|
|
||||||
<TrendingTopicLink
|
|
||||||
key={topic.link}
|
|
||||||
topic={topic}
|
|
||||||
metricContext="interstitial"
|
|
||||||
rank={rank}
|
|
||||||
recId={trending.recId}
|
|
||||||
onPress={() => {
|
|
||||||
ax.metric('trendingTopic:click', {
|
|
||||||
context: 'interstitial',
|
|
||||||
rank,
|
|
||||||
recId: trending.recId,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<View style={[a.py_lg]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.text_sm,
|
|
||||||
a.font_semi_bold,
|
|
||||||
]}>
|
|
||||||
{topic.topic}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TrendingTopicLink>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<Button
|
|
||||||
label={l`Hide trending topics`}
|
|
||||||
size="tiny"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
shape="round"
|
|
||||||
onPress={() => trendingPrompt.open()}>
|
|
||||||
<ButtonIcon icon={X} />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</BlockDrawerGesture>
|
|
||||||
|
|
||||||
<Prompt.Basic
|
|
||||||
control={trendingPrompt}
|
|
||||||
title={l`Hide trending topics?`}
|
|
||||||
description={l`You can update this later from your settings.`}
|
|
||||||
confirmButtonCta={l`Hide`}
|
|
||||||
onConfirm={onConfirmHide}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -226,7 +226,7 @@ function BlockDialogInner({
|
|||||||
<View style={[a.py_lg, a.align_center, a.justify_center]}>
|
<View style={[a.py_lg, a.align_center, a.justify_center]}>
|
||||||
<Loader size="lg" />
|
<Loader size="lg" />
|
||||||
</View>
|
</View>
|
||||||
) : null
|
) : undefined
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<Dialog.FlatListFooter
|
<Dialog.FlatListFooter
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ function Inner(
|
|||||||
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
|
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ref = useRef<ScrollView>(null)
|
const ref = useRef<React.ComponentRef<typeof ScrollView>>(null)
|
||||||
const {
|
const {
|
||||||
data: allLabelers,
|
data: allLabelers,
|
||||||
isLoading: isLabelerLoading,
|
isLoading: isLabelerLoading,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function GifPickerBody({
|
|||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
onSelectGif: (gif: Gif) => void
|
onSelectGif: (gif: Gif) => void
|
||||||
}) {
|
}) {
|
||||||
const textInputRef = useRef<TextInput>(null)
|
const textInputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
|
||||||
const listRef = useRef<ListMethods>(null)
|
const listRef = useRef<ListMethods>(null)
|
||||||
const [rawSearch, setRawSearch] = useState('')
|
const [rawSearch, setRawSearch] = useState('')
|
||||||
const [activeCategory, setActiveCategory] = useState<string>('trending')
|
const [activeCategory, setActiveCategory] = useState<string>('trending')
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export const GifPickerGrid = forwardRef<ListMethods, Props>(
|
|||||||
onRetry={fetchNextPage}
|
onRetry={fetchNextPage}
|
||||||
style={{borderTopWidth: 0}}
|
style={{borderTopWidth: 0}}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function GifPickerHeader({
|
|||||||
onEscape,
|
onEscape,
|
||||||
canClear,
|
canClear,
|
||||||
}: {
|
}: {
|
||||||
inputRef: Ref<TextInput>
|
inputRef: Ref<React.ComponentRef<typeof TextInput>>
|
||||||
onChangeText: (text: string) => void
|
onChangeText: (text: string) => void
|
||||||
onClear: () => void
|
onClear: () => void
|
||||||
onEscape: () => void
|
onEscape: () => void
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function useWebOnlyDebugLiveEventPreferences() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (env.IS_DEV && IS_WEB && typeof window !== 'undefined') {
|
if (env.IS_DEV && IS_WEB && typeof window !== 'undefined') {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
window.__updateLiveEventPreferences = async (
|
window.__updateLiveEventPreferences = async (
|
||||||
action: LiveEventPreferencesAction,
|
action: LiveEventPreferencesAction,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -4,7 +4,54 @@ import {isPostInLanguage} from '../../locale/helpers'
|
|||||||
import {FALLBACK_MARKER_POST} from './feed/home'
|
import {FALLBACK_MARKER_POST} from './feed/home'
|
||||||
import {type ReasonFeedSource} from './feed/types'
|
import {type ReasonFeedSource} from './feed/types'
|
||||||
|
|
||||||
type FeedViewPost = app.bsky.feed.defs.FeedViewPost
|
export type FeedPostNumbering = Pick<
|
||||||
|
app.bsky.unspecced.defs.ThreadItemPost,
|
||||||
|
'opThreadPostIndex' | 'opThreadPostCount'
|
||||||
|
>
|
||||||
|
|
||||||
|
type ValidFeedPostNumbering = Required<FeedPostNumbering>
|
||||||
|
|
||||||
|
// AppView adds these fields to feed responses ahead of their feed lexicon.
|
||||||
|
type FeedViewPost = app.bsky.feed.defs.FeedViewPost & FeedPostNumbering
|
||||||
|
|
||||||
|
function getPostNumbering(
|
||||||
|
value: FeedPostNumbering,
|
||||||
|
): ValidFeedPostNumbering | undefined {
|
||||||
|
const {opThreadPostIndex: index, opThreadPostCount: count} = value
|
||||||
|
|
||||||
|
if (
|
||||||
|
index === undefined ||
|
||||||
|
count === undefined ||
|
||||||
|
index < 1 ||
|
||||||
|
count < 1 ||
|
||||||
|
index > count
|
||||||
|
) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
opThreadPostIndex: index,
|
||||||
|
opThreadPostCount: count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferPostNumbering(
|
||||||
|
feedPost: FeedViewPost,
|
||||||
|
position: 'parent' | 'root',
|
||||||
|
): ValidFeedPostNumbering | undefined {
|
||||||
|
const postNumbering = getPostNumbering(feedPost)
|
||||||
|
if (!postNumbering) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed responses number only the selected post, so derive the hydrated
|
||||||
|
// context that the feed renders alongside it.
|
||||||
|
return getPostNumbering({
|
||||||
|
opThreadPostIndex:
|
||||||
|
position === 'root' ? 1 : postNumbering.opThreadPostIndex - 1,
|
||||||
|
opThreadPostCount: postNumbering.opThreadPostCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export type FeedTunerFn = (
|
export type FeedTunerFn = (
|
||||||
tuner: FeedTuner,
|
tuner: FeedTuner,
|
||||||
@@ -15,6 +62,7 @@ export type FeedTunerFn = (
|
|||||||
type FeedSliceItem = {
|
type FeedSliceItem = {
|
||||||
post: app.bsky.feed.defs.PostView
|
post: app.bsky.feed.defs.PostView
|
||||||
record: app.bsky.feed.post.Main
|
record: app.bsky.feed.post.Main
|
||||||
|
postNumbering: FeedPostNumbering | undefined
|
||||||
parentAuthor: app.bsky.actor.defs.ProfileViewBasic | undefined
|
parentAuthor: app.bsky.actor.defs.ProfileViewBasic | undefined
|
||||||
isParentBlocked: boolean
|
isParentBlocked: boolean
|
||||||
isParentNotFound: boolean
|
isParentNotFound: boolean
|
||||||
@@ -38,7 +86,10 @@ export class FeedViewPostsSlice {
|
|||||||
rootUri: string
|
rootUri: string
|
||||||
feedPostUri: string
|
feedPostUri: string
|
||||||
|
|
||||||
constructor(feedPost: FeedViewPost) {
|
constructor(
|
||||||
|
feedPost: FeedViewPost,
|
||||||
|
postNumberingByUri: Map<string, FeedPostNumbering>,
|
||||||
|
) {
|
||||||
const {post, reply, reason} = feedPost
|
const {post, reply, reason} = feedPost
|
||||||
this.items = []
|
this.items = []
|
||||||
this.isIncompleteThread = false
|
this.isIncompleteThread = false
|
||||||
@@ -80,6 +131,7 @@ export class FeedViewPostsSlice {
|
|||||||
this.items.push({
|
this.items.push({
|
||||||
post,
|
post,
|
||||||
record: post.record,
|
record: post.record,
|
||||||
|
postNumbering: postNumberingByUri.get(post.uri),
|
||||||
parentAuthor,
|
parentAuthor,
|
||||||
isParentBlocked,
|
isParentBlocked,
|
||||||
isParentNotFound,
|
isParentNotFound,
|
||||||
@@ -128,6 +180,9 @@ export class FeedViewPostsSlice {
|
|||||||
this.items.unshift({
|
this.items.unshift({
|
||||||
post: parent,
|
post: parent,
|
||||||
record: parent.record,
|
record: parent.record,
|
||||||
|
postNumbering:
|
||||||
|
postNumberingByUri.get(parent.uri) ??
|
||||||
|
inferPostNumbering(feedPost, 'parent'),
|
||||||
parentAuthor: grandparentAuthor,
|
parentAuthor: grandparentAuthor,
|
||||||
isParentBlocked: isGrandparentBlocked,
|
isParentBlocked: isGrandparentBlocked,
|
||||||
isParentNotFound: isGrandparentNotFound,
|
isParentNotFound: isGrandparentNotFound,
|
||||||
@@ -151,6 +206,9 @@ export class FeedViewPostsSlice {
|
|||||||
this.items.unshift({
|
this.items.unshift({
|
||||||
post: root,
|
post: root,
|
||||||
record: root.record,
|
record: root.record,
|
||||||
|
postNumbering:
|
||||||
|
postNumberingByUri.get(root.uri) ??
|
||||||
|
inferPostNumbering(feedPost, 'root'),
|
||||||
isParentBlocked: false,
|
isParentBlocked: false,
|
||||||
isParentNotFound: false,
|
isParentNotFound: false,
|
||||||
parentAuthor: undefined,
|
parentAuthor: undefined,
|
||||||
@@ -241,8 +299,16 @@ export class FeedTuner {
|
|||||||
dryRun: false,
|
dryRun: false,
|
||||||
},
|
},
|
||||||
): FeedViewPostsSlice[] {
|
): FeedViewPostsSlice[] {
|
||||||
|
const postNumberingByUri = new Map<string, FeedPostNumbering>()
|
||||||
|
for (const item of feed) {
|
||||||
|
const postNumbering = getPostNumbering(item)
|
||||||
|
if (postNumbering) {
|
||||||
|
postNumberingByUri.set(item.post.uri, postNumbering)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let slices: FeedViewPostsSlice[] = feed
|
let slices: FeedViewPostsSlice[] = feed
|
||||||
.map(item => new FeedViewPostsSlice(item))
|
.map(item => new FeedViewPostsSlice(item, postNumberingByUri))
|
||||||
.filter(s => s.items.length > 0 || s.isFallbackMarker)
|
.filter(s => s.items.length > 0 || s.isFallbackMarker)
|
||||||
|
|
||||||
// run the custom tuners
|
// run the custom tuners
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ class MergeFeedSource_Custom extends MergeFeedSource {
|
|||||||
feed = feed.filter(post => new Date(post.post.indexedAt) > this.minDate)
|
feed = feed.filter(post => new Date(post.post.indexedAt) > this.minDate)
|
||||||
// attach source info
|
// attach source info
|
||||||
for (const post of feed) {
|
for (const post of feed) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
post.__source = this.sourceInfo
|
post.__source = this.sourceInfo
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
export {unstable_batchedUpdates as batchedUpdates} from 'react-native'
|
import {unstable_batchedUpdates} from 'react-native'
|
||||||
|
|
||||||
|
export function batchedUpdates(fn: () => void): void {
|
||||||
|
unstable_batchedUpdates(fn, undefined)
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,8 +194,7 @@ export const MAX_LABELERS = 20
|
|||||||
export const VIDEO_SERVICE = 'https://video.bsky.app'
|
export const VIDEO_SERVICE = 'https://video.bsky.app'
|
||||||
export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app'
|
export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app'
|
||||||
|
|
||||||
export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds
|
export const VIDEO_MAX_DURATION_MS = 10 * 60 * 1000 // 10 minutes in milliseconds
|
||||||
export const VIDEO_10_MINUTE_MAX_DURATION_MS = 10 * 60 * 1000
|
|
||||||
/**
|
/**
|
||||||
* Maximum size of a video in megabytes, _not_ mebibytes. Backend uses
|
* Maximum size of a video in megabytes, _not_ mebibytes. Backend uses
|
||||||
* ISO megabytes.
|
* ISO megabytes.
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import {type ForwardedRef, useEffect, useMemo, useRef} from 'react'
|
import {type ForwardedRef, useEffect, useMemo, useRef} from 'react'
|
||||||
import {type ScrollView} from 'react-native'
|
import {Platform, type ScrollView} from 'react-native'
|
||||||
import {Platform} from 'react-native'
|
|
||||||
|
|
||||||
import {mergeRefs} from '#/lib/merge-refs'
|
import {mergeRefs} from '#/lib/merge-refs'
|
||||||
|
|
||||||
type Props<Scrollable extends ScrollView = ScrollView> = {
|
type ScrollViewInstance = React.ComponentRef<typeof ScrollView>
|
||||||
|
|
||||||
|
type Props<Scrollable extends ScrollViewInstance = ScrollViewInstance> = {
|
||||||
cursor?: string
|
cursor?: string
|
||||||
outerRef?: ForwardedRef<Scrollable>
|
outerRef?: ForwardedRef<Scrollable>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({
|
export function useDraggableScroll<
|
||||||
outerRef,
|
Scrollable extends ScrollViewInstance = ScrollViewInstance,
|
||||||
cursor = 'grab',
|
>({outerRef, cursor = 'grab'}: Props<Scrollable> = {}) {
|
||||||
}: Props<Scrollable> = {}) {
|
|
||||||
const ref = useRef<Scrollable>(null)
|
const ref = useRef<Scrollable>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ describe('useApplyPullRequestOTAUpdate', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
|
const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
|
||||||
act(() => buttons?.[1].onPress?.())
|
void act(() => buttons?.[1].onPress?.())
|
||||||
|
|
||||||
await waitFor(() => expect(reloadAsync).toHaveBeenCalled())
|
await waitFor(() => expect(reloadAsync).toHaveBeenCalled())
|
||||||
expect(device.set).toHaveBeenCalledWith(['pendingOTAUpdate'], {
|
expect(device.set).toHaveBeenCalledWith(['pendingOTAUpdate'], {
|
||||||
@@ -256,7 +256,7 @@ describe('useApplyPullRequestOTAUpdate', () => {
|
|||||||
await act(() => result.current.tryApplyUpdate('pull-request-123'))
|
await act(() => result.current.tryApplyUpdate('pull-request-123'))
|
||||||
const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
|
const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
|
||||||
|
|
||||||
act(() => buttons?.[1].onPress?.())
|
void act(() => buttons?.[1].onPress?.())
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(Alert.alert).toHaveBeenLastCalledWith(
|
expect(Alert.alert).toHaveBeenLastCalledWith(
|
||||||
@@ -283,7 +283,7 @@ describe('useApplyPullRequestOTAUpdate', () => {
|
|||||||
await act(() => result.current.tryApplyUpdate('pull-request-123'))
|
await act(() => result.current.tryApplyUpdate('pull-request-123'))
|
||||||
const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
|
const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
|
||||||
|
|
||||||
act(() => buttons?.[1].onPress?.())
|
void act(() => buttons?.[1].onPress?.())
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(Alert.alert).toHaveBeenLastCalledWith(
|
expect(Alert.alert).toHaveBeenLastCalledWith(
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ export const splash = (scheme: 'light' | 'dark') => {
|
|||||||
: require('../../../assets/splash/splash-dark.png')
|
: require('../../../assets/splash/splash-dark.png')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
image: RNImage.resolveAssetSource(source).uri,
|
image: RNImage.resolveAssetSource(source)!.uri,
|
||||||
imageFullScreen: true,
|
imageFullScreen: true,
|
||||||
imageResizeMode: 'cover',
|
imageResizeMode: 'cover',
|
||||||
backgroundColor: scheme === 'light' ? '#006AFF' : '#002861',
|
backgroundColor: scheme === 'light' ? '#006AFF' : '#002861',
|
||||||
|
|||||||
+1
-1
@@ -24,6 +24,6 @@ export function isJwtExpired(token: string) {
|
|||||||
|
|
||||||
export function isAppPassword(token: string) {
|
export function isAppPassword(token: string) {
|
||||||
const payload = jwtDecode(token)
|
const payload = jwtDecode(token)
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
return payload.scope === 'com.atproto.appPass'
|
return payload.scope === 'com.atproto.appPass'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export async function openPicker(opts?: ImagePickerOptions) {
|
|||||||
quality: 1,
|
quality: 1,
|
||||||
selectionLimit: 1,
|
selectionLimit: 1,
|
||||||
...opts,
|
...opts,
|
||||||
|
shouldDownloadFromNetwork: true,
|
||||||
legacy: true,
|
legacy: true,
|
||||||
preferredAssetRepresentationMode:
|
preferredAssetRepresentationMode:
|
||||||
UIImagePickerPreferredAssetRepresentationMode.Automatic,
|
UIImagePickerPreferredAssetRepresentationMode.Automatic,
|
||||||
@@ -63,6 +64,7 @@ export async function openUnifiedPicker({
|
|||||||
// the `file` returned on each asset after validation instead.
|
// the `file` returned on each asset after validation instead.
|
||||||
base64: false,
|
base64: false,
|
||||||
selectionLimit: IS_IOS ? selectionCountRemaining : undefined,
|
selectionLimit: IS_IOS ? selectionCountRemaining : undefined,
|
||||||
|
shouldDownloadFromNetwork: true,
|
||||||
preferredAssetRepresentationMode:
|
preferredAssetRepresentationMode:
|
||||||
UIImagePickerPreferredAssetRepresentationMode.Automatic,
|
UIImagePickerPreferredAssetRepresentationMode.Automatic,
|
||||||
videoExportPreset: VideoExportPreset.Passthrough,
|
videoExportPreset: VideoExportPreset.Passthrough,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {type Client} from '@atproto/lex'
|
||||||
|
|
||||||
|
import {com} from '#/lexicons'
|
||||||
|
import {
|
||||||
|
getServiceAuthToken,
|
||||||
|
SERVICE_AUTH_TTL_SEC,
|
||||||
|
serviceAuthExp,
|
||||||
|
} from '../upload.shared'
|
||||||
|
|
||||||
|
function createClient() {
|
||||||
|
const call = jest.fn().mockResolvedValue({token: 'token'})
|
||||||
|
return {client: {call} as unknown as Client, call}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('serviceAuthExp', () => {
|
||||||
|
it('returns an integer even when the clock has sub-second precision', () => {
|
||||||
|
jest.spyOn(Date, 'now').mockReturnValue(1_700_000_000_500)
|
||||||
|
expect(serviceAuthExp()).toBe(1_700_000_000 + SERVICE_AUTH_TTL_SEC)
|
||||||
|
expect(Number.isInteger(serviceAuthExp())).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts a custom ttl and keeps the result integral', () => {
|
||||||
|
jest.spyOn(Date, 'now').mockReturnValue(1_700_000_000_999)
|
||||||
|
expect(serviceAuthExp(90.7)).toBe(1_700_000_090)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getServiceAuthToken', () => {
|
||||||
|
it('floors a fractional exp before sending it', async () => {
|
||||||
|
const {client, call} = createClient()
|
||||||
|
await getServiceAuthToken({
|
||||||
|
client,
|
||||||
|
aud: 'did:web:video.bsky.app',
|
||||||
|
lxm: 'com.atproto.repo.uploadBlob',
|
||||||
|
exp: 1_700_001_800.5,
|
||||||
|
})
|
||||||
|
expect(call).toHaveBeenCalledWith(com.atproto.server.getServiceAuth, {
|
||||||
|
aud: 'did:web:video.bsky.app',
|
||||||
|
lxm: 'com.atproto.repo.uploadBlob',
|
||||||
|
exp: 1_700_001_800,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves exp undefined when the caller omits it', async () => {
|
||||||
|
const {client, call} = createClient()
|
||||||
|
await getServiceAuthToken({
|
||||||
|
client,
|
||||||
|
aud: 'did:web:video.bsky.app',
|
||||||
|
lxm: 'app.bsky.video.getUploadLimits',
|
||||||
|
})
|
||||||
|
expect(call).toHaveBeenCalledWith(com.atproto.server.getServiceAuth, {
|
||||||
|
aud: 'did:web:video.bsky.app',
|
||||||
|
lxm: 'app.bsky.video.getUploadLimits',
|
||||||
|
exp: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a non-finite exp rather than sending NaN', async () => {
|
||||||
|
const {client, call} = createClient()
|
||||||
|
await expect(
|
||||||
|
getServiceAuthToken({
|
||||||
|
client,
|
||||||
|
aud: 'did:web:video.bsky.app',
|
||||||
|
lxm: 'com.atproto.repo.uploadBlob',
|
||||||
|
exp: NaN,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('Invalid service auth exp')
|
||||||
|
expect(call).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,13 +5,6 @@ export class VideoTooLargeError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message)
|
|
||||||
this.name = 'ServerError'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UploadLimitError extends Error {
|
export class UploadLimitError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message)
|
super(message)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {AbortError} from '#/lib/async/cancelable'
|
|||||||
import {type CompressedVideo} from '#/lib/media/video/types'
|
import {type CompressedVideo} from '#/lib/media/video/types'
|
||||||
import {shouldRetryError} from '#/lib/strings/errors'
|
import {shouldRetryError} from '#/lib/strings/errors'
|
||||||
import {type app} from '#/lexicons'
|
import {type app} from '#/lexicons'
|
||||||
import {getServiceAuthToken} from '../upload.shared'
|
import {getServiceAuthToken, serviceAuthExp} from '../upload.shared'
|
||||||
import {mimeToExt} from '../util'
|
import {mimeToExt} from '../util'
|
||||||
import {
|
import {
|
||||||
abortUpload,
|
abortUpload,
|
||||||
@@ -26,15 +26,12 @@ import {createUploadPart} from './uploadPart'
|
|||||||
import {uploadParts} from './uploadParts'
|
import {uploadParts} from './uploadParts'
|
||||||
import {delay, isRetryableMultipartError, retryDelayMs} from './utils'
|
import {delay, isRetryableMultipartError, retryDelayMs} from './utils'
|
||||||
|
|
||||||
export class MultipartFallbackError extends Error {}
|
|
||||||
|
|
||||||
export async function uploadVideoMultipart({
|
export async function uploadVideoMultipart({
|
||||||
video,
|
video,
|
||||||
client,
|
client,
|
||||||
dispatchUrl,
|
dispatchUrl,
|
||||||
setProgress,
|
setProgress,
|
||||||
signal,
|
signal,
|
||||||
onStarted,
|
|
||||||
}: {
|
}: {
|
||||||
video: CompressedVideo
|
video: CompressedVideo
|
||||||
client: Client
|
client: Client
|
||||||
@@ -42,25 +39,12 @@ export async function uploadVideoMultipart({
|
|||||||
dispatchUrl: string | URL
|
dispatchUrl: string | URL
|
||||||
setProgress: (progress: number) => void
|
setProgress: (progress: number) => void
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
onStarted?: () => void
|
|
||||||
}): Promise<app.bsky.video.defs.JobStatus> {
|
}): Promise<app.bsky.video.defs.JobStatus> {
|
||||||
throwIfAborted(signal)
|
throwIfAborted(signal)
|
||||||
const tokenProvider = createTokenProvider(client, dispatchUrl, signal)
|
const tokenProvider = createTokenProvider(client, dispatchUrl, signal)
|
||||||
const token = await tokenProvider.get()
|
const token = await tokenProvider.get()
|
||||||
const name = `${nanoid(12)}.${mimeToExt(video.mimeType)}`
|
const name = `${nanoid(12)}.${mimeToExt(video.mimeType)}`
|
||||||
let session
|
const session = await startUpload({token, video, name, signal})
|
||||||
try {
|
|
||||||
session = await startUpload({token, video, name, signal})
|
|
||||||
} catch (err) {
|
|
||||||
if (signal.aborted) throw new AbortError()
|
|
||||||
// A server without multipart support, or one with the kill switch active,
|
|
||||||
// leaves no reservation behind. The legacy path remains authoritative.
|
|
||||||
throw new MultipartFallbackError(
|
|
||||||
err instanceof Error ? err.message : 'Multipart upload unavailable',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onStarted?.()
|
|
||||||
|
|
||||||
const {jobId} = session
|
const {jobId} = session
|
||||||
const abortOnCancel = () => {
|
const abortOnCancel = () => {
|
||||||
void tokenProvider
|
void tokenProvider
|
||||||
@@ -89,7 +73,7 @@ export async function uploadVideoMultipart({
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (signal.aborted) throw new AbortError()
|
if (signal.aborted) throw new AbortError()
|
||||||
return await abortThenFallbackOrResolve(
|
return await abortThenRethrowOrResolve(
|
||||||
jobId,
|
jobId,
|
||||||
await tokenProvider.get(),
|
await tokenProvider.get(),
|
||||||
err,
|
err,
|
||||||
@@ -166,14 +150,14 @@ async function finishAndRecover({
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throwIfAborted(signal)
|
throwIfAborted(signal)
|
||||||
return await abortThenFallbackOrResolve(jobId, token, err)
|
return await abortThenRethrowOrResolve(jobId, token, err)
|
||||||
}
|
}
|
||||||
createdFailures++
|
createdFailures++
|
||||||
if (createdFailures < MULTIPART_FINISH_ATTEMPTS) {
|
if (createdFailures < MULTIPART_FINISH_ATTEMPTS) {
|
||||||
await delay(500 * 2 ** (createdFailures - 1), signal)
|
await delay(500 * 2 ** (createdFailures - 1), signal)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return await abortThenFallbackOrResolve(jobId, token, finishError)
|
return await abortThenRethrowOrResolve(jobId, token, finishError)
|
||||||
case 'finishing':
|
case 'finishing':
|
||||||
// The service may have assembled the upload even though the finish
|
// The service may have assembled the upload even though the finish
|
||||||
// request failed. Poll and retry instead of starting a second upload.
|
// request failed. Poll and retry instead of starting a second upload.
|
||||||
@@ -224,16 +208,21 @@ async function getUploadStatusWithRetry(
|
|||||||
throw lastError
|
throw lastError
|
||||||
}
|
}
|
||||||
|
|
||||||
async function abortThenFallbackOrResolve(
|
/**
|
||||||
|
* Releases the reservation for an upload we can no longer finish, then surfaces
|
||||||
|
* the failure that got us here. The abort can race a service-side completion,
|
||||||
|
* so a `completed` result is resolved as a success instead.
|
||||||
|
*/
|
||||||
|
async function abortThenRethrowOrResolve(
|
||||||
jobId: string,
|
jobId: string,
|
||||||
token: string,
|
token: string,
|
||||||
cause: unknown,
|
cause: unknown,
|
||||||
): Promise<app.bsky.video.defs.JobStatus> {
|
): Promise<app.bsky.video.defs.JobStatus> {
|
||||||
const result = await abortUploadWithRetry(jobId, token)
|
const result = await abortUploadWithRetry(jobId, token)
|
||||||
if (result.state === 'aborted') {
|
if (result.state === 'aborted') {
|
||||||
throw new MultipartFallbackError(
|
throw cause instanceof Error
|
||||||
cause instanceof Error ? cause.message : 'Multipart upload failed',
|
? cause
|
||||||
)
|
: new MultipartUploadError('Multipart upload failed')
|
||||||
}
|
}
|
||||||
if (result.state === 'completed' && result.completedJobId) {
|
if (result.state === 'completed' && result.completedJobId) {
|
||||||
const status = await getUploadStatus(jobId, token)
|
const status = await getUploadStatus(jobId, token)
|
||||||
@@ -280,7 +269,7 @@ function createTokenProvider(
|
|||||||
async function get(forceRefresh = false) {
|
async function get(forceRefresh = false) {
|
||||||
if (!forceRefresh && token && Date.now() < expiresAt - 60_000) return token
|
if (!forceRefresh && token && Date.now() < expiresAt - 60_000) return token
|
||||||
if (!refresh) {
|
if (!refresh) {
|
||||||
const exp = Math.floor(Date.now() / 1000) + 60 * 30
|
const exp = serviceAuthExp()
|
||||||
refresh = getServiceAuthTokenWithRetry(client, dispatchUrl, exp, signal)
|
refresh = getServiceAuthTokenWithRetry(client, dispatchUrl, exp, signal)
|
||||||
.then(nextToken => {
|
.then(nextToken => {
|
||||||
token = nextToken
|
token = nextToken
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
import {
|
import {
|
||||||
type ProbedMetadata,
|
type ProbedMetadata,
|
||||||
type VideoCompressSkipReason,
|
type VideoCompressSkipReason,
|
||||||
type VideoUploadTransport,
|
|
||||||
} from '#/lib/media/video/types'
|
} from '#/lib/media/video/types'
|
||||||
import {Sentry} from '#/logger/sentry/lib'
|
import {Sentry} from '#/logger/sentry/lib'
|
||||||
import {type Metrics} from '#/analytics/metrics'
|
import {type Metrics} from '#/analytics/metrics'
|
||||||
@@ -46,7 +45,6 @@ export type VideoTelemetry = {
|
|||||||
compressCompleted: (video: {size: number; mimeType: string}) => void
|
compressCompleted: (video: {size: number; mimeType: string}) => void
|
||||||
compressFailed: (e: unknown) => void
|
compressFailed: (e: unknown) => void
|
||||||
uploadStarted: (bytes: number) => void
|
uploadStarted: (bytes: number) => void
|
||||||
uploadTransport: (transport: VideoUploadTransport) => void
|
|
||||||
uploadCompleted: (jobId: string) => void
|
uploadCompleted: (jobId: string) => void
|
||||||
uploadFailed: (e: unknown) => void
|
uploadFailed: (e: unknown) => void
|
||||||
processingStarted: (jobId: string) => void
|
processingStarted: (jobId: string) => void
|
||||||
@@ -72,7 +70,6 @@ export function createVideoTelemetry({
|
|||||||
let phaseStartedAt = startedAt
|
let phaseStartedAt = startedAt
|
||||||
let jobId: string | undefined
|
let jobId: string | undefined
|
||||||
let uploadBytes: number | undefined
|
let uploadBytes: number | undefined
|
||||||
let uploadTransport: VideoUploadTransport = 'legacy'
|
|
||||||
let txnEnded = false
|
let txnEnded = false
|
||||||
let abortBound = true
|
let abortBound = true
|
||||||
|
|
||||||
@@ -229,11 +226,6 @@ export function createVideoTelemetry({
|
|||||||
metric('video:upload:uploadStarted', {uploadId, engine, bytes})
|
metric('video:upload:uploadStarted', {uploadId, engine, bytes})
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadTransport(transport) {
|
|
||||||
uploadTransport = transport
|
|
||||||
phaseSpan?.setAttribute('video.upload.transport', transport)
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadCompleted(id) {
|
uploadCompleted(id) {
|
||||||
jobId = id
|
jobId = id
|
||||||
const elapsedMs = Date.now() - phaseStartedAt
|
const elapsedMs = Date.now() - phaseStartedAt
|
||||||
@@ -246,7 +238,6 @@ export function createVideoTelemetry({
|
|||||||
elapsedMs,
|
elapsedMs,
|
||||||
throughputBytesPerSec:
|
throughputBytesPerSec:
|
||||||
elapsedMs > 0 ? Math.round((bytes * 1000) / elapsedMs) : 0,
|
elapsedMs > 0 ? Math.round((bytes * 1000) / elapsedMs) : 0,
|
||||||
transport: uploadTransport,
|
|
||||||
})
|
})
|
||||||
endPhaseSpan()
|
endPhaseSpan()
|
||||||
phase = undefined
|
phase = undefined
|
||||||
@@ -259,7 +250,6 @@ export function createVideoTelemetry({
|
|||||||
bytes: uploadBytes ?? 0,
|
bytes: uploadBytes ?? 0,
|
||||||
errorClass: errorClass(e),
|
errorClass: errorClass(e),
|
||||||
elapsedMs: Date.now() - phaseStartedAt,
|
elapsedMs: Date.now() - phaseStartedAt,
|
||||||
transport: uploadTransport,
|
|
||||||
})
|
})
|
||||||
endTxn('error')
|
endTxn('error')
|
||||||
detachAbort()
|
detachAbort()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user