Merge branch 'main' into @tomekzaw/nano-icons-feed-icons

This commit is contained in:
Tomek Zawadzki
2026-08-27 10:53:01 +02:00
209 changed files with 1580 additions and 2104 deletions
@@ -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]
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+3 -3
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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",
+2 -2
View File
@@ -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",
+27
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+74 -74
View File
@@ -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':
-52
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 =
+8 -2
View File
@@ -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
+7 -4
View File
@@ -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
View File
@@ -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,
+1 -4
View File
@@ -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
-16
View File
@@ -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,
+1 -3
View File
@@ -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',
+8 -8
View File
@@ -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)
} }
+1 -6
View File
@@ -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
+1 -1
View File
@@ -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
} }
+3 -3
View File
@@ -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,
+2 -1
View File
@@ -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'
+3 -2
View File
@@ -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}
+13 -6
View File
@@ -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
}} }}
+5 -2
View File
@@ -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) =>
+76 -71
View File
@@ -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,
+3 -2
View File
@@ -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} />
} }
+2 -2
View File
@@ -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 {
+11 -8
View File
@@ -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(() => {
+1 -1
View File
@@ -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>
} }
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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()
-3
View File
@@ -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: {
+1 -1
View File
@@ -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}
+6 -3
View File
@@ -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}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
+37
View File
@@ -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()
+3 -6
View File
@@ -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}
+10 -1
View File
@@ -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]}
+9 -9
View File
@@ -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
+5 -4
View File
@@ -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()
+2 -2
View File
@@ -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],
+1 -1
View File
@@ -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])
+2 -2
View File
@@ -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={[
+1 -1
View File
@@ -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({
+1 -1
View File
@@ -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}
+16 -15
View File
@@ -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}
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -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
+37 -35
View File
@@ -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,
) )
} }
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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}
+7 -5
View File
@@ -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
-154
View File
@@ -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>
)
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
) => { ) => {
+69 -3
View File
@@ -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
+1 -1
View File
@@ -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 {
+5 -1
View File
@@ -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)
}
+1 -2
View File
@@ -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.
+7 -7
View File
@@ -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(() => {
+3 -3
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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'
} }
+2
View File
@@ -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()
})
})
-7
View File
@@ -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)
+15 -26
View File
@@ -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
-10
View File
@@ -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