Merge remote-tracking branch 'origin/main' into app-2774
# Conflicts: # package.json # pnpm-lock.yaml
@@ -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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:*)"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.*
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
@@ -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.
|
||||
@@ -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 +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 |
@@ -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 |
@@ -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.
|
||||
@@ -6,7 +6,7 @@
|
||||
"devEngines": {
|
||||
"packageManager": {
|
||||
"name": "pnpm",
|
||||
"version": "11.13.1",
|
||||
"version": "11.21.0",
|
||||
"onFail": "warn"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -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/*'
|
||||
|
||||
@@ -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.')
|
||||
@@ -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
|
||||
|
||||
@@ -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)',
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -60,6 +60,7 @@ export type DialogControlOpenOptions = {
|
||||
|
||||
export type DialogOuterProps = {
|
||||
control: DialogControlProps
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||
webOptions?: {
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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('--')
|
||||
})
|
||||
})
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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
|
||||
|
||||