Merge remote-tracking branch 'origin/main' into app-2774

# Conflicts:
#	package.json
#	pnpm-lock.yaml
This commit is contained in:
vineyardbovines
2026-08-13 09:18:23 -04:00
180 changed files with 41891 additions and 30211 deletions
+41
View File
@@ -0,0 +1,41 @@
name: Asset notices
# Verifies that every path named in ASSETS.md and NOTICE.md still exists.
#
# Those files tell forkers which assets our MIT license does not cover, and carry the third-party
# attribution notices we are required to pass along. If an asset moves and the notice is not
# updated, the notice silently stops meaning anything. This job makes that visible in review
# rather than a year later.
#
# No paths filter: the notices also reference files under src/ (e.g. the inline logo components),
# so any rename anywhere in the tree can rot a notice. The check runs in under a second.
on:
push:
branches: [main]
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
permissions:
contents: read
jobs:
check:
name: Check asset licensing notices
runs-on: ubuntu-latest
steps:
- name: ⬇️ Check out Git repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: ⚙️ Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
- name: 📜 Verify asset notices
run: node scripts/check-asset-notices.mjs
@@ -28,7 +28,7 @@ jobs:
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
@@ -29,7 +29,7 @@ jobs:
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME }}
@@ -28,7 +28,7 @@ jobs:
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
@@ -28,7 +28,7 @@ jobs:
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
@@ -28,7 +28,7 @@ jobs:
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
+2 -2
View File
@@ -88,7 +88,7 @@ jobs:
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: "temurin"
java-version: "17"
@@ -195,7 +195,7 @@ jobs:
# bundletool needs a JRE. ubuntu-latest ships a default JDK, but pin it explicitly
# like the build job so the toolchain is deterministic.
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: "temurin"
java-version: "17"
@@ -116,7 +116,7 @@ jobs:
echo "version-changed=true" >> "$GITHUB_OUTPUT"
fi
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+8 -9
View File
@@ -26,6 +26,7 @@ permissions:
pull-requests: write
issues: write
actions: read
# Required for claude-code-action's GitHub App token exchange.
id-token: write
jobs:
@@ -58,19 +59,17 @@ jobs:
with:
fetch-depth: 1
- name: ☁️ Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }}
aws-region: us-east-2
- name: 🤖 Claude
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1.0.187
env:
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
with:
use_bedrock: 'true'
# Agent Gateway service keys use Authorization: Bearer, the wire
# shape emitted by the action's OAuth-token input.
claude_code_oauth_token: ${{ secrets.AGENT_GATEWAY_CLAUDE_GH_REVIEW_KEY }}
additional_permissions: |
actions: read
track_progress: true
claude_args: |
--model global.anthropic.claude-opus-5
--model claude-opus-4-8:api
--allowedTools "mcp__github_inline_comment__create_inline_comment,mcp__github_ci__get_ci_status,mcp__github_ci__download_job_log,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
+12 -14
View File
@@ -1,7 +1,7 @@
name: claude-review
# Automatic Claude review on PR creation/update, via Bedrock (OIDC, no
# long-lived tokens). Self-contained: this intentionally uses upstream
# Automatic Claude review on PR creation/update, via Agent Gateway.
# Self-contained: this intentionally uses upstream
# claude-code-action defaults rather than the org reusable workflows in
# bluesky-social/.github (which a public repo cannot call, and whose
# customizations added no value over upstream).
@@ -16,14 +16,14 @@ permissions:
contents: read
pull-requests: write
actions: read
# Required for claude-code-action's GitHub App token exchange.
id-token: write
jobs:
review:
# Internal PRs only. This repo is public: fork PRs are the bulk of
# community traffic and MUST NOT trigger reviews (no Bedrock spend on
# unvetted code, and fork PRs can't mint the OIDC token anyway —
# belt-and-braces with this explicit guard). Branch PRs can only be
# community traffic and MUST NOT trigger reviews (no gateway spend on
# unvetted code). Branch PRs can only be
# created by people with write access, i.e. org members.
# Bot-authored PRs (dependabot, changesets) are also skipped.
if: >
@@ -44,21 +44,19 @@ jobs:
with:
fetch-depth: 1
- name: ☁️ Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }}
aws-region: us-east-2
- name: 🤖 Claude review
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1.0.187
env:
ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev
with:
use_bedrock: 'true'
# Agent Gateway service keys use Authorization: Bearer, the wire
# shape emitted by the action's OAuth-token input.
claude_code_oauth_token: ${{ secrets.AGENT_GATEWAY_CLAUDE_GH_REVIEW_KEY }}
additional_permissions: |
actions: read
track_progress: true
claude_args: |
--model global.anthropic.claude-opus-5
--model claude-opus-4-8:api
--allowedTools "mcp__github_inline_comment__create_inline_comment,mcp__github_ci__get_ci_status,mcp__github_ci__download_job_log,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
prompt: |
REPO: ${{ github.repository }}
+2 -2
View File
@@ -51,7 +51,7 @@ jobs:
v=$(grep -oE '"node":[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+"' eas.json | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
check "eas.json" "$v"
exit $rc
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
@@ -89,7 +89,7 @@ jobs:
steps:
- name: ⬇️ Check out Git repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
+2 -2
View File
@@ -51,7 +51,7 @@ jobs:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ☕️ Set up Java 17
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: "17"
@@ -176,7 +176,7 @@ jobs:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ☕️ Set up Java 17
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: "17"
@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
+3 -3
View File
@@ -35,7 +35,7 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -76,7 +76,7 @@ jobs:
with:
fetch-depth: 0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -175,7 +175,7 @@ jobs:
run: git fetch origin main:main --depth 100
if: github.event_name == 'pull_request'
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
BASE_REF: ${{ github.base_ref }}
run: git fetch origin $BASE_REF --depth=1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: 🛡️ Run zizmor
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
with:
# Annotate the PR directly instead of uploading SARIF to the
# security tab, and fail the check on any finding
+208
View File
@@ -0,0 +1,208 @@
# Asset licensing
The [MIT license](./LICENSE) in this repository covers our source code. It does not cover
every file in the tree.
Some of the images, icons, fonts, and brand assets here are licensed to Bluesky Social PBC
by third parties, or are our trademarks, or are third-party trademarks. We cannot pass those
rights on to you. This document identifies them.
We should have written this down sooner. If you have already forked this repository and
shipped one of the assets listed below, we are not treating that as bad faith on your part —
the repository did not tell you, and that is our fault. Please work through the
[If you are forking](#if-you-are-forking) checklist when you can.
Assets are scoped by directory wherever possible, so that adding a file to a carved-out
directory does not require an edit here. Individual paths are listed only where an asset does
not sit in a dedicated directory.
**The rule:** if a file is listed in this document, the MIT license does not grant you rights
to it. Everything in [Section 6](#6-third-party-assets-you-may-redistribute) is redistributable
on its own terms, which travel with the files.
---
## 1. Commissioned artwork — licensed to Bluesky only
**`assets/illustrations/`**
The landing-screen illustration, in light and dark variants, used by
`src/view/com/auth/SplashScreen.tsx` (`illustration-mobile.png` and
`illustration-mobile-dark.png`).
This artwork was commissioned by Bluesky Social PBC from illustrator Owen D. Pomery, through
his agent Brilliant Artists Ltd. Copyright remains with the artist. Our license is limited to
Bluesky's own products and channels, it is exclusive to us, and it does not permit us to
sublicense the artwork or to distribute modified versions of it.
**If you are forking this repository, replace these files.** They are not available for
third-party licensing while our license runs, so please do not contact the artist or his agent
to request permission — the answer is constrained by our agreement, not by their willingness.
See [`assets/illustrations/README.md`](./assets/illustrations/README.md).
## 2. Licensed icon system — not ours to pass on
**`assets/icons/` (top level), and the icon glyphs in `bskyembed/assets/`**
The user-interface glyphs at the top level of `assets/icons/`, plus copies of several in
`bskyembed/assets/`, come from the Central icon system by Iconists (David & Storm GbR). Bluesky
Social PBC licenses these icons for use in our own products. **That license is for our own use.
It does not include the right to pass any rights to the icons on to you.**
This is not us telling you that you cannot use these icons. It is us telling you that any right
you have to use them has to come from Iconists rather than from us, and that you should not
assume our MIT license gave you one. Licenses are available directly from
[iconists.co](https://iconists.co), and there are good openly licensed alternatives if you would
rather not buy one.
This section covers every file at the top level of `assets/icons/` **except** those named
elsewhere in this document — specifically `logomark.svg`, `newskie.svg`, `verifiedCheck.svg`,
`verifierCheck.svg`, `starterPack.svg`, `starterPack_stroke2_corner0_rounded.svg`, `custom_logo_japan.svg`,
`apple_logo.svg`, and `android_logo.svg`. The `assets/icons/flags/` and `assets/icons/community/`
subdirectories are covered by [Section 6](#6-third-party-assets-you-may-redistribute) and
[Section 5](#5-third-party-trademarks) respectively.
See [`assets/icons/README.md`](./assets/icons/README.md).
## 3. Bluesky trademarks and brand assets
Our name, logo, butterfly mark, logotype, and app icons are trademarks of Bluesky Social PBC.
They are not licensed to you under the MIT license or by this document. Use of them is governed
by our [Trademark Policy](https://bsky.social/about/support/trademarks) and [Brand Guidelines](https://bsky.social/about/support/branding).
You may refer to Bluesky by name to describe interoperability or origin — for example, "a client
for Bluesky," or "based on the Bluesky app." You may not use our marks as the identity of your
own product or service, or in any way likely to suggest that Bluesky publishes, endorses, or
supports it.
- `assets/app-icons/` — all iOS and Android app icon variants, including the `.icon` bundles
- `assets/favicon.png`
- `assets/logo.png`
- `assets/default-avatar.png`
- `assets/icon-android-foreground.png`
- `assets/icon-android-monochrome.png`
- `assets/icon-android-notification.png`
- `assets/splash/splash.png`
- `assets/splash/splash-dark.png`
- `assets/splash/android-splash-logo-white.png`
- `assets/icons/logomark.svg`
- `assets/icons/newskie.svg`
- `assets/icons/verifiedCheck.svg`
- `assets/icons/verifierCheck.svg`
- `assets/icons/starterPack.svg`, `assets/icons/starterPack_stroke2_corner0_rounded.svg`
- `bskyembed/assets/logo.svg`
- `bskyembed/assets/logo_full_name.svg`
- `bskyweb/static/favicon.png`, `favicon-16x16.png`, `favicon-32x32.png`
- `bskyweb/static/apple-touch-icon.png`
- `bskyweb/static/safari-pinned-tab.svg`
- `bskyweb/static/social-card-default.png`, `bskyweb/static/social-card-default-gradient.png`
- `bskyweb/embedr-static/favicon.png`, `favicon-16x16.png`, `favicon-32x32.png`
- `modules/BlueskyClip/Images.xcassets/AppIcon.appiconset/`
- Inline vector path data in `src/view/icons/Logo.tsx` and `src/view/icons/Logotype.tsx`
We are not asking anyone to remove these files from the repository. They are here because the
app needs them to build. Replacing them is a forker's responsibility, not ours.
## 4. Community and contest artwork — credited, but not ours to license
These are third-party artworks that appear in the app with attribution. We hold no license that
lets us pass rights to them on to you.
- `assets/kawaii.png`, `assets/kawaii_smol.png` — logo by
[@sawaratsuki.bsky.social](https://bsky.app/profile/sawaratsuki.bsky.social), shown as an
opt-in variant and credited in `src/view/shell/Drawer.tsx` and
`src/view/shell/desktop/RightNav.tsx`
- `assets/icons/custom_logo_japan.svg` — the winning entry from the Bluesky Japan logo contest,
created by a contest entrant
Replace these if you fork. If you want to use them, that is a conversation with the artist,
not with us.
## 5. Third-party trademarks
These marks belong to other companies. We include them to identify their services in our UI —
sign-in buttons, store badges, and links to third-party applications. We are neither granting
nor withholding permission, because it is not ours to give. Your use of them rests on your own
nominative-use basis or on permission from the mark owner.
- `assets/icons/apple_logo.svg` — Apple Inc.
- `assets/icons/android_logo.svg` — Google LLC
- `assets/icons/community/` — Leaflet, Offprint, pckt (`pckt.svg`, `pckt-full.svg`),
Standard.site, and Germ Network (`germ_logo.webp`)
Apple's and Google's marks in particular carry their own brand guidelines governing size,
spacing, and permitted contexts. If you ship a sign-in button or a store badge, follow theirs,
not ours.
## 6. Third-party assets you may redistribute
These are licensed on terms that permit redistribution. Nothing in this document restricts them.
Their license text travels with the files, and you must keep it there.
| Asset | Path | License | Notice |
|---|---|---|---|
| Inter typeface | `assets/fonts/inter/` | SIL Open Font License 1.1 | [`OFL.txt`](./assets/fonts/inter/OFL.txt) |
| Inter typeface (OG card service) | `bskyogcard/src/assets/fonts/` | SIL Open Font License 1.1 | [`README.md`](./bskyogcard/src/assets/fonts/README.md) |
| country-flag-icons | `assets/icons/flags/` | MIT, © @catamphetamine | [`README.md`](./assets/icons/flags/README.md) |
| Material Icons | `bskyweb/static/media/MaterialIcons.*.ttf` | Apache License 2.0 | [`NOTICE.md`](./NOTICE.md) |
Build output under `bskyweb/static/media/` also contains compiled Inter files. They are the same
OFL-licensed typeface, emitted by the web build.
**One thing to watch on Inter:** OFL 1.1 includes a Reserved Font Name provision. If you modify
or subset the font, the result cannot be distributed under the name "Inter."
See [`NOTICE.md`](./NOTICE.md) for the consolidated third-party notices.
## 7. Product imagery — treat as not licensed to you
**`assets/images/`**
Product illustration and announcement imagery — onboarding value-prop art, chat backgrounds,
feature announcement graphics, and similar.
**Treat everything in this directory as outside the MIT license and not licensed for your use.**
Some of it is commissioned work. Rather than have you guess file by file which is which, we are
drawing the line at the directory.
If you are forking, replace these or ship without them.
See [`assets/images/README.md`](./assets/images/README.md).
---
## If you are forking
You have our blessing to fork this application. To do it cleanly:
1. **Replace `assets/illustrations/`.** Commissioned artwork, licensed to Bluesky only. See
[Section 1](#1-commissioned-artwork--licensed-to-bluesky-only).
2. **Source your own UI icons.** The glyph set in `assets/icons/` is licensed to us for our own
use. See [Section 2](#2-licensed-icon-system--not-ours-to-pass-on).
3. **Replace `assets/images/`.** Treat as not licensed to you. See
[Section 7](#7-product-imagery--treat-as-not-licensed-to-you).
4. **Replace the Bluesky marks** listed in [Section 3](#3-bluesky-trademarks-and-brand-assets) —
app icons, favicons, logo files, and the inline logo paths in `src/view/icons/`.
5. **Replace or remove the community artwork** in
[Section 4](#4-community-and-contest-artwork--credited-but-not-ours-to-license).
6. **Check your own position on the third-party marks** in
[Section 5](#5-third-party-trademarks).
7. **Keep the license notices** for the assets in
[Section 6](#6-third-party-assets-you-may-redistribute).
8. **Change your branding, support links, and analytics** as described in the
[Forking guidelines](./README.md#forking-guidelines).
This list is about licensing. The [Forking guidelines](./README.md#forking-guidelines) in the
README cover the rest of what makes a fork clearly distinguishable from Bluesky, which matters
both for your users and for app store review.
## Questions
If something in this repository looks like it should be on this list and is not, or if you are
unsure whether an asset is covered, open an issue or email us and we will sort it out. We would
much rather answer the question than have someone guess.
---
*Last reviewed: August 2026. This document describes the licensing position of assets in this
repository. It is not a grant of rights, and it does not modify the [MIT license](./LICENSE) as
it applies to source code.*
+9 -1
View File
@@ -224,6 +224,7 @@ return conditional styles inline in a style array: `web({cursor: 'pointer'})`,
- Prefer prop destructuring via parameters over a const within the component.
- Prefer inline types over `Props` types or interfaces.
- Set reasonable defaults for optional props.
- Prefer the implicit global `React` for types over `type` imports.
```tsx
import {Fragment} from 'react'
@@ -232,7 +233,13 @@ import {Trans} from '@lingui/react/macro'
import {Text} from '#/components/Typography'
function MyComponent({items = []}: {items?: string[]}) {
function MyComponent({
items = [],
children,
}: {
items?: string[]
children: React.ReactNode
}) {
return (
<>
<View>
@@ -247,6 +254,7 @@ function MyComponent({items = []}: {items?: string[]}) {
<Text>{item}</Text>
</Fragment>
))}
{children}
</View>
</>
)
+1 -1
View File
@@ -9,7 +9,7 @@ WORKDIR /app
COPY ./bskyogcard/package.json ./
COPY ./bskyogcard/pnpm-lock.yaml ./
COPY ./bskyogcard/pnpm-workspace.yaml ./
RUN npm install --global pnpm@11.13.1
RUN npm install --global pnpm@11.21.0
RUN pnpm install --frozen-lockfile
COPY ./bskyogcard ./
+1 -1
View File
@@ -33,7 +33,7 @@ RUN mkdir --parents $NVM_DIR && \
RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
npm install --global pnpm@11.13.1 && \
npm install --global pnpm@11.21.0 && \
pnpm install --frozen-lockfile && \
cd bskyembed && pnpm install --frozen-lockfile && cd .. && \
pnpm intl:build && \
+62
View File
@@ -0,0 +1,62 @@
# Third-party notices
This file collects the attribution and license notices that third-party components in this
repository require us to carry. It is separate from [`ASSETS.md`](./ASSETS.md), which describes
which assets our [MIT license](./LICENSE) does and does not cover.
If you distribute this software or a fork of it, these notices need to travel with it.
---
## Material Icons
**Path:** `bskyweb/static/media/MaterialIcons.*.ttf`
**License:** Apache License, Version 2.0 — full text at [`licenses/APACHE-2.0.txt`](./licenses/APACHE-2.0.txt)
```
Copyright 2018 Google, Inc. All Rights Reserved.
```
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing permissions and limitations under
the License.
*This font is emitted into the web build by `@expo/vector-icons`, which the Expo toolchain pulls
in transitively. It is a distributed artifact rather than a source asset, which is why the notice
lives here rather than beside the file.*
## Inter
**Paths:** `assets/fonts/inter/`, `bskyogcard/src/assets/fonts/`, and compiled copies under
`bskyweb/static/media/`
**License:** SIL Open Font License, Version 1.1 — full text at
[`assets/fonts/inter/OFL.txt`](./assets/fonts/inter/OFL.txt)
```
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
```
Inter is licensed under the SIL Open Font License, Version 1.1. The license includes a Reserved
Font Name provision: a modified or subsetted version of the font may not be distributed under the
name "Inter."
## country-flag-icons
**Path:** `assets/icons/flags/`
**License:** MIT — see [`assets/icons/flags/README.md`](./assets/icons/flags/README.md)
```
Copyright (c) 2020 @catamphetamine <purecatamphetamine@gmail.com>
```
---
For the licensing position of assets that are **not** covered by our MIT license — commissioned
artwork, the licensed icon system, Bluesky trademarks, and third-party marks — see
[`ASSETS.md`](./ASSETS.md).
+14 -1
View File
@@ -57,6 +57,15 @@ Please be sure to:
- Change all branding in the repository and UI to clearly differentiate from Bluesky.
- Change any support links (feedback, email, terms of service, etc) to your own systems.
- Replace any analytics or error-collection systems with your own so we don't get super confused.
- Replace the landing-screen illustration in `assets/illustrations/`. It is commissioned artwork licensed to Bluesky alone, and our MIT license does not cover it.
- Source your own UI icons. The glyph set in `assets/icons/` is licensed to us by a third party for our own use, and that license does not extend to you.
- Replace the Bluesky logo, app icons, and other brand assets. Our trademarks are not licensed with the code.
Please read [./ASSETS.md](./ASSETS.md) before you ship. Not every file in this repository is
covered by our MIT license — some of the artwork, icons, fonts, and brand assets are licensed to
us by third parties or are trademarks, and `ASSETS.md` says which ones and what to do about them.
That file is new. Its absence is why some forks have shipped assets they did not have rights to,
and that was our omission rather than theirs.
## Security disclosures
@@ -68,7 +77,11 @@ Bluesky is an open social network built on the AT Protocol, a flexible technolog
## License (MIT)
See [./LICENSE](./LICENSE) for the full license.
See [./LICENSE](./LICENSE) for the full license, which covers the source code in this repository.
It does not cover every file. Certain images, icons, fonts, and brand assets are licensed to us
by third parties, or are trademarks, and are carved out — see [./ASSETS.md](./ASSETS.md). Required
third-party attribution notices are collected in [./NOTICE.md](./NOTICE.md).
Bluesky Social PBC has committed to a software patent non-aggression pledge. For details see [the original announcement](https://bsky.social/about/blog/10-01-2025-patent-pledge).
+16
View File
@@ -0,0 +1,16 @@
The app icons in this directory are trademarks of Bluesky Social
PBC — the butterfly mark and its variants, as shipped to the iOS App Store and Google Play.
**These are not covered by the [MIT license](../../LICENSE) that applies to the rest of this
repository.** Use of Bluesky's marks is governed by our Trademark Policy and Brand Guidelines,
not by the license on our source code.
You may say that your app is a client for Bluesky, or that it is based on the Bluesky app. You
may not use our marks as the identity of your own product, or in any way likely to suggest that
Bluesky publishes, endorses, or supports it.
If you are forking this repository, replace these files with your own icons. Shipping an app that
looks like Bluesky is also a problem under the app stores' own rules on copycat apps, quite apart
from trademark.
See [`ASSETS.md`](../../ASSETS.md) for the full asset licensing picture.
+92
View File
@@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org/
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+15
View File
@@ -0,0 +1,15 @@
[Inter](https://rsms.me/inter/) is a typeface by Rasmus
Andersson, licensed under the SIL Open Font License, Version 1.1.
The full license text is in [`OFL.txt`](./OFL.txt) and must travel with these files.
```
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
```
**You may redistribute these files.** Nothing in [`ASSETS.md`](../../../ASSETS.md) restricts them
— they are here because the OFL requires the license text to accompany the font, not because they
are carved out of our [MIT license](../../../LICENSE).
**One thing to watch:** OFL 1.1 includes a Reserved Font Name provision. If you modify or subset
the font, you may not distribute the result under the name "Inter."
+23
View File
@@ -0,0 +1,23 @@
The user-interface glyphs in this directory are from the
[Central icon system](https://iconists.co/central) by Iconists (David & Storm GbR). Bluesky
Social PBC licenses them for use in our own products. **That license is for our own use, and it
does not extend to you.**
**These icons are not covered by the [MIT license](../../LICENSE) that applies to the rest of
this repository.** If you want to use them, buy a license from
[iconists.co](https://iconists.co). There are also good openly licensed icon sets if you would
rather not.
This notice covers the SVG files at the top level of this directory. It does not cover:
- `flags/` — country-flag-icons, MIT licensed, see [`flags/README.md`](./flags/README.md)
- `community/` — third-party service marks, see [`community/README.md`](./community/README.md)
- `logomark.svg`, `newskie.svg`, `verifiedCheck.svg`, `verifierCheck.svg`, `starterPack.svg`,
`starterPack_stroke2_corner0_rounded.svg` — Bluesky trademarks and brand assets
- `custom_logo_japan.svg` — artwork by a Bluesky Japan logo contest entrant
- `apple_logo.svg`, `android_logo.svg` — Apple and Google trademarks
Each of those is described in [`ASSETS.md`](../../ASSETS.md).
Adding an icon here? If it came from Central, this notice already covers it. If it came from
anywhere else, add it to `ASSETS.md` so the notice does not go stale.
+14
View File
@@ -0,0 +1,14 @@
The icons in this directory are the marks of third-party
services that appear in the Bluesky app — [Leaflet](https://leaflet.pub),
[Offprint](https://offprint.net), [pckt](https://pckt.blog), [Standard.site](https://standard.site), and
[Germ Network](https://germnetwork.com).
**These marks belong to their respective owners.** They are not Bluesky trademarks, they are not
covered by the [MIT license](../../../LICENSE) that applies to the rest of this repository, and we
are not in a position to grant or withhold permission to use them. We include them to identify
those services in our UI.
If you fork this repository and keep these icons, your use of them rests on your own
nominative-use basis or on permission from the mark owner.
See [`ASSETS.md`](../../../ASSETS.md) for the full asset licensing picture.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V5h14v14H5Zm10.725-5.2c0 .566-.283.872-.802.872-.538 0-.848-.318-.848-.872v-3.635c0-.512.314-.826.82-.826h2.496c.35 0 .609.272.609.64 0 .369-.26.629-.609.629h-1.666v.973h1.47c.365 0 .608.248.608.613 0 .36-.247.613-.608.613h-1.47v.993Zm-3.367.872c.526 0 .813-.31.813-.872v-3.627c0-.558-.295-.873-.825-.873s-.825.31-.825.873V13.8c0 .558.302.872.837.872Zm-3.879.078C6.92 14.75 6 13.827 6 12.287v-.617c0-1.47.955-2.42 2.472-2.42.589 0 1.139.147 1.548.388.404.236.664.562.664.915 0 .373-.271.636-.656.636a.8.8 0 0 1-.41-.108 2 2 0 0 1-.271-.177c-.208-.148-.421-.3-.746-.3-.644 0-.95.38-.95 1.155v.52c0 .768.306 1.168.903 1.168.436 0 .735-.248.735-.61v-.061h-.146c-.412 0-.632-.194-.632-.551 0-.353.216-.535.632-.535h.806c.617 0 .884.256.884.834v.166c0 1.253-.92 2.06-2.354 2.06Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 992 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6 12.287c0 1.54.92 2.463 2.48 2.463 1.434 0 2.353-.807 2.353-2.06v-.166c0-.578-.267-.834-.884-.834h-.806c-.416 0-.632.182-.632.535 0 .357.22.55.632.55h.146v.063c0 .36-.299.609-.735.609-.597 0-.904-.4-.904-1.168v-.52c0-.775.307-1.155.951-1.155.464 0 .7.31 1.018.477a.8.8 0 0 0 .409.108c.385 0 .656-.263.656-.636 0-.353-.26-.679-.664-.915-.409-.24-.96-.388-1.548-.388C6.955 9.25 6 10.2 6 11.67v.617Zm6.358 2.385c.526 0 .813-.31.813-.872v-3.627c0-.558-.295-.873-.825-.873s-.825.31-.825.873V13.8c0 .558.302.872.837.872Zm2.565 0c.519 0 .802-.306.802-.872v-.993h1.47c.361 0 .608-.252.608-.613 0-.365-.243-.613-.608-.613h-1.47v-.973h1.666c.35 0 .609-.26.609-.629 0-.368-.26-.64-.609-.64h-2.495c-.507 0-.821.314-.821.826V13.8c0 .554.31.872.848.872ZM19 7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7Zm2 10a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v10Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 514 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M19 15.355c-1.112.696-2.385 1.1-4 1.1-2.81 0-4.796-1.756-6.312-3.39a1 1 0 0 0-1.273-.16L5 14.71V17a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1.646ZM16 9a1 1 0 1 0-2 0 1 1 0 0 0 2 0Zm3-2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v5.213l1.246-.932.044-.031a3 3 0 0 1 3.862.455c1.468 1.581 2.942 2.75 4.848 2.75 1.704 0 2.854-.558 4-1.621V7Zm-1 2a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v10Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19H6.414l1.293 1.293a1 1 0 0 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 1 1 1.414 1.414L6.414 17h9.172L5.909 7.323A2 2 0 0 0 5 9v2a1 1 0 0 1-2 0V9c0-1.255.579-2.372 1.482-3.104L2.293 3.707a1 1 0 0 1 0-1.414M20 12a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1m-3.707-9.707a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H11a1 1 0 0 1 0-2h6.586l-1.293-1.293a1 1 0 0 1 0-1.414"/></svg>

After

Width:  |  Height:  |  Size: 575 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M10.775 2.74a3 3 0 0 1 3.524 2.158l.57 2.13a7.5 7.5 0 1 1-6.12 10.744l-2.228.598-.15.036a3 3 0 0 1-3.48-2.009l-.044-.148L.777 8.52A3 3 0 0 1 2.75 4.89l.148-.044 7.728-2.07.149-.037Zm4.624 6.262.97 3.624.037.149a3 3 0 0 1-2.009 3.48l-.148.044-3.517.942A5.5 5.5 0 1 0 15.5 9l-.101.002Zm-3.031-3.586a1 1 0 0 0-1.225-.707l-7.727 2.07a1 1 0 0 0-.707 1.225l2.07 7.727a1 1 0 0 0 1.225.707l2.123-.57a7.504 7.504 0 0 1 4.788-8.412l-.547-2.04ZM13.435 9.4a5.5 5.5 0 0 0-3.37 5.948l3.666-.981a1 1 0 0 0 .707-1.225L13.435 9.4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 656 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 153 133"><path fill="url(#a)" fill-rule="evenodd" d="m60.196 105.445-18.1 4.85c-11.73 3.143-23.788-3.819-26.931-15.55L1.19 42.597c-3.143-11.731 3.819-23.79 15.55-26.932L68.889 1.69C80.62-1.452 92.68 5.51 95.821 17.241l4.667 17.416a50 50 0 0 1 3.522-.125c27.053 0 48.984 21.931 48.984 48.984S131.063 132.5 104.01 132.5c-19.17 0-35.769-11.012-43.814-27.055ZM19.457 25.804 71.606 11.83c6.131-1.643 12.434 1.996 14.076 8.127l4.44 16.571c-20.289 5.987-35.096 24.758-35.096 46.988 0 4.157.517 8.193 1.492 12.047l-17.138 4.593c-6.131 1.642-12.434-1.996-14.077-8.128L11.33 39.88c-1.643-6.131 1.996-12.434 8.127-14.077Zm83.812 19.232q.369-.007.741-.007c21.256 0 38.487 17.231 38.487 38.487s-17.231 38.488-38.487 38.488c-14.29 0-26.76-7.788-33.4-19.35l23.635-6.333c11.731-3.143 18.693-15.2 15.55-26.932l-6.526-24.353Zm-10.428 1.638 6.815 25.432c1.642 6.131-1.996 12.434-8.128 14.076l-24.867 6.664a38.6 38.6 0 0 1-1.139-9.33c0-17.372 11.51-32.056 27.32-36.842Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="76.715" x2="76.715" y1=".937" y2="132.5" gradientUnits="userSpaceOnUse"><stop stop-color="#0a7aff"/><stop offset="1" stop-color="#59b9ff"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

+19
View File
@@ -0,0 +1,19 @@
The landing screen illustration in this directory was commissioned by Bluesky Social PBC from
illustrator [Owen D. Pomery](https://owenpomery.com/work), through his agent
[Brilliant Artists Ltd](https://brilliantartists.co.uk/). Copyright in the artwork remains with
the artist.
**This artwork is not covered by the [MIT license](../../LICENSE) that applies to the rest of
this repository.** Our license is limited to Bluesky's own products and channels, it is exclusive
to us, and it does not permit us to sublicense the artwork or to distribute modified versions
of it.
If you are forking this repository, replace these files. Because our license is exclusive, the
artwork is not available for separate third-party licensing while that license runs — please do
not approach the artist or his agent for permission.
Adding a file here? It is carved out of the MIT license by directory, so a new file inherits
that treatment automatically. If the file is not commissioned artwork covered by this notice,
put it somewhere else.
See [`ASSETS.md`](../../ASSETS.md) for the full asset licensing picture.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+14
View File
@@ -0,0 +1,14 @@
The product imagery in this directory — onboarding illustrations,
chat backgrounds, feature announcement graphics, and similar — is **not covered by the
[MIT license](../../LICENSE)** that applies to the rest of this repository.
Some of it is commissioned artwork that we are not able to sublicense. Rather than have you work
out file by file which is which, treat the whole directory as not licensed for your use.
If you are forking this repository, replace these files or ship without them.
**Internal note:** this directory is carved out of the MIT license as a whole. If you are adding
an asset that we do want forks to be able to reuse, put it somewhere else rather than adding an
exception here — the carve-out only stays maintainable if the directory boundary means something.
See [`ASSETS.md`](../../ASSETS.md) for the full asset licensing picture.
+1 -1
View File
@@ -6,7 +6,7 @@
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "11.13.1",
"version": "11.21.0",
"onFail": "warn"
}
},
+38 -38
View File
@@ -7,52 +7,52 @@ importers:
configDependencies: {}
packageManagerDependencies:
'@pnpm/exe':
specifier: 11.13.1
version: 11.13.1
specifier: 11.21.0
version: 11.21.0
pnpm:
specifier: 11.13.1
version: 11.13.1
specifier: 11.21.0
version: 11.21.0
packages:
'@pnpm/exe@11.13.1':
resolution: {integrity: sha512-P4euEK6lOFnd5oTHEc5M/HhvyF4XUhTnVsklEcM6rmY0QJxPD6xbT+u1+gskEIBp4nSRorz20IJQtAU1Nerggg==}
'@pnpm/exe@11.21.0':
resolution: {integrity: sha512-zawQxIewH1od72HhlmXWq3No6XyuWn+nMvQ9BjWWGNBVskmS+RDlTu7ey2ruL650PbbyuCATYSal1DaXFKBdcw==}
hasBin: true
'@pnpm/linux-arm64@11.13.1':
resolution: {integrity: sha512-wB8zloqrYrudPyuA5qbuTCnJGe4eETPwqOjoPjoyyyvA4zFI5XfLpxgqOOcaY5UJBoqzckcGpRVDwhSRfsQ/6A==}
'@pnpm/linux-arm64@11.21.0':
resolution: {integrity: sha512-gOSfQKr6kZjEwHyoRwMt9qrqQ9sqbZmUm2hbgJJG8bp0ZR9YkQ4BZV2k4qlQA2jtsmHV1u1MwiaLcuK7DauvBg==}
cpu: [arm64]
os: [linux]
'@pnpm/linux-x64@11.13.1':
resolution: {integrity: sha512-A+wnEvzfWEvanXiwww3tnOPmtjPSrrf5tOP6vk8+K0BRFEe/Df0oPytm2nWgGcn5iwPnqtr1Btkof913McnSPA==}
'@pnpm/linux-x64@11.21.0':
resolution: {integrity: sha512-X+kBR8yscKyhhElO+WLrb6sFbl/3Ow70B+6fqZUYI8T8wtmlCw5GtcPXVBJPDJcLN5joe227h7lyCCZo4tdKdw==}
cpu: [x64]
os: [linux]
'@pnpm/linuxstatic-arm64@11.13.1':
resolution: {integrity: sha512-k4t65VeqRX4COMFe45TF58CVmCpmAsKZShaR1HobmUeleo98mWTctggKolrA2MHcVUeSS+12yB5Urb3uDazhmw==}
'@pnpm/linuxstatic-arm64@11.21.0':
resolution: {integrity: sha512-IUJfAclH0b3QxaHuQuVxXQIzEkDtTm0C+G3tgG0ET5tDRGc7wH7eU0GEM75ojHOwzqv7s0y00xPVVCIsxUM4Nw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/linuxstatic-x64@11.13.1':
resolution: {integrity: sha512-A65GqPzwCl0bAMk3kRWfbjSRBm5RRaqR2oMxV/9AYZrwO0X9yEfngbLBISCPHjt6/Qe4nH7DFemyhy6yODYwEw==}
'@pnpm/linuxstatic-x64@11.21.0':
resolution: {integrity: sha512-6Y2u+AfOUuTqWgTCpFhySL8HAcONDucCFixKle9tWoW7bm8RF0+fwQBRWNWGdx+Toau07wZ1LNZPqN8gpgeBDQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/macos-arm64@11.13.1':
resolution: {integrity: sha512-MJvOtyGOWSfBoqdVEfAH8ljmHs13mt82k/UxN4f+q7koDxJRR2n4Nie6Og6RwbnbaubCz0Fh2bTeL1+MxDSFpA==}
'@pnpm/macos-arm64@11.21.0':
resolution: {integrity: sha512-sLMGvVJXWdhFAouY2icjeZ2VCFmyPPZvvtHkfj1oeCWGppsbWcP5cExSw9yU1Uw7ALwrV0I2lRZv33yWYfDtcQ==}
cpu: [arm64]
os: [darwin]
'@pnpm/win-arm64@11.13.1':
resolution: {integrity: sha512-kl/g1cCKOJPe4HntspyrAJW0LRco0UHnVfxHSspezo4Zj4AanJAZ8WzLqfa6/w3lBSKHTEN4x0pb3m4J7B7Vpw==}
'@pnpm/win-arm64@11.21.0':
resolution: {integrity: sha512-79Nc+YI5B2ddH5MQD2YITL/PKnmXdcQKwmwx0HaD4QnsCco8DFaTho242e5sd9QfXKxYRvB9AOnuqIV4VjhAAw==}
cpu: [arm64]
os: [win32]
'@pnpm/win-x64@11.13.1':
resolution: {integrity: sha512-Bcb14NeBlbHS2Gq1qr8VnCiAz5eC1lYzXOls7zH0bnV0Taaj4/xyfm0HVO4dn9R2TQtVtz1qnBZHHL9PDFumqQ==}
'@pnpm/win-x64@11.21.0':
resolution: {integrity: sha512-zT3TufmVOroWPrzXTPPPgYvIsTZIsK13kjpmgXlICyEFrhd16RFLoTWFhP+8UqHXpTNmVbtTHzg+fEVYy9rlEQ==}
cpu: [x64]
os: [win32]
@@ -116,45 +116,45 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
pnpm@11.13.1:
resolution: {integrity: sha512-svx2g7imUlQU59E+G6KMqt3elr9m7FQL+ut+cCuB8+C+TR8pXt9/n+A5Z0Co3ORQnFgt33mJH0VD/qMtN2RfJQ==}
pnpm@11.21.0:
resolution: {integrity: sha512-UhcFvOaJkk6scvWjWHEi82JonvZXHlW6gAdv1jfBETLs/62ib61Op5xIW/3b/T1aKlsFgFp36JPeceyKbMo7sQ==}
engines: {node: '>=22.13'}
hasBin: true
snapshots:
'@pnpm/exe@11.13.1':
'@pnpm/exe@11.21.0':
dependencies:
'@reflink/reflink': 0.1.19
detect-libc: 2.1.2
optionalDependencies:
'@pnpm/linux-arm64': 11.13.1
'@pnpm/linux-x64': 11.13.1
'@pnpm/linuxstatic-arm64': 11.13.1
'@pnpm/linuxstatic-x64': 11.13.1
'@pnpm/macos-arm64': 11.13.1
'@pnpm/win-arm64': 11.13.1
'@pnpm/win-x64': 11.13.1
'@pnpm/linux-arm64': 11.21.0
'@pnpm/linux-x64': 11.21.0
'@pnpm/linuxstatic-arm64': 11.21.0
'@pnpm/linuxstatic-x64': 11.21.0
'@pnpm/macos-arm64': 11.21.0
'@pnpm/win-arm64': 11.21.0
'@pnpm/win-x64': 11.21.0
'@pnpm/linux-arm64@11.13.1':
'@pnpm/linux-arm64@11.21.0':
optional: true
'@pnpm/linux-x64@11.13.1':
'@pnpm/linux-x64@11.21.0':
optional: true
'@pnpm/linuxstatic-arm64@11.13.1':
'@pnpm/linuxstatic-arm64@11.21.0':
optional: true
'@pnpm/linuxstatic-x64@11.13.1':
'@pnpm/linuxstatic-x64@11.21.0':
optional: true
'@pnpm/macos-arm64@11.13.1':
'@pnpm/macos-arm64@11.21.0':
optional: true
'@pnpm/win-arm64@11.13.1':
'@pnpm/win-arm64@11.21.0':
optional: true
'@pnpm/win-x64@11.13.1':
'@pnpm/win-x64@11.21.0':
optional: true
'@reflink/reflink-darwin-arm64@0.1.19':
@@ -194,7 +194,7 @@ snapshots:
detect-libc@2.1.2: {}
pnpm@11.13.1: {}
pnpm@11.21.0: {}
---
lockfileVersion: '9.0'
+16
View File
@@ -0,0 +1,16 @@
These are subsets of
[Inter](https://rsms.me/inter/) by Rasmus Andersson, used by the OG card rendering service.
Inter is licensed under the SIL Open Font License, Version 1.1. The full license text is at
[`assets/fonts/inter/OFL.txt`](../../../../assets/fonts/inter/OFL.txt) and must travel with these
files.
```
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
```
**You may redistribute these files.** They are not carved out of our
[MIT license](../../../../LICENSE) — this notice exists because the OFL requires it.
**One thing to watch:** OFL 1.1 includes a Reserved Font Name provision. If you modify or subset
the font further, you may not distribute the result under the name "Inter."
+1
View File
@@ -6,6 +6,7 @@
# codes are used for rate-limiting. Up to a handful concurrent requests should
# be ok.
User-Agent: *
Disallow: /intent/compose
Allow: /
Sitemap: https://bsky.app/sitemap/users.xml.gz
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-16
View File
@@ -244,17 +244,6 @@
"count": 3
}
},
"src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx": {
"typescript/no-floating-promises": {
"count": 2
},
"typescript/no-unsafe-enum-comparison": {
"count": 1
},
"typescript/no-unsafe-member-access": {
"count": 1
}
},
"src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils.tsx": {
"typescript/no-explicit-any": {
"count": 2
@@ -992,11 +981,6 @@
"count": 3
}
},
"src/screens/Profile/Header/Shell.tsx": {
"typescript/no-explicit-any": {
"count": 1
}
},
"src/screens/Profile/KnownFollowers.tsx": {
"typescript/no-misused-promises": {
"count": 2
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.130.0",
"version": "1.131.0",
"private": true,
"engines": {
"node": ">=24.18.0"
@@ -8,7 +8,7 @@
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "11.13.1",
"version": "11.21.0",
"onFail": "warn"
},
"runtime": {
@@ -97,7 +97,7 @@
},
"dependencies": {
"@atproto/api": "0.20.40",
"@atproto/common-web": "0.5.6",
"@atproto/common-web": "0.5.7",
"@atproto/syntax": "0.7.2",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
@@ -108,7 +108,7 @@
"@bsky.app/expo-scroll-edge-effect": "^0.1.9",
"@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/peek-menu": "^0.3.2",
"@bsky.app/react-native-uitextview": "^2.6.0",
"@bsky.app/react-native-uitextview": "^2.7.0",
"@bsky.app/sift": "^0.3.9",
"@bsky.app/tapper": "^0.6.1",
"@bsky.app/video": "0.3.6",
+86
View File
@@ -0,0 +1,86 @@
diff --git a/lib/module/threads.js b/lib/module/threads.js
index dd3a7f1ab12e5a8030af7f17b6a7a891e0b645d2..7d48ccbf95f00724db6b083a7155f25aadd6b8f5 100644
--- a/lib/module/threads.js
+++ b/lib/module/threads.js
@@ -2,7 +2,6 @@
import { WorkletsError } from './debug/WorkletsError';
import { IS_JEST } from './platformChecker';
-import { mockedRequestAnimationFrame } from './runLoop/uiRuntime/mockedRequestAnimationFrame';
export function scheduleOnUI(worklet, ...args) {
enqueueUI(worklet, args);
}
@@ -24,38 +23,50 @@ export function scheduleOnRN(fun, ...args) {
queueMicrotask(args.length ? () => fun(...args) : fun);
}
export function runOnUIAsync(worklet, ...args) {
- return new Promise(resolve => {
- enqueueUI(worklet, args, resolve);
+ return new Promise((resolve, reject) => {
+ enqueueUI(worklet, args, resolve, reject);
});
}
let runOnUIQueue = [];
-function enqueueUI(worklet, args, resolve) {
- if (IS_JEST) {
- mockedRequestAnimationFrame(() => {
- const result = worklet(...args);
- resolve?.(result);
- });
- } else {
- const job = [worklet, args, resolve];
- runOnUIQueue.push(job);
- if (runOnUIQueue.length === 1) {
+function enqueueUI(worklet, args, resolve, reject) {
+ const job = [worklet, args, resolve, reject];
+ runOnUIQueue.push(job);
+ if (runOnUIQueue.length === 1) {
+ if (IS_JEST) {
flushUIQueue();
+ } else {
+ queueMicrotask(flushUIQueue);
}
}
}
+let offset = 0;
function flushUIQueue() {
- queueMicrotask(() => {
- const queue = runOnUIQueue;
- runOnUIQueue = [];
- requestAnimationFrameImpl(() => {
- queue.forEach(([workletFunction, workletArgs, jobResolve]) => {
- const result = workletFunction(...workletArgs);
- if (jobResolve) {
- jobResolve(result);
+ const queue = runOnUIQueue;
+ runOnUIQueue = [];
+ requestAnimationFrame(() => {
+ offset = 0;
+ while (queue.length > offset) {
+ try {
+ drainUIQueue(queue);
+ } catch (e) {
+ const [, , , jobReject] = queue[offset - 1];
+ if (jobReject) {
+ jobReject(e);
+ } else {
+ console.error(e);
}
- });
- });
+ }
+ }
});
}
-const requestAnimationFrameImpl = !globalThis.requestAnimationFrame ? mockedRequestAnimationFrame : globalThis.requestAnimationFrame;
+function drainUIQueue(queue) {
+ while (queue.length > offset) {
+ const [workletFunction, workletArgs, jobResolve] = queue[offset];
+ offset++;
+ const result = workletFunction(...workletArgs);
+ if (jobResolve) {
+ jobResolve(result);
+ }
+ }
+}
//# sourceMappingURL=threads.js.map
@@ -0,0 +1,37 @@
# react-native-worklets@0.8.3.patch
Backport of https://github.com/software-mansion/react-native-reanimated/pull/10167
("fix(Worklets): web scheduleOnUI implementation on errors").
## The bug
On web, `scheduleOnUI`/`runOnUI` batch their callbacks per animation frame and
run them with `queue.forEach(...)`. If any callback in the batch throws,
`forEach` aborts immediately and every callback still queued after it is
silently dropped - it never runs, and any `runOnUIAsync` promise for it never
resolves or rejects.
Reanimated's own internals rely on those callbacks running in order (e.g. to
populate `frameCallbackRegistry`), so a single throwing worklet in a batch can
leave later, unrelated frame callbacks referencing state that was never set
up, surfacing as:
```
TypeError: can't access property "startTime", this.frameCallbackRegistry.get(...) is undefined
```
## The fix
Replace the `forEach` batch runner with a `while` loop (`drainUIQueue`) that
tracks its position via an `offset`, wrapped in a `try`/`catch`. A throw now
only aborts the *current* callback: the loop resumes at the next queued job
instead of abandoning the rest of the batch. Errors are routed to the
matching `runOnUIAsync` promise's `reject` (a new second argument threaded
through `enqueueUI`) if there is one, or `console.error`-ed otherwise, rather
than crashing the whole frame.
Only `lib/module/threads.js` (the compiled web entry point actually loaded by
the app's webpack build) is patched - `src/threads.ts` is unused here since
this repo's web build resolves the package's `module` field, and native
platforms use the separate `threads.native.ts` implementation untouched by
this PR.
+1610 -1299
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -39,6 +39,7 @@ patchedDependencies:
react-native-screens@4.24.0: patches/react-native-screens@4.24.0.patch
'react-native-svg@15.12.1': patches/react-native-svg@15.12.1.patch
'react-native-view-shot@4.0.3': patches/react-native-view-shot@4.0.3.patch
react-native-worklets@0.8.3: patches/react-native-worklets@0.8.3.patch
'react-native@0.81.5': patches/react-native@0.81.5.patch
minimumReleaseAgeExclude:
- '@atproto/*'
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env node
/**
* Verifies that every path named in ASSETS.md and NOTICE.md still exists.
*
* These two files tell forkers which assets our MIT license does not cover. If a file moves or
* is renamed and the notice is not updated, the notice quietly stops meaning anything which is
* the failure mode that produced the problem in the first place. This check makes that loud.
*
* Usage: node scripts/check-asset-notices.mjs
* Exits 1 if any referenced path is missing.
*/
import {readFileSync, existsSync, readdirSync} from 'node:fs'
import {dirname, join, resolve} from 'node:path'
import {fileURLToPath} from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const SOURCES = ['ASSETS.md', 'NOTICE.md']
/** Tokens that look like paths but are not: package names, URLs, prose. */
const NOT_A_PATH = /^(https?:|@|[A-Z_]+$)/
/** Extract `backticked` tokens that are plausibly repo paths. */
function extractPaths(markdown) {
const out = new Set()
for (const [, token] of markdown.matchAll(/`([^`\n]+)`/g)) {
const t = token.trim()
// Only validate tokens that are unambiguously repo-relative paths. Bare filenames that
// appear in comma-separated lists ("`bskyweb/static/favicon.png`, `favicon-16x16.png`")
// are reported separately rather than guessed at.
if (!t.includes('/') || NOT_A_PATH.test(t) || t.includes(' ')) continue
out.add(t.replace(/^\.\//, ''))
}
return [...out]
}
/** Resolve a path that may end in `/` (directory) or contain a single `*` glob segment. */
function pathExists(p) {
const full = join(ROOT, p)
if (existsSync(full)) return true
if (!p.includes('*')) return false
// Handle one glob in the final segment, e.g. bskyweb/static/media/MaterialIcons.*.ttf
const dir = dirname(full)
const pattern = p.split('/').pop()
if (!existsSync(dir)) return false
const re = new RegExp(
'^' +
pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') +
'$',
)
return readdirSync(dir).some(f => re.test(f))
}
let missing = 0
let checked = 0
const skipped = new Set()
for (const source of SOURCES) {
const file = join(ROOT, source)
if (!existsSync(file)) {
console.error(`${source} is missing`)
missing++
continue
}
const markdown = readFileSync(file, 'utf8')
for (const [, token] of markdown.matchAll(/`([^`\n]+)`/g)) {
const t = token.trim()
if (!t.includes('/') && /\.[a-z0-9]{2,5}$/i.test(t)) skipped.add(t)
}
for (const p of extractPaths(markdown)) {
checked++
if (!pathExists(p)) {
console.error(`${source} references a path that does not exist: ${p}`)
missing++
}
}
}
console.log(`Checked ${checked} referenced paths across ${SOURCES.join(', ')}.`)
if (skipped.size) {
console.log(
`Note: ${skipped.size} bare filename(s) not verified (they are relative to a nearby path): ` +
[...skipped].sort().join(', '),
)
}
if (missing) {
console.error(
`\n${missing} problem(s) found. An asset named in a licensing notice has moved or been ` +
`removed. Update the notice — do not just silence this check.`,
)
process.exit(1)
}
console.log('All referenced paths exist.')
+6 -6
View File
@@ -67,13 +67,14 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
import {PostThreadScreen} from '#/view/screens/PostThread'
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
import {ProfileScreen} from '#/view/screens/Profile'
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
import {StorybookScreen} from '#/view/screens/Storybook'
import {SupportScreen} from '#/view/screens/Support'
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
import {BookmarksScreen} from '#/screens/Bookmarks'
import {CustomFeedScreen} from '#/screens/CustomFeed'
import {CustomFeedLikedByScreen} from '#/screens/CustomFeed/CustomFeedLikedBy'
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
import {FindContactsFlowScreen} from '#/screens/FindContactsFlowScreen'
import HashtagScreen from '#/screens/Hashtag'
@@ -92,7 +93,6 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
@@ -288,13 +288,13 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
})}
/>
<Stack.Screen
name="ProfileFeed"
getComponent={() => ProfileFeedScreen}
name="CustomFeed"
getComponent={() => CustomFeedScreen}
options={{title: title(msg`Feed`)}}
/>
<Stack.Screen
name="ProfileFeedLikedBy"
getComponent={() => ProfileFeedLikedByScreen}
name="CustomFeedLikedBy"
getComponent={() => CustomFeedLikedByScreen}
options={{title: title(msg`Liked by`)}}
/>
<Stack.Screen
+23 -5
View File
@@ -77,19 +77,37 @@ export function getFeatures() {
export function getFeatureDescription(feature: Features, i18n: I18n) {
switch (feature) {
case Features.PostThreadKnownLikersEnable:
case Features.VideoAllow10MinuteEnable:
return {
key: feature,
name: i18n._(
msg({
message: 'Social proofing on posts',
comment: 'Name for a feature flag',
message: 'Longer videos',
comment: 'Name for a feature flag (longer videos)',
}),
),
description: i18n._(
msg({
message: 'Spot posts your friends and follows have liked.',
comment: 'Description of a feature flag (Social proofing on posts)',
message: 'Enable 10-minute video uploads.',
comment: 'Description of a feature flag (10-minute video uploads)',
}),
),
}
case Features.CanonicalPostNumberingEnable:
return {
key: feature,
name: i18n._(
msg({
message: 'Thread numbering',
comment:
'Name for a feature flag (See numbered badges (1/3, 2/3, etc.) on posts in a thread by the same author.)',
}),
),
description: i18n._(
msg({
message:
'See numbered badges (1/3, 2/3, etc.) on posts in a thread by the same author.',
comment: 'Description of a feature flag (Thread numbering)',
}),
),
}
+21
View File
@@ -1,3 +1,4 @@
import {useSyncExternalStore} from 'react'
import uuid from 'react-native-uuid'
import AsyncStorage from '@react-native-async-storage/async-storage'
@@ -24,3 +25,23 @@ export function getDeviceIdOrThrow() {
}
return id
}
function subscribeToDeviceId(onChange: () => void) {
const sub = device.addOnValueChangedListener(['deviceId'], onChange)
return () => sub.remove()
}
/**
* Reads the device ID for use during render. The app awaits
* `getAndMigrateDeviceId` before booting (see `setupDeviceId` in
* `analytics/index.tsx`), so this is normally set on first read.
*
* Subscribing rather than reading storage directly means a late write - a
* caller that mounts before the migration resolves - still propagates, instead
* of leaving consumers pinned to `undefined` for the lifetime of the component.
* `useSyncExternalStore` re-evaluates the snapshot every render, so there's no
* gap between the initial read and the subscription.
*/
export function useDeviceId() {
return useSyncExternalStore(subscribeToDeviceId, getDeviceId)
}
+4 -1
View File
@@ -20,6 +20,7 @@ import {
getAndMigrateDeviceId,
getDeviceId,
getInitialSessionId,
useDeviceId,
useSessionId,
} from '#/analytics/identifiers'
import {
@@ -178,6 +179,7 @@ export function AnalyticsContext({
)
}
}
const deviceId = useDeviceId() ?? 'unknown'
const sessionId = useSessionId()
const geolocation = useGeolocationServiceResponse()
const parentContext = useContext(Context)
@@ -197,6 +199,7 @@ export function AnalyticsContext({
...metadata,
base: {
...parentContext.metadata.base,
deviceId,
sessionId,
isBetaUser,
},
@@ -217,7 +220,7 @@ export function AnalyticsContext({
},
}
return context
}, [parentContext, metadata, sessionId, isBetaUser, geolocation])
}, [parentContext, metadata, deviceId, sessionId, isBetaUser, geolocation])
return <Context.Provider value={childContext}>{children}</Context.Provider>
}
+24
View File
@@ -510,6 +510,8 @@ export type Events = {
}
'profile:mute': {}
'profile:unmute': {}
'profile:muteReposts': {}
'profile:unmuteReposts': {}
'profile:block': {}
'profile:unblock': {}
'suggestedUser:follow': {
@@ -855,6 +857,7 @@ export type Events = {
reason: string
labeler: string
details: boolean
videoTimestamp: boolean
}
'reportDialog:failure': {}
@@ -1506,4 +1509,25 @@ export type Events = {
}
'post:likedBy:click': {}
/*
* Beta features settings screen
*/
// user toggled "Enable beta features"; fired only after the preference
// write succeeds
'betaFeatures:toggle': {
enabled: boolean
/** Gate keys of beta features active for this user at toggle time */
betaFeatureKeys: string[]
}
// user pressed the "Share feedback" button, opening the dialog
'betaFeatures:feedback:open': {
betaFeatureKeys: string[]
}
// user submitted feedback and it was sent successfully
'betaFeatures:feedback:submit': {
betaFeatureKeys: string[]
feedbackLength: number
}
}
+3 -1
View File
@@ -62,6 +62,7 @@ export const Input = createInput(TextInput)
export function Outer({
children,
control,
onOpen,
onClose,
nativeOptions,
testID,
@@ -97,9 +98,10 @@ export function Outer({
const open = useCallback<DialogControlProps['open']>(() => {
// Run any leftover callbacks that might have been queued up before calling `.open()`
callQueuedCallbacks()
onOpen?.()
setDialogIsOpen(control.id, true)
ref.current?.present()
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
}, [setDialogIsOpen, control.id, callQueuedCallbacks, onOpen])
// This is the function that we call when we want to dismiss the dialog.
const close = useCallback<DialogControlProps['close']>(cb => {
+3 -1
View File
@@ -52,6 +52,7 @@ const preventDefault = (e: any) => e.preventDefault()
export function Outer({
children,
control,
onOpen,
onClose,
webOptions,
}: React.PropsWithChildren<DialogOuterProps>) {
@@ -61,9 +62,10 @@ export function Outer({
const {setDialogIsOpen} = useDialogStateControlContext()
const open = useCallback(() => {
onOpen?.()
setDialogIsOpen(control.id, true)
setIsOpen(true)
}, [setIsOpen, setDialogIsOpen, control.id])
}, [setIsOpen, setDialogIsOpen, control.id, onOpen])
const close = useCallback<DialogControlProps['close']>(
cb => {
+1
View File
@@ -60,6 +60,7 @@ export type DialogControlOpenOptions = {
export type DialogOuterProps = {
control: DialogControlProps
onOpen?: () => void
onClose?: () => void
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
webOptions?: {
+24 -1
View File
@@ -1,12 +1,13 @@
import {useMemo} from 'react'
import {View} from 'react-native'
import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api'
import {Trans} from '@lingui/react/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
import {Button} from '#/components/Button'
import {RepostStrike_Stroke2_Corner0_Rounded as RepostStrikeIcon} from '#/components/icons/Repost'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
@@ -215,3 +216,25 @@ export function FollowsYou({size = 'sm'}: CommonProps) {
</View>
)
}
export function MutedOnlyReposts(props: CommonProps) {
const t = useTheme()
const {t: l} = useLingui()
const size = props.size === 'lg' ? 16 : 12
return (
<LabelBase
{...props}
label={l`Reposts Hidden`}
icon={
<RepostStrikeIcon
width={size}
fill={t.atoms.text_contrast_medium.color}
/>
}
disabled={true}
onPress={() => {}}
/>
)
}
@@ -92,7 +92,7 @@ export const ExternalEmbed = ({
style={[a.rounded_md]}
onPress={onPress}
onLongPress={onShareExternal}>
{({hovered}) => (
{({hovered, pressed}) => (
<View
style={[
a.transition_color,
@@ -101,7 +101,7 @@ export const ExternalEmbed = ({
a.overflow_hidden,
a.w_full,
a.border,
t.atoms.bg,
pressed && t.atoms.bg,
style,
hovered
? t.atoms.border_contrast_high
@@ -129,7 +129,6 @@ export const StandardSiteEmbed = ({
a.rounded_lg,
a.w_full,
a.border,
t.atoms.bg,
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
preview && a.pointer_events_none,
style,
@@ -154,7 +153,7 @@ export const StandardSiteEmbed = ({
})}
onFocus={onInteract}
onBlur={onInteractOut}>
{() => (
{({pressed}) => (
<View
style={[
a.w_full,
@@ -171,7 +170,7 @@ export const StandardSiteEmbed = ({
borderBottomLeftRadius: a.rounded_lg.borderRadius,
borderBottomRightRadius: a.rounded_lg.borderRadius,
},
interacted ? t.atoms.bg_contrast_25 : t.atoms.bg,
interacted ? t.atoms.bg_contrast_25 : pressed && t.atoms.bg,
]}>
{imageUri ? (
<Image
@@ -2,8 +2,7 @@ import {useImperativeHandle, useRef, useState} from 'react'
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api'
import {BlueskyVideoView} from '@bsky.app/video'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {HITSLOP_30} from '#/lib/constants'
import {useAutoplayDisabled} from '#/state/preferences'
@@ -16,6 +15,7 @@ import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {KeepAwake} from '#/components/KeepAwake'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useReportDialogMetadataContext} from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {GifPresentationControls} from '../GifPresentationControls'
import {TimeIndicator} from './TimeIndicator'
@@ -39,11 +39,13 @@ export function VideoEmbedInnerNative({
*/
onError?: (error: string) => void
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const videoRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
const isWithinMessage = useIsWithinMessage()
const [muted, setMuted] = useVideoMuteState()
const reportDialogMetadata = useReportDialogMetadataContext()
const maxTimeRemainingSeconds = useRef(0)
const [isPlaying, setIsPlaying] = useState(false)
const [timeRemaining, setTimeRemaining] = useState(0)
@@ -84,16 +86,30 @@ export function VideoEmbedInnerNative({
setIsPlaying(e.nativeEvent.status === 'playing')
}}
onTimeRemainingChange={e => {
setTimeRemaining(e.nativeEvent.timeRemaining)
const {timeRemaining} = e.nativeEvent
setTimeRemaining(timeRemaining)
if (
!isGif &&
reportDialogMetadata &&
Number.isFinite(timeRemaining) &&
timeRemaining >= 0
) {
maxTimeRemainingSeconds.current = Math.max(
maxTimeRemainingSeconds.current,
timeRemaining,
)
reportDialogMetadata.current.videoTimestampSeconds = Math.max(
0,
maxTimeRemainingSeconds.current - timeRemaining,
)
}
}}
onError={e => {
onError?.(e.nativeEvent.error)
setError(e.nativeEvent.error)
}}
ref={videoRef}
accessibilityLabel={
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
}
accessibilityLabel={embed.alt ? l`Video: ${embed.alt}` : l`Video`}
accessibilityHint=""
/>
{isGif ? (
@@ -144,7 +160,7 @@ function VideoPresentationControls({
timeRemaining: number
isPlaying: boolean
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const t = useTheme()
const [muted] = useVideoMuteState()
@@ -159,14 +175,14 @@ function VideoPresentationControls({
<Pressable
onPress={enterFullscreen}
style={a.flex_1}
accessibilityLabel={_(msg`Video`)}
accessibilityHint={_(msg`Enters full screen`)}
accessibilityLabel={l`Video`}
accessibilityHint={l`Enters full screen`}
accessibilityRole="button"
/>
<ControlButton
onPress={togglePlayback}
label={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
accessibilityHint={_(msg`Plays or pauses the video`)}
label={isPlaying ? l`Pause` : l`Play`}
accessibilityHint={l`Plays or pauses the video`}
style={{left: 6}}>
{isPlaying ? (
<PauseIcon width={13} fill={t.palette.white} />
@@ -175,15 +191,14 @@ function VideoPresentationControls({
)}
</ControlButton>
{showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />}
<ControlButton
onPress={toggleMuted}
label={
muted
? _(msg({message: `Unmute`, context: 'video'}))
: _(msg({message: `Mute`, context: 'video'}))
? l({message: `Unmute`, context: 'video'})
: l({message: `Mute`, context: 'video'})
}
accessibilityHint={_(msg`Toggles the sound`)}
accessibilityHint={l`Toggles the sound`}
style={{right: 6}}>
{muted ? (
<MuteIcon width={13} fill={t.palette.white} />
@@ -1,13 +1,13 @@
import {useCallback, useEffect, useId, useRef, useState} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import type * as HlsTypes from 'hls.js'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a} from '#/alf'
import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog'
import {useFullscreen} from '#/components/hooks/useFullscreen'
import {useReportDialogMetadataContext} from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
import * as BandwidthEstimate from './bandwidth-estimate'
import {
HLSFatalError,
@@ -36,9 +36,10 @@ export function VideoEmbedInnerWeb({
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
const [hlsLoading, setHlsLoading] = useState(false)
const figId = useId()
const {_} = useLingui()
const {t: l} = useLingui()
const [isFullscreen] = useFullscreen(containerRef)
const isGif = embed.presentation === 'gif'
const reportDialogMetadata = useReportDialogMetadataContext()
// send error up to error boundary
const [error, setError] = useState<Error | null>(null)
@@ -63,7 +64,7 @@ export function VideoEmbedInnerWeb({
return (
<View
style={[a.flex_1, a.rounded_md, a.overflow_hidden]}
accessibilityLabel={_(msg`Embedded video player`)}
accessibilityLabel={l`Embedded video player`}
accessibilityHint="">
<div ref={containerRef} style={{height: '100%', width: '100%'}}>
<figure style={{margin: 0, position: 'absolute', inset: 0}}>
@@ -76,7 +77,16 @@ export function VideoEmbedInnerWeb({
muted={embed.presentation === 'gif' || !focused}
aria-labelledby={embed.alt ? figId : undefined}
onTimeUpdate={e => {
lastKnownTime.current = e.currentTarget.currentTime
const currentTime = e.currentTarget.currentTime
lastKnownTime.current = currentTime
if (
!isGif &&
reportDialogMetadata &&
Number.isFinite(currentTime) &&
currentTime >= 0
) {
reportDialogMetadata.current.videoTimestampSeconds = currentTime
}
}}
loop={loop}
/>
@@ -141,9 +151,10 @@ type CachedPromise<T> = Promise<T> & {value: undefined | T}
const promiseForHls = import(
// @ts-ignore
'hls.js/dist/hls.min'
// oxlint-disable-next-line typescript/no-unsafe-member-access
).then(mod => mod.default) as CachedPromise<typeof HlsTypes.default>
promiseForHls.value = undefined
promiseForHls.then(Hls => {
void promiseForHls.then(Hls => {
promiseForHls.value = Hls
})
@@ -166,7 +177,7 @@ function useHLS({
useEffect(() => {
if (!Hls) {
setHlsLoading(true)
promiseForHls.then(loadedHls => {
void promiseForHls.then(loadedHls => {
setHls(() => loadedHls)
setHlsLoading(false)
})
@@ -303,7 +314,7 @@ function useHLS({
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
if (
data.details === 'manifestLoadError' &&
(data.details as string) === 'manifestLoadError' &&
data.response?.code === 404
) {
setError(new VideoNotFoundError())
@@ -3,11 +3,11 @@ import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {formatTime} from '#/lib/media/video/formatTime'
import {clamp} from '#/lib/numbers'
import {atoms as a, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {IS_WEB_FIREFOX, IS_WEB_TOUCH_DEVICE} from '#/env'
import {formatTime} from './utils'
export function Scrubber({
duration,
@@ -4,6 +4,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {formatTime} from '#/lib/media/video/formatTime'
import {clamp} from '#/lib/numbers'
import {
useAutoplayDisabled,
@@ -31,7 +32,7 @@ import {GifPresentationControls} from '../../GifPresentationControls'
import {TimeIndicator} from '../TimeIndicator'
import {ControlButton} from './ControlButton'
import {Scrubber} from './Scrubber'
import {formatTime, useVideoElement} from './utils'
import {useVideoElement} from './utils'
import {type ControlsProps} from './VideoControls.shared'
import {VolumeControl} from './VolumeControl'
@@ -238,16 +238,3 @@ export function useVideoElement(ref: RefObject<HTMLVideoElement | null>) {
canPlay,
}
}
export function formatTime(time: number) {
if (isNaN(time)) {
return '--'
}
time = Math.round(time)
const minutes = Math.floor(time / 60)
const seconds = String(time % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
+8 -3
View File
@@ -23,6 +23,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
import {GalleryBleed} from '#/components/images/Gallery'
import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import * as ReportDialogMetadataContext from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed'
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {RichText} from '#/components/RichText'
@@ -67,7 +68,11 @@ export function Embed({embed: rawEmbed, ...rest}: EmbedProps) {
}
case 'post_with_media': {
return (
<View style={rest.style}>
<View
style={[
rest.style,
rest.viewContext === PostEmbedViewContext.ChatMessage && a.gap_sm,
]}>
<MediaEmbed embed={embed.media} {...rest} />
<RecordEmbed embed={embed.view} {...rest} />
</View>
@@ -311,7 +316,7 @@ export function QuoteEmbed({
} = useInteractionState()
const contents = (
<>
<ReportDialogMetadataContext.Provider key={quote.uri}>
<PostMeta
author={quote.author}
moderation={moderation}
@@ -350,7 +355,7 @@ export function QuoteEmbed({
post={quote}
/>
)}
</>
</ReportDialogMetadataContext.Provider>
)
return (
@@ -494,6 +494,13 @@ let PostMenuItems = ({
void openLink(url)
}
const onLabelReply = () => {
const url = `https://docs.google.com/forms/d/e/1FAIpQLScWa03XbS_knVbSjnc4DENACN5A2YvBZjtjrpI1XdDbK7d3Ow/viewform?entry.1843100496=${toShareUrl(
href,
)}`
void openLink(url)
}
const onSignIn = () => requireSignIn(() => {})
const onPressHideTranslation = () => clearTranslation()
@@ -618,6 +625,15 @@ let PostMenuItems = ({
<Menu.ItemText>{l`Assign topic for algo`}</Menu.ItemText>
<Menu.ItemIcon icon={AtomIcon} position="right" />
</Menu.Item>
{isReply && (
<Menu.Item
testID="postDropdownLabelReplyBtn"
label={l`Label reply for algo`}
onPress={onLabelReply}>
<Menu.ItemText>{l`Label reply for algo`}</Menu.ItemText>
<Menu.ItemIcon icon={AtomIcon} position="right" />
</Menu.Item>
)}
</>
)}
+3 -1
View File
@@ -623,8 +623,9 @@ export function Labels({
const moderation = moderateProfile(profile, moderationOpts)
const modui = moderation.ui('profileList')
const followedBy = profile.viewer?.followedBy
const mutedOnlyReposts = profile.viewer?.mutedOnlyReposts
if (!followedBy && !modui.inform && !modui.alert) {
if (!followedBy && !mutedOnlyReposts && !modui.inform && !modui.alert) {
return null
}
@@ -637,6 +638,7 @@ export function Labels({
{modui.informs.map(inform => (
<Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
))}
{mutedOnlyReposts && <Pills.MutedOnlyReposts />}
</Pills.Row>
)
}
+20 -4
View File
@@ -3,7 +3,7 @@ import {type StyleProp, type TextStyle} from 'react-native'
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
import {toShortUrl} from '#/lib/strings/url-helpers'
import {atoms as a, flatten, type TextStyleProp} from '#/alf'
import {android, atoms as a, flatten, type TextStyleProp} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography'
import {InlineLinkText, type LinkProps} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -28,6 +28,17 @@ export type RichTextProps = TextStyleProp &
emojiMultiplier?: number
shouldProxyLinks?: boolean
suffix?: React.ReactNode
/**
* How far below the text baseline `suffix` extends, in px.
*
* Android clips inline views that are translated below the measured text
* bounds. Reserve matching room there and cancel it with a negative margin
* so content following the text does not move. iOS allows inline attachment
* overflow through `RNUITextView` and does not need this compensation.
*
* Overrides any `paddingBottom`/`marginBottom` set via `style` on Android.
*/
suffixOffset?: number
/**
* DANGEROUS: Disable facet lexicon validation
*
@@ -56,6 +67,7 @@ export function RichText({
onTextLayout,
shouldProxyLinks,
suffix,
suffixOffset = 0,
disableMentionFacetValidation,
}: RichTextProps) {
const richText = useMemo(() => {
@@ -69,6 +81,10 @@ export function RichText({
}, [value])
const plainStyles = style
const suffixStyles =
suffix && suffixOffset
? android({paddingBottom: suffixOffset, marginBottom: -suffixOffset})
: null
const interactiveStyles = [plainStyles, interactiveStyle]
const {text, facets} = richText
@@ -83,7 +99,7 @@ export function RichText({
emoji
selectable={selectable}
testID={testID}
style={[plainStyles, {fontSize}]}
style={[plainStyles, {fontSize}, suffixStyles]}
onLayout={onLayout}
onTextLayout={onTextLayout}
// @ts-ignore web only -prf
@@ -99,7 +115,7 @@ export function RichText({
emoji
selectable={selectable}
testID={testID}
style={plainStyles}
style={[plainStyles, suffixStyles]}
numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
@@ -187,7 +203,7 @@ export function RichText({
emoji
selectable={selectable}
testID={testID}
style={plainStyles}
style={[plainStyles, suffixStyles]}
numberOfLines={numberOfLines}
onLayout={onLayout}
onTextLayout={onTextLayout}
@@ -13,7 +13,7 @@ import {precacheResolvedUri} from '#/state/queries/resolve-uri'
import {precacheStarterPack} from '#/state/queries/starter-packs'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
import {StarterPackMultiPathLarge as StarterPackIcon} from '#/components/icons/StarterPack'
import {
Link as BaseLink,
type LinkProps as BaseLinkProps,
+2 -2
View File
@@ -26,7 +26,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {StarterPack} from '#/components/icons/StarterPack'
import {StarterPackMultiPathLarge as StarterPackIcon} from '#/components/icons/StarterPack'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
@@ -94,7 +94,7 @@ function Empty({onStartWizard}: {onStartWizard: () => void}) {
return (
<View style={[a.gap_2xl, {paddingTop: IS_WEB ? 100 : 64}]}>
<View style={[a.gap_xs, a.align_center]}>
<StarterPack
<StarterPackIcon
width={48}
fill={t.atoms.border_contrast_medium.borderColor}
/>
+1
View File
@@ -82,6 +82,7 @@ export type ItemProps = ViewStyleProp & {
children: ((props: ItemState) => React.ReactNode) | React.ReactNode
hitSlop?: PressableProps['hitSlop']
highlightRow?: boolean
testID?: string
}
export function useItemContext() {
+2 -2
View File
@@ -4,6 +4,6 @@ export const Gif_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 4a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3Zm1 14V6h16v12H4Zm2-5.713c0 1.54.92 2.463 2.48 2.463 1.434 0 2.353-.807 2.353-2.06v-.166c0-.578-.267-.834-.884-.834h-.806c-.416 0-.632.182-.632.535 0 .357.22.55.632.55h.146v.063c0 .36-.299.609-.735.609-.597 0-.904-.4-.904-1.168v-.52c0-.775.307-1.155.951-1.155.325 0 .538.152.746.3.089.064.176.127.272.177a.82.82 0 0 0 .409.108c.385 0 .656-.263.656-.636 0-.353-.26-.679-.664-.915-.409-.24-.96-.388-1.548-.388C6.955 9.25 6 10.2 6 11.67v.617Zm6.358 2.385c.526 0 .813-.31.813-.872v-3.627c0-.558-.295-.873-.825-.873s-.825.31-.825.873V13.8c0 .558.302.872.837.872Zm3.367-.872c0 .566-.283.872-.802.872-.538 0-.848-.318-.848-.872v-3.635c0-.512.314-.826.82-.826h2.496c.35 0 .609.272.609.64 0 .369-.26.629-.609.629h-1.666v.973h1.47c.365 0 .608.248.608.613 0 .36-.247.613-.608.613h-1.47v.993Z',
})
export const GifSquare_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V5h14v14H5Zm10.725-5.2c0 .566-.283.872-.802.872-.538 0-.848-.318-.848-.872v-3.635c0-.512.314-.826.82-.826h2.496c.35 0 .609.272.609.64 0 .369-.26.629-.609.629h-1.666v.973h1.47c.365 0 .608.248.608.613 0 .36-.247.613-.608.613h-1.47v.993Zm-3.367.872c.526 0 .813-.31.813-.872v-3.627c0-.558-.295-.873-.825-.873s-.825.31-.825.873V13.8c0 .558.302.872.837.872Zm-3.879.078C6.92 14.75 6 13.827 6 12.287v-.617c0-1.47.955-2.42 2.472-2.42.589 0 1.139.147 1.548.388.404.236.664.562.664.915 0 .373-.271.636-.656.636a.82.82 0 0 1-.41-.108 2.34 2.34 0 0 1-.271-.177c-.208-.148-.421-.3-.746-.3-.644 0-.95.38-.95 1.155v.52c0 .768.306 1.168.903 1.168.436 0 .735-.248.735-.61v-.061h-.146c-.412 0-.632-.194-.632-.551 0-.353.216-.535.632-.535h.806c.617 0 .884.256.884.834v.166c0 1.253-.92 2.06-2.354 2.06Z',
export const GifSquare_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M6 12.287C6 13.8269 6.91945 14.75 8.47937 14.75C9.91356 14.75 10.833 13.9432 10.833 12.6904V12.5236C10.833 11.9457 10.5658 11.6897 9.94892 11.6897H9.14342C8.72692 11.6897 8.51081 11.872 8.51081 12.225C8.51081 12.5818 8.73084 12.7757 9.14342 12.7757H9.2888V12.8378C9.2888 13.1985 8.99018 13.4468 8.55403 13.4468C7.95678 13.4468 7.65029 13.0472 7.65029 12.2793V11.7595C7.65029 10.9838 7.95678 10.6037 8.60118 10.6037C9.06483 10.6037 9.30059 10.914 9.61886 11.0807C9.73674 11.1467 9.87033 11.1894 10.0275 11.1894C10.4126 11.1894 10.6837 10.9256 10.6837 10.5532C10.6837 10.2003 10.4244 9.87447 10.0196 9.63787C9.611 9.39739 9.0609 9.25 8.47151 9.25C6.95481 9.25 6 10.2003 6 11.6703V12.287ZM12.3576 14.6724C12.8841 14.6724 13.1709 14.3621 13.1709 13.7997V10.1731C13.1709 9.6146 12.8762 9.30042 12.3458 9.30042C11.8153 9.30042 11.5206 9.61072 11.5206 10.1731V13.7997C11.5206 14.3583 11.8232 14.6724 12.3576 14.6724ZM14.9234 14.6724C15.442 14.6724 15.725 14.366 15.725 13.7997V12.8068H17.1945C17.556 12.8068 17.8035 12.5547 17.8035 12.1939C17.8035 11.8293 17.5599 11.5811 17.1945 11.5811H15.725V10.6075H17.391C17.7407 10.6075 18 10.3477 18 9.97922C18 9.61074 17.7407 9.33923 17.391 9.33923H14.8959C14.389 9.33923 14.0747 9.65341 14.0747 10.1654V13.7997C14.0747 14.3544 14.3851 14.6724 14.9234 14.6724ZM19 7C19 5.89542 18.1046 5 17 5H7C5.89543 5 5 5.89543 5 7V17C5 18.1046 5.89542 19 7 19H17C18.1046 19 19 18.1046 19 17V7ZM21 17C21 19.2092 19.2092 21 17 21H7C4.79088 21 3 19.2092 3 17V7C3 4.79087 4.79087 3 7 3H17C19.2092 3 21 4.79088 21 7V17Z',
})
+2 -2
View File
@@ -7,6 +7,6 @@ export const Image_Stroke1_Corner0_Rounded = createSinglePathSVG({
path: 'm1.417 28.645 7.586-5.676a5.33 5.33 0 0 1 6.867.809c3.98 4.286 8.594 8.182 14.88 8.182 5.794 0 9.633-2.147 13.333-5.847m-38 18.637h33.334a5.333 5.333 0 0 0 5.333-5.333V6.083A5.333 5.333 0 0 0 39.417.75H6.083A5.333 5.333 0 0 0 .75 6.083v33.334a5.333 5.333 0 0 0 5.333 5.333ZM36.75 14.083a5.333 5.333 0 1 1-10.667 0 5.333 5.333 0 0 1 10.667 0Z',
})
export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z',
export const Image_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M19 15.3545C17.8878 16.0512 16.6152 16.4541 15 16.4541C12.1908 16.4541 10.2043 14.6989 8.6875 13.0654C8.35777 12.7104 7.82149 12.6439 7.41504 12.9043L5 14.71V17C5 18.1046 5.89542 19 7 19H17C18.1046 19 19 18.1046 19 17V15.3545ZM16 9C16 8.4477 15.5523 8 15 8C14.4477 8 14 8.4477 14 9C14 9.55232 14.4477 10 15 10C15.5523 10 16 9.55232 16 9ZM19 7C19 5.89542 18.1046 5 17 5H7C5.89543 5 5 5.89543 5 7V12.2129L6.24609 11.2812L6.29004 11.25C7.5148 10.4335 9.15081 10.6266 10.1523 11.7051C11.6204 13.2862 13.0944 14.4541 15 14.4541C16.7041 14.4541 17.8543 13.897 19 12.834V7ZM18 9C18 10.6569 16.6569 12 15 12C13.3431 12 12 10.6569 12 9C12 7.34315 13.3431 6 15 6C16.6569 6 18 7.34315 18 9ZM21 17C21 19.2092 19.2092 21 17 21H7C4.79088 21 3 19.2092 3 17V7C3 4.79087 4.79087 3 7 3H17C19.2092 3 21 4.79088 21 7V17Z',
})
+4
View File
@@ -15,3 +15,7 @@ export const Repost_Stroke2_Corner3_Rounded = createSinglePathSVG({
export const RepostRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M6.043 14.293a1 1 0 1 1 1.414 1.414L5.164 18l2.293 2.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47Zm6.22 0a1 1 0 0 1 1.414 1.414L12.384 17H18a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3h-5.616l1.293 1.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47ZM3 11V8a3 3 0 0 1 3-3h5.586l-1.293-1.293-.068-.076a1 1 0 0 1 1.406-1.406l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L11.586 7H6a1 1 0 0 0-1 1v3a1 1 0 1 1-2 0Zm13.543-8.707a1 1 0 0 1 1.338-.068l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L18.836 6l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z',
})
export const RepostStrike_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19H6.414l1.293 1.293a1 1 0 0 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 1 1 1.414 1.414L6.414 17h9.172L5.909 7.323A2 2 0 0 0 5 9v2a1 1 0 0 1-2 0V9c0-1.255.579-2.372 1.482-3.104L2.293 3.707a1 1 0 0 1 0-1.414M20 12a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1M16.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H11a1 1 0 0 1 0-2h6.586l-1.293-1.293a1 1 0 0 1 0-1.414',
})
+11 -2
View File
@@ -1,8 +1,17 @@
import {createMultiPathSVG} from './TEMPLATE'
import {createMultiPathSVG, createSinglePathSVG} from './TEMPLATE'
export const StarterPack = createMultiPathSVG({
/**
* Special icon for large display. Use with `gradient="sky"`
*
* If trying to slot into other components (e.g. as a Menu.Item) use StarterPack_Stroke2_Corner0_Rounded instead
*/
export const StarterPackMultiPathLarge = createMultiPathSVG({
paths: [
'M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672c.734-.197 1.17-.951.973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z',
'M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z',
],
})
export const StarterPack_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M10.775 2.74a3 3 0 0 1 3.524 2.158l.57 2.13a7.5 7.5 0 1 1-6.12 10.744l-2.228.598-.15.036a3 3 0 0 1-3.48-2.009l-.044-.148L.777 8.52a3 3 0 0 1 1.973-3.63l.148-.044 7.728-2.07.149-.037Zm4.624 6.262.97 3.624.037.149a3 3 0 0 1-2.009 3.48l-.148.044-3.517.942A5.5 5.5 0 1 0 15.5 9l-.101.002Zm-3.031-3.586a1 1 0 0 0-1.225-.707l-7.727 2.07a1 1 0 0 0-.707 1.225l2.07 7.727a1 1 0 0 0 1.225.707l2.123-.57a7.504 7.504 0 0 1 4.788-8.412l-.547-2.04ZM13.435 9.4a5.5 5.5 0 0 0-3.37 5.948l3.666-.981a1 1 0 0 0 .707-1.225l-1.003-3.742Z',
})
@@ -3,16 +3,21 @@ import {type ModerationDecision} from '@atproto/api'
import {getModerationCauseKey, unique} from '#/lib/moderation'
import * as Pills from '#/components/Pills'
import type * as bsky from '#/types/bsky'
export function ProfileHeaderAlerts({
moderation,
profile,
style,
}: {
moderation: ModerationDecision
profile: bsky.profile.AnyProfileView
style?: StyleProp<ViewStyle>
}) {
const modui = moderation.ui('profileView')
if (!modui.alert && !modui.inform) {
const mutedOnlyReposts = profile.viewer?.mutedOnlyReposts
if (!mutedOnlyReposts && !modui.alert && !modui.inform) {
return null
}
@@ -32,6 +37,7 @@ export function ProfileHeaderAlerts({
cause={cause}
/>
))}
{mutedOnlyReposts && <Pills.MutedOnlyReposts size="lg" />}
</Pills.Row>
)
}
@@ -0,0 +1,24 @@
import {createContext, useContext, useRef} from 'react'
export type ReportDialogMetadata = {
videoTimestampSeconds?: number
}
export type ReportDialogMetadataRef = React.RefObject<ReportDialogMetadata>
const Context = createContext<ReportDialogMetadataRef | null>(null)
Context.displayName = 'ReportDialogMetadataContext'
/**
* Scopes report metadata to a rendered subject. The mutable ref lets media
* events update metadata without rerendering the post on every playback tick.
*/
export function Provider({children}: React.PropsWithChildren) {
const metadata = useRef<ReportDialogMetadata>({})
return <Context.Provider value={metadata}>{children}</Context.Provider>
}
export function useReportDialogMetadataContext() {
return useContext(Context)
}
@@ -1,5 +1,6 @@
import {
type $Typed,
BSKY_LABELER_DID,
type ChatBskyConvoDefs,
type ComAtprotoModerationCreateReport,
} from '@atproto/api'
@@ -9,7 +10,7 @@ import {useMutation} from '@tanstack/react-query'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {NEW_TO_OLD_REASONS_MAP} from './const'
import {NEW_TO_OLD_REASONS_MAP, REPORT_MOD_TOOL_NAME} from './const'
import {type ReportState} from './state'
import {type ParsedReportSubject} from './types'
@@ -21,9 +22,15 @@ export function useSubmitReportMutation() {
async mutationFn({
subject,
state,
videoTimestampSeconds,
}: {
subject: ParsedReportSubject
state: ReportState
/**
* How far the viewer watched when the dialog opened, if the subject is a
* post with a video.
*/
videoTimestampSeconds?: number
}) {
if (!state.selectedOption) {
throw new Error(_(msg`Please select a reason for this report`))
@@ -115,6 +122,21 @@ export function useSubmitReportMutation() {
}
}
const modToolMeta =
state.includeVideoTimestamp &&
videoTimestampSeconds != null &&
subject.type === 'post' &&
labeler.creator.did === BSKY_LABELER_DID
? {videoTimestampSeconds}
: undefined
if (modToolMeta) {
report.modTool = {
name: REPORT_MOD_TOOL_NAME,
meta: modToolMeta,
}
}
if (__DEV__) {
logger.info('Submitting report (dry run)', {
labeler: {
@@ -1,14 +1,27 @@
import {applicationId} from 'expo-application'
import {
ComAtprotoModerationDefs as RootReportDefs,
ToolsOzoneReportDefs as OzoneReportDefs,
} from '@atproto/api'
import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types'
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
export const SUPPORT_PAGE = 'https://bsky.social/about/support'
export const NCII_FORM = 'https://forms.bsky.app/f/ncii'
/**
* Identifies this client as the source of a report.
*/
export const REPORT_MOD_TOOL_NAME = IS_IOS
? `bsky-app/ios/${applicationId}`
: IS_ANDROID
? `bsky-app/android/${applicationId}`
: IS_WEB
? `bsky-web/${window.location.hostname}`
: 'bsky' // Should never occur
export const NEW_TO_OLD_REASON_MAPPING: Record<string, string> = {}
/**
@@ -11,6 +11,7 @@ import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {wait} from '#/lib/async/wait'
import {formatTime} from '#/lib/media/video/formatTime'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {useCallOnce} from '#/lib/once'
import {sanitizeHandle} from '#/lib/strings/handles'
@@ -22,6 +23,7 @@ import * as Admonition from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import * as Toggle from '#/components/forms/Toggle'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotate'
import {
@@ -46,6 +48,7 @@ import {
} from './const'
import {useCopyForSubject} from './copy'
import {classifyReportError} from './errors'
import {useReportDialogMetadataContext} from './ReportDialogMetadataContext'
import {
getNciiQualificationOutcome,
initialState,
@@ -79,19 +82,47 @@ export function ReportDialog(
},
) {
const ax = useAnalytics()
const reportDialogMetadata = useReportDialogMetadataContext()
const subject = useMemo(
() => (props.subject ? parseReportSubject(props.subject) : undefined),
[props.subject],
)
const [presentation, setPresentation] = useState<{
openCount: number
videoTimestampSeconds?: number
}>({openCount: 0})
const onOpen = useCallback(() => {
const seconds =
subject?.type === 'post' && subject.attributes.video
? reportDialogMetadata?.current.videoTimestampSeconds
: undefined
setPresentation(current => ({
openCount: current.openCount + 1,
// Values below one second indicate that the video was never meaningfully
// played, so avoid offering to attach a noisy "0:00" timestamp.
videoTimestampSeconds:
seconds !== undefined && seconds >= 1 ? Math.floor(seconds) : undefined,
}))
}, [reportDialogMetadata, subject])
const propsOnClose = props.onClose
const onClose = useCallback(() => {
ax.metric('reportDialog:close', {})
propsOnClose?.()
}, [ax, propsOnClose])
return (
<Dialog.Outer control={props.control} onClose={onClose}>
<Dialog.Outer control={props.control} onOpen={onOpen} onClose={onClose}>
<Dialog.Handle />
{subject ? <Inner {...props} subject={subject} /> : <Invalid />}
{subject ? (
<Inner
key={presentation.openCount}
{...props}
subject={subject}
videoTimestampSeconds={presentation.videoTimestampSeconds}
/>
) : (
<Invalid />
)}
</Dialog.Outer>
)
}
@@ -118,7 +149,11 @@ function Invalid() {
)
}
function Inner(props: ReportDialogProps) {
function Inner(
props: ReportDialogProps & {
videoTimestampSeconds?: number
},
) {
const ax = useAnalytics()
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
const t = useTheme()
@@ -142,6 +177,8 @@ function Inner(props: ReportDialogProps) {
const [isPending, setIsPending] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const {videoTimestampSeconds} = props
// some reasons ONLY go to Bluesky
const isBskyOnlyReason = state?.selectedOption?.reason
? BSKY_LABELER_ONLY_REPORT_REASONS.has(state.selectedOption.reason)
@@ -230,6 +267,7 @@ function Inner(props: ReportDialogProps) {
submitReport({
subject: props.subject,
state,
videoTimestampSeconds,
}),
)
setIsSuccess(true)
@@ -237,6 +275,7 @@ function Inner(props: ReportDialogProps) {
reason: state.selectedOption?.reason ?? '',
labeler: state.selectedLabeler?.creator.handle ?? '',
details: !!state.details,
videoTimestamp: state.includeVideoTimestamp,
})
// give time for user feedback
setTimeout(() => {
@@ -282,7 +321,7 @@ function Inner(props: ReportDialogProps) {
} finally {
setIsPending(false)
}
}, [logger, submitReport, props, state, ax, l])
}, [logger, submitReport, props, state, ax, l, videoTimestampSeconds])
useCallOnce(() => {
ax.metric('reportDialog:open', {
@@ -602,6 +641,18 @@ function Inner(props: ReportDialogProps) {
</View>
)}
</View>
{videoTimestampSeconds !== undefined &&
state.selectedLabeler?.creator.did === BSKY_LABELER_DID && (
<IncludeVideoTimestampToggle
seconds={videoTimestampSeconds}
selected={state.includeVideoTimestamp}
onChange={include => {
dispatch({type: 'setIncludeVideoTimestamp', include})
}}
/>
)}
<Button
testID="report:submit"
label={l`Submit report`}
@@ -632,6 +683,42 @@ function Inner(props: ReportDialogProps) {
)
}
/**
* Opt-in for attaching how far the viewer had watched to a video report. Only
* rendered once we have a position and the report is going to Bluesky.
*/
function IncludeVideoTimestampToggle({
seconds,
selected,
onChange,
}: {
seconds: number
selected: boolean
onChange: (selected: boolean) => void
}) {
const {t: l} = useLingui()
const videoTimestamp = formatTime(seconds)
const label = l({
message: `Include video timestamp (${videoTimestamp})`,
comment:
'Checkbox shown when reporting a video. The value is the current playback position, formatted as minutes:seconds.',
})
return (
<Toggle.Item
testID="report:includeVideoTimestamp"
name="includeVideoTimestamp"
type="checkbox"
label={label}
value={selected}
onChange={onChange}>
<Toggle.Checkbox />
<Toggle.LabelText style={[a.flex_1, a.font_normal, a.leading_snug]}>
{label}
</Toggle.LabelText>
</Toggle.Item>
)
}
function ActionOnce({
check,
callback,
@@ -108,3 +108,43 @@ describe('reducer NCII qualification', () => {
expect(reducer(state, {type: 'clearCategory'}).ncii).toBeUndefined()
})
})
describe('reducer video timestamp', () => {
const labeler = {} as AppBskyLabelerDefs.LabelerViewDetailed
const opted: ReportState = {...initialState, includeVideoTimestamp: true}
it('is off by default', () => {
expect(initialState.includeVideoTimestamp).toBe(false)
})
it('toggles on and back off', () => {
let state = reducer(initialState, {
type: 'setIncludeVideoTimestamp',
include: true,
})
expect(state.includeVideoTimestamp).toBe(true)
state = reducer(state, {type: 'setIncludeVideoTimestamp', include: false})
expect(state.includeVideoTimestamp).toBe(false)
})
it('clears when the moderation service is cleared', () => {
expect(reducer(opted, {type: 'clearLabeler'}).includeVideoTimestamp).toBe(
false,
)
})
it('clears when a moderation service is selected', () => {
expect(
reducer(opted, {type: 'selectLabeler', labeler}).includeVideoTimestamp,
).toBe(false)
})
it('clears when the reason or category is cleared', () => {
expect(reducer(opted, {type: 'clearOption'}).includeVideoTimestamp).toBe(
false,
)
expect(reducer(opted, {type: 'clearCategory'}).includeVideoTimestamp).toBe(
false,
)
})
})
@@ -21,6 +21,11 @@ export type ReportState = {
detailsOpen: boolean
activeStepIndex1: number
error?: string
/**
* Whether to attach how far the viewer had watched to the report. Only
* offered for posts with a video.
*/
includeVideoTimestamp: boolean
/**
* Present while the selected reason is NCII. Tracks the answer to the
* qualifying question that determines whether the report should go through
@@ -85,6 +90,10 @@ export type ReportAction =
| {
type: 'showDetails'
}
| {
type: 'setIncludeVideoTimestamp'
include: boolean
}
export const initialState: ReportState = {
selectedCategory: undefined,
@@ -93,6 +102,7 @@ export const initialState: ReportState = {
details: undefined,
detailsOpen: false,
activeStepIndex1: 1,
includeVideoTimestamp: false,
}
export function reducer(state: ReportState, action: ReportAction): ReportState {
@@ -114,6 +124,7 @@ export function reducer(state: ReportState, action: ReportAction): ReportState {
activeStepIndex1: 1,
detailsOpen: false,
ncii: undefined,
includeVideoTimestamp: false,
}
case 'selectOption': {
const isNcii = action.option.reason === OzoneReportDefs.REASONSEXUALNCII
@@ -134,6 +145,7 @@ export function reducer(state: ReportState, action: ReportAction): ReportState {
activeStepIndex1: 2,
detailsOpen: false,
ncii: undefined,
includeVideoTimestamp: false,
}
case 'answerNciiQuestion': {
const ncii = {...state.ncii, [action.question]: action.answer}
@@ -163,12 +175,18 @@ export function reducer(state: ReportState, action: ReportAction): ReportState {
detailsOpen: state.selectedOption
? OTHER_REPORT_REASONS.has(state.selectedOption?.reason)
: false,
/*
* Picking a service is a fresh consent decision - the opt-in is scoped
* to Bluesky, so it must not survive a switch to another labeler.
*/
includeVideoTimestamp: false,
}
case 'clearLabeler':
return {
...state,
selectedLabeler: undefined,
activeStepIndex1: 3,
includeVideoTimestamp: false,
}
case 'setDetails':
return {
@@ -190,5 +208,10 @@ export function reducer(state: ReportState, action: ReportAction): ReportState {
...state,
detailsOpen: true,
}
case 'setIncludeVideoTimestamp':
return {
...state,
includeVideoTimestamp: action.include,
}
}
}
@@ -8,7 +8,7 @@ import {toNiceDomain} from '#/lib/strings/url-helpers'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {atoms as a, useTheme} from '#/alf'
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {Image_Stroke2_Corner2_Rounded as ImageIcon} from '#/components/icons/Image'
import {Text} from '#/components/Typography'
export function LinkPreview({
@@ -21,7 +21,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {Globe_Stroke2_Corner0_Rounded} from '#/components/icons/Globe'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {Image_Stroke2_Corner2_Rounded as ImageIcon} from '#/components/icons/Image'
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRightIcon} from '#/components/icons/SquareArrowTopRight'
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import * as Hider from '#/components/moderation/Hider'
+7 -1
View File
@@ -97,7 +97,13 @@ export function useLiveNowConfig(): LiveNowConfig {
export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
const shadowed = useMaybeProfileShadow(actor)
const tick = useTickEveryMinute()
/*
* The minute tick only exists to re-validate an existing status's expiry,
* so actors without a status record skip the subscription entirely. This
* keeps the every-minute tick from re-rendering every post in a feed.
*/
const hasStatus = !!(shadowed && 'status' in shadowed && shadowed.status)
const tick = useTickEveryMinute(hasStatus)
const config = useLiveNowConfig()
const moderationOpts = useModerationOpts()
@@ -0,0 +1,16 @@
import {formatTime} from '../formatTime'
describe('formatTime', () => {
it('formats seconds as minutes and seconds', () => {
expect(formatTime(0)).toBe('0:00')
expect(formatTime(65)).toBe('1:05')
})
it('rounds fractional seconds', () => {
expect(formatTime(12.5)).toBe('0:13')
})
it('handles an unknown duration', () => {
expect(formatTime(NaN)).toBe('--')
})
})
+16
View File
@@ -0,0 +1,16 @@
/**
* Formats a duration in seconds as minutes and seconds for video controls and
* related UI.
*/
export function formatTime(time: number) {
if (isNaN(time)) {
return '--'
}
time = Math.round(time)
const minutes = Math.floor(time / 60)
const seconds = String(time % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
+2 -2
View File
@@ -39,12 +39,12 @@ export type CommonNavigatorParams = {
PostLikedBy: {name: string; rkey: string}
PostRepostedBy: {name: string; rkey: string}
PostQuotes: {name: string; rkey: string}
ProfileFeed: {
CustomFeed: {
name: string
rkey: string
feedCacheKey?: 'discover' | 'explore' | undefined
}
ProfileFeedLikedBy: {name: string; rkey: string}
CustomFeedLikedBy: {name: string; rkey: string}
ProfileLabelerLikedBy: {name: string}
Debug: undefined
DebugMod: undefined
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More