Compare commits

...

13 Commits

Author SHA1 Message Date
Austin McKinley ad075d03e6 prettier
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 07:55:49 -07:00
Austin McKinley fc750703ac bskyogcard: downscale avatars before rendering
The card routes fetch each avatar from cdn.bsky.app/img/avatar/,
which serves up to 1000x1000. Cards never display avatars above
~210px, but satori parses and resvg rasterizes images at intrinsic
size: ~4MB of decoded native RGBA per avatar per render, up to 7
avatars per starter-pack card, and satori's 500-entry image LRU
retains the oversized data URIs. Under sustained traffic the native
churn and allocator fragmentation grow RSS unbounded and the service
OOMs against its 2Gi limit roughly hourly (observed in prod: ~20
OOMKills/day/pod for weeks).

Resize fetched avatars to 256px max with sharp before handing them
to the render pipeline. Decoded pixel volume drops ~15x; rendered
output is visually identical since display sizes never exceed the
resize cap. libvips' operation cache is disabled since unique-image
traffic never hits it.

Load-tested with 74 real starter packs (6 rounds, concurrency 8):
peak RSS 1441Mi -> ~1000Mi, JS heap 130-160Mi -> 45-70Mi, and
late-round RSS flattens instead of compounding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:07:28 -07:00
pfrazee f06f2cbc25 Nightly source-language update 2026-07-09 03:14:03 +00:00
DS Boyce 0a4f5e132c Update mergeRefs util for React 19 (#11108) 2026-07-08 12:32:15 -07:00
Spence Pope 6dce96881c use source file for web video compression (#11090) 2026-07-08 15:07:52 -04:00
DS Boyce 510dc51d7a Update advanced search UI (#11088) 2026-07-08 10:14:39 -07:00
Eric Bailey decac43c34 Adjust AA device signals copy (#11106) 2026-07-08 11:40:33 -05:00
Samuel Newman efcc15fc55 Bump version to v1.128.0 (#11105) 2026-07-08 08:38:50 -07:00
DS Boyce 53d9db174c Revert "Add more search placeholders (#11089)" (#11101) 2026-07-08 07:25:41 -07:00
DS Boyce a689322f4a Add more search placeholders (#11089) 2026-07-08 07:10:12 -07:00
dependabot[bot] aaf45129aa Bump golang.org/x/crypto from 0.51.0 to 0.52.0 in /bskyweb (#11100)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 06:44:33 -07:00
Samuel Newman 48e1502bd1 Fix Android crash in chat keyboard scroll (part 2) (#11096)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 06:21:32 -07:00
Samuel Newman b41624edea Lazy-load mediabunny on web (#11094)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 06:21:16 -07:00
27 changed files with 771 additions and 297 deletions
+1
View File
@@ -28,6 +28,7 @@
"prom-client": "^15.1.3",
"react": "^19.2.6",
"satori": "^0.26.0",
"sharp": "^0.35.3",
"twemoji": "^14.0.2"
},
"devDependencies": {
+372 -38
View File
@@ -7,52 +7,52 @@ importers:
configDependencies: {}
packageManagerDependencies:
'@pnpm/exe':
specifier: 11.5.2
version: 11.5.2
specifier: 11.10.0
version: 11.10.0
pnpm:
specifier: 11.5.2
version: 11.5.2
specifier: 11.10.0
version: 11.10.0
packages:
'@pnpm/exe@11.5.2':
resolution: {integrity: sha512-4UFnP2rhNu1xjAQ+I1GdIUUEtCJuTYJlbpiWSFA4POAID3Lpt+2vrjImWO7eOJ7iCY3vpc4TFe2IW3sAolW4Kg==}
'@pnpm/exe@11.10.0':
resolution: {integrity: sha512-mrmfi2C7LpZkyq0voKKye6MzrK8/K7tYQRiSB/jqOiwnFtQxxcA3xGTY9cEsrGpNl2ClPecQofLuj+BO8AIsxw==}
hasBin: true
'@pnpm/linux-arm64@11.5.2':
resolution: {integrity: sha512-MbJySnu2y9cCBqlODLjUlZ87JnRC3Inq40rvGHWJSrSQ0PnuHeSw2NDMnLI8Hf9hCY+ooussRc5iiR4IAkjUvg==}
'@pnpm/linux-arm64@11.10.0':
resolution: {integrity: sha512-NbvDeUfs0SJuli9OPvgVvmnlbo2DvJ861XGXKrzgLu5AuTnLDLXgbZEEUd8mJ5I0YNrqOVXSWVfWEqiAazWzPA==}
cpu: [arm64]
os: [linux]
'@pnpm/linux-x64@11.5.2':
resolution: {integrity: sha512-g6g2BGpQA47wUACy6B1MdeSHPtnl6x4AeCg0IOWQ7xXorEtC+VRiSHhLpA5kByFGeSwyYh/nLc7mLul5DAaELw==}
'@pnpm/linux-x64@11.10.0':
resolution: {integrity: sha512-kdgb8BXZ/3XQ0x2cOmgLmsij7+SUIqd1bcV6OZhdGzQiDrOY6FAPrR+Y2Bp+NjrrhjzMHVm5pZrrmdeC67ymSQ==}
cpu: [x64]
os: [linux]
'@pnpm/linuxstatic-arm64@11.5.2':
resolution: {integrity: sha512-xTxs9BLxYW39BPNGnmvYCUBnMPWm4mzmzujmdYbpRxDnBXrx55qPR5K/3LSohX7VrmsdDrYxuH6AmG1AaOlIfA==}
'@pnpm/linuxstatic-arm64@11.10.0':
resolution: {integrity: sha512-JE1WrSyKGvqGQgWzqrMXn//ehedpiRax3hi1oP+v6mhvcnlJD1lpfXsjSfIFl9weTWC5KVtFbxSNKbKWfP+v6g==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/linuxstatic-x64@11.5.2':
resolution: {integrity: sha512-RGmmc/SoGLD90gmOHcU85UEKNoNRstLvizli4wzDASmETz/VeqJOqU5nD1YBgjzcP72sUMS352dh4bmzTfKyvQ==}
'@pnpm/linuxstatic-x64@11.10.0':
resolution: {integrity: sha512-1TBZVRkWb78GnsusIfVgwz20MOOW0fyehW+qLL3MxE9vT0IedNWsAhe3KWXgEvtC4M7NtMt55uQQJm+1egwBkA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/macos-arm64@11.5.2':
resolution: {integrity: sha512-gW3A2jRlC3SJRw8qX2SAzjMIu9o98daTSqCKzeeYcjF/uEbtbz3dn4HqYrYffBnenKbc4hsgZQmNOHAvUKIlSg==}
'@pnpm/macos-arm64@11.10.0':
resolution: {integrity: sha512-94AVpPixBqyNT6SHYvIKFb1bfaHR5vxBzZsiFJahNSlGkgyPrgzmcqDiloZ/Jl+zxJd8L5PU1ddP0Q9PZnRqlA==}
cpu: [arm64]
os: [darwin]
'@pnpm/win-arm64@11.5.2':
resolution: {integrity: sha512-+VJCDoH/pRzLXBikwjvxgAnGfQufT8EALBX8cfSmrwD40JABUZvgPtjBjde7OwEoK/XwtlH8w+ZceFV0K3/YHQ==}
'@pnpm/win-arm64@11.10.0':
resolution: {integrity: sha512-g2Ymnq+LgVyZaWsGBQSlpIcBCOOxyLky2UX+kTwGiIXnj6k/xXqZR0ZJLuxeiGNh/CVmhftOqokpyJNzyj8kng==}
cpu: [arm64]
os: [win32]
'@pnpm/win-x64@11.5.2':
resolution: {integrity: sha512-zgglREh75RbFgV/E0tNRS03ElX+hJOV43KRSSeaboxtj3ei1rrguxOgOCXUs/GsizoHVsuD+qXGABE4Kc4GMCg==}
'@pnpm/win-x64@11.10.0':
resolution: {integrity: sha512-kCHYZudUEBjrEchgnJUnNHCdvLXpUZun2z0rMAeP5DymsaXwEVvVzGj220iR/NyhgDocJUmgtTKtwL/ejL785Q==}
cpu: [x64]
os: [win32]
@@ -116,45 +116,45 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
pnpm@11.5.2:
resolution: {integrity: sha512-ccYx44IGbvwlYl1c8CkHXeB7YbN/bic1D72Esb2lhkyMGWetwoB3a0XDCnFcA1mjvgj+9C1bsJ4rmQKZeWkpFg==}
pnpm@11.10.0:
resolution: {integrity: sha512-C3+LmAYAMZBMAX46QesYehbUDuuCm5XE+MsDaBdh/Eq1PdIZEVubRH9NzhoFohR2RGHn03AzkqnzL5URzoyGyA==}
engines: {node: '>=22.13'}
hasBin: true
snapshots:
'@pnpm/exe@11.5.2':
'@pnpm/exe@11.10.0':
dependencies:
'@reflink/reflink': 0.1.19
detect-libc: 2.1.2
optionalDependencies:
'@pnpm/linux-arm64': 11.5.2
'@pnpm/linux-x64': 11.5.2
'@pnpm/linuxstatic-arm64': 11.5.2
'@pnpm/linuxstatic-x64': 11.5.2
'@pnpm/macos-arm64': 11.5.2
'@pnpm/win-arm64': 11.5.2
'@pnpm/win-x64': 11.5.2
'@pnpm/linux-arm64': 11.10.0
'@pnpm/linux-x64': 11.10.0
'@pnpm/linuxstatic-arm64': 11.10.0
'@pnpm/linuxstatic-x64': 11.10.0
'@pnpm/macos-arm64': 11.10.0
'@pnpm/win-arm64': 11.10.0
'@pnpm/win-x64': 11.10.0
'@pnpm/linux-arm64@11.5.2':
'@pnpm/linux-arm64@11.10.0':
optional: true
'@pnpm/linux-x64@11.5.2':
'@pnpm/linux-x64@11.10.0':
optional: true
'@pnpm/linuxstatic-arm64@11.5.2':
'@pnpm/linuxstatic-arm64@11.10.0':
optional: true
'@pnpm/linuxstatic-x64@11.5.2':
'@pnpm/linuxstatic-x64@11.10.0':
optional: true
'@pnpm/macos-arm64@11.5.2':
'@pnpm/macos-arm64@11.10.0':
optional: true
'@pnpm/win-arm64@11.5.2':
'@pnpm/win-arm64@11.10.0':
optional: true
'@pnpm/win-x64@11.5.2':
'@pnpm/win-x64@11.10.0':
optional: true
'@reflink/reflink-darwin-arm64@0.1.19':
@@ -194,7 +194,7 @@ snapshots:
detect-libc@2.1.2: {}
pnpm@11.5.2: {}
pnpm@11.10.0: {}
---
lockfileVersion: '9.0'
@@ -237,6 +237,9 @@ importers:
satori:
specifier: ^0.26.0
version: 0.26.0
sharp:
specifier: ^0.35.3
version: 0.35.3(@types/node@24.12.2)
twemoji:
specifier: ^14.0.2
version: 14.0.2
@@ -295,6 +298,9 @@ packages:
resolution: {integrity: sha512-NJy02bIKrWlE2NQkRV1kT0Cj0ixbuxlF/MejBdo4cPWAa9v3oZexvAcjjb0zaOYeABkaU14iyIhvn2G4e/oLpw==}
engines: {node: '>=22'}
'@emnapi/runtime@1.11.2':
resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
engines: {node: '>=18'}
@@ -451,6 +457,168 @@ packages:
cpu: [x64]
os: [win32]
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.35.3':
resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.35.3':
resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.3':
resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.2':
resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.2':
resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.3.2':
resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.2':
resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.2':
resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.2':
resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.2':
resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.2':
resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.3':
resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.3':
resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.3':
resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.3':
resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.3':
resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.3':
resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.3':
resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.3':
resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.3':
resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.3':
resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.35.3':
resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.35.3':
resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.35.3':
resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
'@opentelemetry/api@1.9.0':
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
@@ -692,6 +860,10 @@ packages:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1020,6 +1192,11 @@ packages:
semver-compare@1.0.0:
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
@@ -1031,6 +1208,15 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.35.3:
resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -1215,6 +1401,11 @@ snapshots:
'@atproto/lexicon': 0.7.1
zod: 3.25.76
'@emnapi/runtime@1.11.2':
dependencies:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.27.7':
optional: true
@@ -1293,6 +1484,112 @@ snapshots:
'@esbuild/win32-x64@0.27.7':
optional: true
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.2
optional: true
'@img/sharp-darwin-x64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.2
optional: true
'@img/sharp-freebsd-wasm32@0.35.3':
dependencies:
'@img/sharp-wasm32': 0.35.3
optional: true
'@img/sharp-libvips-darwin-arm64@1.3.2':
optional: true
'@img/sharp-libvips-darwin-x64@1.3.2':
optional: true
'@img/sharp-libvips-linux-arm64@1.3.2':
optional: true
'@img/sharp-libvips-linux-arm@1.3.2':
optional: true
'@img/sharp-libvips-linux-ppc64@1.3.2':
optional: true
'@img/sharp-libvips-linux-riscv64@1.3.2':
optional: true
'@img/sharp-libvips-linux-s390x@1.3.2':
optional: true
'@img/sharp-libvips-linux-x64@1.3.2':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.2':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.2':
optional: true
'@img/sharp-linux-arm64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.2
optional: true
'@img/sharp-linux-arm@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.2
optional: true
'@img/sharp-linux-ppc64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.2
optional: true
'@img/sharp-linux-riscv64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.2
optional: true
'@img/sharp-linux-s390x@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.2
optional: true
'@img/sharp-linux-x64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.2
optional: true
'@img/sharp-linuxmusl-arm64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.2
optional: true
'@img/sharp-linuxmusl-x64@0.35.3':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.2
optional: true
'@img/sharp-wasm32@0.35.3':
dependencies:
'@emnapi/runtime': 1.11.2
optional: true
'@img/sharp-webcontainers-wasm32@0.35.3':
dependencies:
'@img/sharp-wasm32': 0.35.3
optional: true
'@img/sharp-win32-arm64@0.35.3':
optional: true
'@img/sharp-win32-ia32@0.35.3':
optional: true
'@img/sharp-win32-x64@0.35.3':
optional: true
'@opentelemetry/api@1.9.0': {}
'@resvg/resvg-js-android-arm-eabi@2.6.2':
@@ -1500,6 +1797,8 @@ snapshots:
destroy@1.2.0: {}
detect-libc@2.1.2: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -1881,6 +2180,8 @@ snapshots:
semver-compare@1.0.0: {}
semver@7.8.5: {}
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -1910,6 +2211,39 @@ snapshots:
setprototypeof@1.2.0: {}
sharp@0.35.3(@types/node@24.12.2):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.3
'@img/sharp-darwin-x64': 0.35.3
'@img/sharp-freebsd-wasm32': 0.35.3
'@img/sharp-libvips-darwin-arm64': 1.3.2
'@img/sharp-libvips-darwin-x64': 1.3.2
'@img/sharp-libvips-linux-arm': 1.3.2
'@img/sharp-libvips-linux-arm64': 1.3.2
'@img/sharp-libvips-linux-ppc64': 1.3.2
'@img/sharp-libvips-linux-riscv64': 1.3.2
'@img/sharp-libvips-linux-s390x': 1.3.2
'@img/sharp-libvips-linux-x64': 1.3.2
'@img/sharp-libvips-linuxmusl-arm64': 1.3.2
'@img/sharp-libvips-linuxmusl-x64': 1.3.2
'@img/sharp-linux-arm': 0.35.3
'@img/sharp-linux-arm64': 0.35.3
'@img/sharp-linux-ppc64': 0.35.3
'@img/sharp-linux-riscv64': 0.35.3
'@img/sharp-linux-s390x': 0.35.3
'@img/sharp-linux-x64': 0.35.3
'@img/sharp-linuxmusl-arm64': 0.35.3
'@img/sharp-linuxmusl-x64': 0.35.3
'@img/sharp-webcontainers-wasm32': 0.35.3
'@img/sharp-win32-arm64': 0.35.3
'@img/sharp-win32-ia32': 0.35.3
'@img/sharp-win32-x64': 0.35.3
'@types/node': 24.12.2
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
+29 -1
View File
@@ -4,6 +4,7 @@ import {
type RequestHandler,
type Response,
} from 'express'
import sharp from 'sharp'
import {type AppContext} from '../context.js'
import {httpLogger} from '../logger.js'
@@ -40,11 +41,38 @@ export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
return res.status(500).end('server error')
}
// Cards render avatars at ~210px or smaller, but the CDN's full-size
// variant is up to 1000x1000. satori parses and resvg rasterizes every
// image at its intrinsic size: ~4MB of native RGBA per avatar per render,
// x7 avatars per starter-pack card. That native churn (plus satori's
// 500-entry image LRU holding the oversized data URIs) is what OOMs the
// service. Downscaling to the largest displayed size before handing the
// bytes to the render pipeline cuts decoded pixel volume ~15x with zero
// visible quality change.
const MAX_AVATAR_RENDER_PX = 256
// sharp's libvips operation cache holds decoded images in native memory
// (50MB default per process). This service re-renders unique avatars, so
// cache hits are rare and the cache is pure RSS overhead: disable it.
sharp.cache(false)
export async function getImage(url: string) {
const response = await fetch(ensureJpeg(url))
const arrayBuf = await response.arrayBuffer()
if (response.status !== 200) return null
return Buffer.from(arrayBuf)
return (
sharp(arrayBuf)
.resize({
width: MAX_AVATAR_RENDER_PX,
height: MAX_AVATAR_RENDER_PX,
fit: 'inside',
withoutEnlargement: true,
})
// quality 95: this is a second lossy encode over an already-lossy CDN
// jpeg, and the default (80) visibly softens avatars at card sizes
.jpeg({quality: 95})
.toBuffer()
)
}
// CDN URLs end with @jpeg, @webp, or no extension (which may default to webp).
+1 -1
View File
@@ -90,7 +90,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
+2 -2
View File
@@ -247,8 +247,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-8
View File
@@ -671,9 +671,6 @@
}
},
"src/components/forms/TextField.tsx": {
"@typescript-eslint/no-unsafe-member-access": {
"count": 3
},
"react-hooks/refs": {
"count": 1
}
@@ -987,11 +984,6 @@
"count": 1
}
},
"src/lib/merge-refs.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"src/lib/notifications/notifications.ts": {
"@typescript-eslint/no-floating-promises": {
"count": 5
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.127.0",
"version": "1.128.0",
"private": true,
"engines": {
"node": ">=24.18.0"
@@ -1,3 +1,26 @@
diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts
index 48178d9246ab94d701983ac20473983eb746d1a6..451d38e6e20646c32795db3be28fbea57de1d12f 100644
--- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts
+++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ts
@@ -172,6 +172,18 @@ function useChatKeyboard(
currentHeight.value = e.height;
+ if (scrollViewRef() == null) {
+ // The scroll view can be detached or not yet attached while a
+ // keyboard animation is running (e.g. the chat list re-creating its
+ // scroll component mid-animation). Every branch below eventually
+ // calls scrollTo (directly or via clampScrollIfNeeded); on Paper
+ // reanimated's scrollTo would hand the null ref to the native
+ // _scrollToPaper HostFunction, which throws "Value is null, expected
+ // a number" and crashes in release builds. currentHeight is kept up
+ // to date above so the animated style keeps committing.
+ return;
+ }
+
if (inverted) {
// Skip post-interactive snap-back (duration === -1)
if (e.duration === -1) {
diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts
index 0f6d7c67a307885310ab184fdf9e7a5c7b296825..1a01093e7909973cef5268f999158df389e77634 100644
--- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts
+3 -3
View File
@@ -228,7 +228,7 @@ patchedDependencies:
react-native-compressor@1.13.0: 58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925
react-native-date-picker@5.0.13: f2a6697da7a7ca79b4f39efda142eee1b82db6de3aa5010ad4bd59df41fe2ce1
react-native-drawer-layout@4.2.3: 74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130
react-native-keyboard-controller@1.21.8: 2f6791837622fb391c3e41fc044b91ddedbe503da14da86165ee0a73924b05f1
react-native-keyboard-controller@1.21.8: ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271
react-native-pager-view@6.8.0: 0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108
react-native-reanimated@3.19.1: 97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6
react-native-svg@15.12.1: 31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9
@@ -639,7 +639,7 @@ importers:
version: 2.28.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-keyboard-controller:
specifier: ^1.21.8
version: 1.21.8(patch_hash=2f6791837622fb391c3e41fc044b91ddedbe503da14da86165ee0a73924b05f1)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
version: 1.21.8(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-pager-view:
specifier: 6.8.0
version: 6.8.0(patch_hash=0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -17878,7 +17878,7 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-keyboard-controller@1.21.8(patch_hash=2f6791837622fb391c3e41fc044b91ddedbe503da14da86165ee0a73924b05f1)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
react-native-keyboard-controller@1.21.8(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
@@ -0,0 +1,63 @@
import {Trans, useLingui} from '@lingui/react/macro'
import {atoms as a, useTheme} from '#/alf'
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {IS_IOS} from '#/env'
/**
* Explains what sharing on-device age signals reveals, shown alongside the
* "Share age range" button on the age assurance surfaces (the no-access screen
* and the account settings card).
*
* Platform-split so the copy can name the OS account the age range comes from
* (Apple Account vs Google Account) and reassure users that only a range is
* shared. Device signals are only supported on iOS and Android, so these are
* the only two cases.
*/
export function DeviceSignalsNotice({onPressKws}: {onPressKws: () => void}) {
const t = useTheme()
const {t: l} = useLingui()
return (
<Text style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
{IS_IOS ? (
<Trans comment="Shown next to the 'Share age range' button when on-device age verification is available. KWS is the name of a third-party verification partner.">
Sharing your age range reveals only the range associated with your
Apple Account for example, that youre at least 18.{' '}
<Text style={[a.text_sm, a.font_bold]}>
Your exact age and birthday are never shared,
</Text>{' '}
and this data never leaves this device. Therefore, it only enables
access on this device. Alternatively,{' '}
<SimpleInlineLinkText
label={l`Verify now using KWS`}
{...createStaticClick(() => {
onPressKws()
})}>
you can use our trusted partner, KWS
</SimpleInlineLinkText>
, to complete your verification and enable access on all platforms.
</Trans>
) : (
<Trans comment="Shown next to the 'Share age range' button when on-device age verification is available. KWS is the name of a third-party verification partner.">
Sharing your age range reveals only the range associated with your
Google Account for example, that youre at least 18.{' '}
<Text style={[a.text_sm, a.font_bold]}>
Your exact age and birthday are never shared,
</Text>{' '}
and this data never leaves this device. Therefore, it only enables
access on this device. Alternatively,{' '}
<SimpleInlineLinkText
label={l`Verify now using KWS`}
{...createStaticClick(() => {
onPressKws()
})}>
you can use our trusted partner, KWS
</SimpleInlineLinkText>
, to complete your verification and enable access on all platforms.
</Trans>
)}
</Text>
)
}
+2 -17
View File
@@ -27,6 +27,7 @@ import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
import {useAgeAssurance} from '#/ageAssurance'
import {DeviceSignalsNotice} from '#/ageAssurance/components/DeviceSignalsNotice'
import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import {useAgeAssuranceVerificationFlow} from '#/ageAssurance/useVerificationFlow'
@@ -428,23 +429,7 @@ function AccessSection() {
</Button>
{useDeviceSignals ? (
<Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
<Trans>
Sharing your age data uses information stored on your
device, and will therefore only work on this device.
Alternatively,{' '}
<SimpleInlineLinkText
label={l`Verify now using KWS`}
{...createStaticClick(() => {
openInitDialog()
})}>
you can use our trusted partner, KWS
</SimpleInlineLinkText>
, to complete your verification and enable access on all
platforms.
</Trans>
</Text>
<DeviceSignalsNotice onPressKws={openInitDialog} />
) : lastInitiatedAt && timeAgo && diff ? (
<Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}
+1 -1
View File
@@ -59,7 +59,7 @@ export function useAgeAssuranceVerificationFlow({
const verifyCta =
allowsDeviceVerification && !deviceSignalsFailed
? l`Share age data`
? l`Share age range`
: hasInitiated
? l`Verify again`
: l`Verify now`
+2
View File
@@ -1404,6 +1404,8 @@ export type Events = {
uploadId: string
engine: string
errorClass: string
/** Truncated to 256 chars */
errorMessage: string
elapsedMs: number
}
'video:upload:uploadStarted': {
@@ -22,6 +22,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAgeAssurance} from '#/ageAssurance'
import {DeviceSignalsNotice} from '#/ageAssurance/components/DeviceSignalsNotice'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import {useAgeAssuranceVerificationFlow} from '#/ageAssurance/useVerificationFlow'
import {createGeolocationString} from '#/ageAssurance/util'
@@ -164,23 +165,7 @@ function Inner({style}: ViewStyleProp & {}) {
</Button>
{useDeviceSignals ? (
<Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
<Trans>
Sharing your age data uses information stored on your
device, and will therefore only work on this device.
Alternatively,{' '}
<InlineLinkText
label={l`Verify now using KWS`}
{...createStaticClick(() => {
openInitDialog()
})}>
you can use our trusted partner, KWS
</InlineLinkText>
, to complete your verification and enable access on all
platforms.
</Trans>
</Text>
<DeviceSignalsNotice onPressKws={openInitDialog} />
) : lastInitiatedAt && timeAgo && diff ? (
<Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}
+1 -1
View File
@@ -207,7 +207,7 @@ export function createInput(Component: typeof TextInput) {
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
const flattened = StyleSheet.flatten([
const flattened = StyleSheet.flatten<TextStyle>([
a.relative,
a.z_20,
a.flex_1,
+23 -13
View File
@@ -1,17 +1,8 @@
import {type ImagePickerAsset} from 'expo-image-picker'
import {
ALL_FORMATS,
type AudioCodec,
BlobSource,
BufferTarget,
canEncodeAudio,
canEncodeVideo,
Conversion,
Input,
Mp4OutputFormat,
Output,
type Input as MediaBunnyInput,
type VideoCodec,
WebMOutputFormat,
} from 'mediabunny'
import {VIDEO_MAX_SIZE} from '#/lib/constants'
@@ -23,6 +14,7 @@ import {
COMPRESSION_MIN_SIZE_BYTES,
COMPRESSION_TARGET_BITRATE,
} from './constants'
import {loadMediaBunny} from './mediabunny'
import {type CompressedVideo, type ProbedMetadata} from './types'
// Codecs to try in order of preference
@@ -44,8 +36,12 @@ export async function compressVideo(
hasWebCodecs: hasWebCodecs(),
})
const response = await fetch(asset.uri)
const blob = await response.blob()
/*
* Prefer the original File over re-fetching the blob URL. A fetch round
* trip copies the bytes and fails with a bare TypeError if the URL was
* revoked or the read fails - the top web compressFailed error class.
*/
const blob = asset.file ?? (await (await fetch(asset.uri)).blob())
const isGif = blob.type === 'image/gif'
const hasCodecs = hasWebCodecs()
@@ -112,6 +108,7 @@ export async function compressVideo(
}
async function probeWithMediaBunny(blob: Blob): Promise<ProbedMetadata> {
const {ALL_FORMATS, BlobSource, Input} = await loadMediaBunny()
const input = new Input({source: new BlobSource(blob), formats: ALL_FORMATS})
try {
const videoTrack = await input.getPrimaryVideoTrack()
@@ -153,6 +150,7 @@ async function findEncodableVideoCodec(
width: number,
height: number,
): Promise<{codec: VideoCodec; useWebM: boolean} | null> {
const {canEncodeVideo} = await loadMediaBunny()
for (const codec of VIDEO_CODECS) {
const canEncode = await canEncodeVideo(codec, {
width,
@@ -179,9 +177,10 @@ const AUDIO_CODECS_MP4: AudioCodec[] = ['aac']
const AUDIO_CODECS_WEBM: AudioCodec[] = ['opus', 'vorbis']
async function findEncodableAudioCodec(
audioTrack: Awaited<ReturnType<Input['getPrimaryAudioTrack']>>,
audioTrack: Awaited<ReturnType<MediaBunnyInput['getPrimaryAudioTrack']>>,
useWebM: boolean,
): Promise<{codec: AudioCodec} | null> {
const {canEncodeAudio} = await loadMediaBunny()
if (!audioTrack) {
return null
}
@@ -228,6 +227,17 @@ async function doCompression(
): Promise<CompressedVideo> {
const {onProgress, signal} = opts
const {
ALL_FORMATS,
BlobSource,
BufferTarget,
Conversion,
Input,
Mp4OutputFormat,
Output,
WebMOutputFormat,
} = await loadMediaBunny()
const input = new Input({
source: new BlobSource(blob),
formats: ALL_FORMATS,
+20
View File
@@ -0,0 +1,20 @@
type MediaBunnyModule = typeof import('mediabunny')
let promise: Promise<MediaBunnyModule> | undefined
/**
* Lazily loads mediabunny into its own webpack chunk and caches the resulting
* promise so the module is fetched at most once. If the import fails (e.g. a
* transient chunk-load error), the cached promise is reset so a later call can
* retry the load.
*/
export function loadMediaBunny(): Promise<MediaBunnyModule> {
promise ??= import(/* webpackChunkName: "mediabunny" */ 'mediabunny').catch(
e => {
// reset so a later attempt can retry after a transient chunk-load failure
promise = undefined
throw e
},
)
return promise
}
+6
View File
@@ -26,6 +26,11 @@ function errorClass(e: unknown): string {
return 'Unknown'
}
function errorMessage(e: unknown): string {
const message = e instanceof Error ? e.message : String(e)
return message.slice(0, 256)
}
export type VideoTelemetry = {
readonly uploadId: string
readonly engine: string
@@ -208,6 +213,7 @@ export function createVideoTelemetry({
uploadId,
engine,
errorClass: errorClass(e),
errorMessage: errorMessage(e),
elapsedMs: Date.now() - phaseStartedAt,
})
endTxn('error')
+32 -11
View File
@@ -1,3 +1,22 @@
import {type Ref, type RefCallback} from 'react'
/**
* Assigns a value to a ref.
* @param ref The ref to assign the value to.
* @param value The value to assign to the ref.
* @returns The ref cleanup callback, if any.
*/
function assignRef<T>(
ref: Ref<T> | undefined | null,
value: T | null,
): ReturnType<RefCallback<T>> {
if (typeof ref === 'function') {
return ref(value)
} else if (ref) {
ref.current = value
}
}
/**
* This TypeScript function merges multiple React refs into a single ref callback.
* When developing low level UI components, it is common to have to use a local ref
@@ -12,16 +31,18 @@
* @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and
* returns a ref callback function that can be used to merge multiple refs into a single ref.
*/
export function mergeRefs<T = any>(
refs: Array<React.MutableRefObject<T> | React.Ref<T> | undefined>,
): React.RefCallback<T> {
return value => {
refs.forEach(ref => {
if (typeof ref === 'function') {
ref(value)
} else if (ref != null) {
;(ref as React.MutableRefObject<T | null>).current = value
}
})
export function mergeRefs<T>(refs: (Ref<T> | undefined)[]): Ref<T> {
return (value: T | null) => {
const cleanups: (() => void)[] = []
for (const ref of refs) {
const cleanup = assignRef(ref, value)
const isCleanup = typeof cleanup === 'function'
cleanups.push(isCleanup ? cleanup : () => assignRef(ref, null))
}
return () => {
for (const cleanup of cleanups) cleanup()
}
}
}
+102 -95
View File
@@ -868,7 +868,7 @@ msgstr "<0>{displayName}</0><1/><2> added you</2>"
msgid "<0>Sign in</0><1> or </1><2>create an account</2><3> </3><4>to search for news, sports, politics, and everything else happening on Bluesky.</4>"
msgstr ""
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:265
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:250
msgid "<0>Tap here to update your location with GPS.</0>"
msgstr "<0>Tap here to update your location with GPS.</0>"
@@ -1164,7 +1164,7 @@ msgstr ""
msgid "Add another post to thread"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:459
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:435
msgid "Add another search filter"
msgstr "Add another search filter"
@@ -1186,7 +1186,7 @@ msgstr "Add automation label to account"
msgid "Add emoji reaction"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:467
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:443
msgid "Add filter"
msgstr "Add filter"
@@ -1285,7 +1285,7 @@ msgstr ""
msgid "Add user to list"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:296
#: src/ageAssurance/components/NoAccessScreen.tsx:297
msgid "Add your birthdate"
msgstr ""
@@ -1361,11 +1361,6 @@ msgstr ""
msgid "Advanced"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:76
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:270
msgid "Advanced search"
msgstr "Advanced search"
#: src/components/ageAssurance/AgeAssuranceBadge.tsx:42
msgid "Age Assurance"
msgstr ""
@@ -1379,11 +1374,11 @@ msgctxt "toast"
msgid "Age assurance inquiry was submitted"
msgstr ""
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:200
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:185
msgid "Age assurance only takes a few minutes"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:464
#: src/ageAssurance/components/NoAccessScreen.tsx:449
msgid "Age assurance only takes a few minutes."
msgstr "Age assurance only takes a few minutes."
@@ -1425,7 +1420,7 @@ msgstr ""
msgid "All languages will be shown in your feeds."
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:277
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:245
msgid "All of these words"
msgstr "All of these words"
@@ -1703,10 +1698,12 @@ msgstr ""
#. Placeholder text for a date picker
#: src/screens/Search/components/AdvancedSearchDialog/ClearableDateField.tsx:43
msgid "Any time"
msgstr "Any time"
msgid "Any date"
msgstr "Any date"
#: src/components/dialogs/PostInteractionSettingsDialog.tsx:457
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:22
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:25
msgid "Anyone"
msgstr ""
@@ -2020,19 +2017,19 @@ msgstr ""
msgid "Banned user returning"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:183
#: src/ageAssurance/components/NoAccessScreen.tsx:184
msgid "Based on your device's location, we think you're in <0>{geolocationString}</0>."
msgstr "Based on your device's location, we think you're in <0>{geolocationString}</0>."
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:251
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:236
msgid "Based on your device's location, we think you're in <0>{region}</0>."
msgstr "Based on your device's location, we think you're in <0>{region}</0>."
#: src/ageAssurance/components/NoAccessScreen.tsx:193
#: src/ageAssurance/components/NoAccessScreen.tsx:194
msgid "Based on your network, we think you're in <0>{geolocationString}</0>. This estimate may be inaccurate if you're using a VPN."
msgstr "Based on your network, we think you're in <0>{geolocationString}</0>. This estimate may be inaccurate if you're using a VPN."
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:256
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:241
msgid "Based on your network, we think you're in <0>{region}</0>. This estimate may be inaccurate if you're using a VPN."
msgstr "Based on your network, we think you're in <0>{region}</0>. This estimate may be inaccurate if you're using a VPN."
@@ -2462,8 +2459,8 @@ msgstr "Camera access needed"
#: src/screens/Messages/ConversationSettings/prompts.tsx:180
#: src/screens/Profile/Header/EditProfileDialog.tsx:215
#: src/screens/Profile/Header/EditProfileDialog.tsx:223
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:234
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:241
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:202
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:209
#: src/screens/Settings/AppIconSettings/index.tsx:42
#: src/screens/Settings/AppIconSettings/index.tsx:228
#: src/screens/Settings/components/ChangeHandleDialog.tsx:80
@@ -2493,7 +2490,7 @@ msgstr ""
msgid "Cancel reply"
msgstr "Cancel reply"
#: src/screens/Search/Shell.tsx:565
#: src/screens/Search/Shell.tsx:583
msgid "Cancel search"
msgstr ""
@@ -2525,8 +2522,8 @@ msgstr "carousel"
msgid "Cashtag {tag}"
msgstr ""
#. Advanced search: Example of an “all of these words” search
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:159
#. Advanced search: Example of an “all of these words” search. Paired with “cows pigs”.
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:250
msgid "cats dogs"
msgstr "cats dogs"
@@ -2835,11 +2832,11 @@ msgstr "Click here to add people to this group chat"
msgid "Click here to create or manage an invite link for this group chat"
msgstr "Click here to create or manage an invite link for this group chat"
#: src/ageAssurance/components/NoAccessScreen.tsx:326
#: src/ageAssurance/components/NoAccessScreen.tsx:327
msgid "Click here to delete your account"
msgstr "Click here to delete your account"
#: src/ageAssurance/components/NoAccessScreen.tsx:317
#: src/ageAssurance/components/NoAccessScreen.tsx:318
msgid "Click here to log out"
msgstr ""
@@ -2852,8 +2849,8 @@ msgstr "Click here to resend the email"
msgid "Click here to restart the verification process."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:118
#: src/ageAssurance/components/NoAccessScreen.tsx:293
#: src/ageAssurance/components/NoAccessScreen.tsx:119
#: src/ageAssurance/components/NoAccessScreen.tsx:294
msgid "Click here to update your birthdate"
msgstr ""
@@ -3115,13 +3112,13 @@ msgstr "Connecting…"
msgid "Connection issue"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:403
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:127
#: src/ageAssurance/components/NoAccessScreen.tsx:404
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:128
#: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:31
msgid "Contact our moderation team"
msgstr ""
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:108
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:109
msgid "Contact our support team"
msgstr ""
@@ -3501,8 +3498,8 @@ msgstr "Couldnt load GIFs"
msgid "Country code"
msgstr ""
#. Advanced search: Example of a “none of these words” search
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:163
#. Advanced search: Example of a “none of these words” search. Paired with “cats dogs”.
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:268
msgid "cows pigs"
msgstr "cows pigs"
@@ -3884,9 +3881,9 @@ msgstr "Device failed to translate :("
msgid "Dialog: adjust who can interact with this post"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:265
msgid "Dialog: Set advanced search options"
msgstr "Dialog: Set advanced search options"
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:233
msgid "Dialog: Set search filters"
msgstr "Dialog: Set search filters"
#: src/screens/Settings/AppearanceSettings.tsx:110
msgid "Dim"
@@ -4591,8 +4588,6 @@ msgstr ""
msgid "Everybody can reply to this post."
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:22
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:26
#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:168
#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:171
msgid "Everyone"
@@ -5169,11 +5164,11 @@ msgstr ""
msgid "File saved successfully!"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:49
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:48
msgid "Filter by author"
msgstr "Filter by author"
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:30
#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:29
msgid "Filter by author (currently: {currentLabel})"
msgstr "Filter by author (currently: {currentLabel})"
@@ -5210,6 +5205,12 @@ msgstr ""
msgid "Filter who you receive notifications from"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:78
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:238
msgctxt "search"
msgid "Filters"
msgstr "Filters"
#: src/screens/Onboarding/StepFinished/index.tsx:335
msgid "Finalizing"
msgstr ""
@@ -5264,7 +5265,7 @@ msgstr ""
msgid "Find people you know"
msgstr "Find people you know"
#: src/screens/Search/Shell.tsx:742
#: src/screens/Search/Shell.tsx:760
msgid "Find posts, users, and feeds on Bluesky"
msgstr ""
@@ -5490,7 +5491,7 @@ msgstr ""
msgid "Food"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:105
#: src/ageAssurance/components/NoAccessScreen.tsx:106
msgid "For organizational accounts, use the birthdate of the person who is responsible for the account."
msgstr ""
@@ -5541,7 +5542,7 @@ msgstr "Four message bubbles representing a group chat. First message: \"Did you
msgid "Free your feed"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:430
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:406
#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:159
msgid "From"
msgstr ""
@@ -6007,11 +6008,11 @@ msgstr ""
msgid "Hey there 👋"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:170
#: src/ageAssurance/components/NoAccessScreen.tsx:171
msgid "Hey there!"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:275
#: src/ageAssurance/components/NoAccessScreen.tsx:276
msgid "Hi there!"
msgstr ""
@@ -6253,7 +6254,7 @@ msgstr ""
msgid "If none are selected, all languages will be shown in your feeds."
msgstr ""
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:102
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:103
msgid "If you are 18 years of age or older and want to try again, click the button below and use a different verification method if one is available in your region. If you have questions or concerns, <0>our support team can help.</0>"
msgstr ""
@@ -6261,7 +6262,7 @@ msgstr ""
msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:115
#: src/ageAssurance/components/NoAccessScreen.tsx:116
msgid "If you believe your birthdate is incorrect, you can update it by <0>clicking here</0>."
msgstr ""
@@ -6396,7 +6397,7 @@ msgstr ""
msgid "Imported on {0}"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:278
#: src/ageAssurance/components/NoAccessScreen.tsx:279
msgid "In order to provide an age-appropriate experience, we need to know your birthdate. This is a one-time thing, and your data will be kept private."
msgstr ""
@@ -6444,7 +6445,7 @@ msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/FilterBlock.tsx:58
#: src/screens/Search/components/AdvancedSearchDialog/FilterBlock.tsx:75
#: src/screens/Search/components/AdvancedSearchDialog/FilterBlock.tsx:81
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:416
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:392
msgid "Include"
msgstr "Include"
@@ -6458,12 +6459,12 @@ msgid "Include posts and/or replies (currently: {currentLabel})"
msgstr "Include posts and/or replies (currently: {currentLabel})"
#. Advanced search filter
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:329
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:305
msgid "Include posts made since this date"
msgstr "Include posts made since this date"
#. Advanced search filter
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:353
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:329
msgid "Include posts made until this date"
msgstr "Include posts made until this date"
@@ -6740,7 +6741,7 @@ msgstr ""
msgid "Labels on your content"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:381
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:357
msgid "Language"
msgstr "Language"
@@ -6758,13 +6759,13 @@ msgstr ""
msgid "Larger"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:458
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:194
#: src/ageAssurance/components/NoAccessScreen.tsx:443
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:179
msgid "Last initiated {timeAgo} ago"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:456
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:192
#: src/ageAssurance/components/NoAccessScreen.tsx:441
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:177
msgid "Last initiated just now"
msgstr ""
@@ -7338,7 +7339,7 @@ msgstr "Marked all requests as read"
msgid "Maybe later"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:399
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:375
#: src/view/screens/Profile.tsx:236
msgid "Media"
msgstr ""
@@ -8176,8 +8177,8 @@ msgstr ""
msgid "Non-sexual Nudity"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:307
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:310
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:263
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:266
msgid "None of these words"
msgstr "None of these words"
@@ -8418,7 +8419,7 @@ msgstr "Oops, this profile doesn't exist. Try scanning another one."
msgid "Oops!"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:64
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:66
msgid "Open advanced search options"
msgstr "Open advanced search options"
@@ -10444,11 +10445,11 @@ msgstr ""
#: src/components/dialogs/SearchablePeopleList.tsx:667
#: src/components/forms/SearchInput.tsx:51
#: src/components/forms/SearchInput.tsx:53
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:250
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:256
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:218
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:224
#: src/screens/Search/Shell.tsx:535
#: src/screens/Search/Shell.tsx:585
#: src/screens/Search/Shell.tsx:730
#: src/screens/Search/Shell.tsx:603
#: src/screens/Search/Shell.tsx:748
#: src/view/shell/bottom-bar/BottomBar.tsx:216
msgid "Search"
msgstr ""
@@ -10537,7 +10538,7 @@ msgstr ""
msgid "Search profiles"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:280
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:248
msgid "Search query"
msgstr "Search query"
@@ -10883,7 +10884,7 @@ msgstr ""
msgid "Set who can reply to your post"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:285
#: src/ageAssurance/components/NoAccessScreen.tsx:286
msgid "Set your birthdate below and we'll get you back to posting and exploring in no time!"
msgstr ""
@@ -10986,8 +10987,8 @@ msgid "Share"
msgstr ""
#: src/ageAssurance/useVerificationFlow.ts:62
msgid "Share age data"
msgstr "Share age data"
msgid "Share age range"
msgstr "Share age range"
#: src/view/com/profile/ProfileMenu.tsx:549
msgid "Share anyway"
@@ -11044,7 +11045,8 @@ msgstr ""
msgid "Share this feed"
msgstr ""
#: src/screens/Search/Shell.tsx:617
#: src/screens/Search/Shell.tsx:556
#: src/screens/Search/Shell.tsx:625
msgid "Share this search"
msgstr "Share this search"
@@ -11075,10 +11077,15 @@ msgstr ""
msgid "Shared Preferences Tester"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:433
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:169
msgid "Sharing your age data uses information stored on your device, and will therefore only work on this device. Alternatively, <0>you can use our trusted partner, KWS</0>, to complete your verification and enable access on all platforms."
msgstr "Sharing your age data uses information stored on your device, and will therefore only work on this device. Alternatively, <0>you can use our trusted partner, KWS</0>, to complete your verification and enable access on all platforms."
#. Shown next to the 'Share age range' button when on-device age verification is available. KWS is the name of a third-party verification partner.
#: src/ageAssurance/components/DeviceSignalsNotice.tsx:25
msgid "Sharing your age range reveals only the range associated with your Apple Account for example, that youre at least 18. <0>Your exact age and birthday are never shared,</0> and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS</1>, to complete your verification and enable access on all platforms."
msgstr "Sharing your age range reveals only the range associated with your Apple Account for example, that youre at least 18. <0>Your exact age and birthday are never shared,</0> and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS</1>, to complete your verification and enable access on all platforms."
#. Shown next to the 'Share age range' button when on-device age verification is available. KWS is the name of a third-party verification partner.
#: src/ageAssurance/components/DeviceSignalsNotice.tsx:43
msgid "Sharing your age range reveals only the range associated with your Google Account for example, that youre at least 18. <0>Your exact age and birthday are never shared,</0> and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS</1>, to complete your verification and enable access on all platforms."
msgstr "Sharing your age range reveals only the range associated with your Google Account for example, that youre at least 18. <0>Your exact age and birthday are never shared,</0> and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS</1>, to complete your verification and enable access on all platforms."
#: src/components/moderation/ContentHider.tsx:220
#: src/components/moderation/LabelPreference.tsx:143
@@ -11308,8 +11315,8 @@ msgstr ""
msgid "Signed in as @{0}"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:323
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:326
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:299
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:302
msgid "Since"
msgstr "Since"
@@ -11801,7 +11808,7 @@ msgstr ""
msgid "Tap for more information"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:212
#: src/ageAssurance/components/NoAccessScreen.tsx:213
#: src/screens/Signup/StepInfo/index.tsx:326
msgid "Tap here to update your location with GPS."
msgstr "Tap here to update your location with GPS."
@@ -11931,8 +11938,8 @@ msgstr ""
msgid "Thanks, you have successfully verified your email address. You can close this dialog."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:232
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:239
#: src/ageAssurance/components/NoAccessScreen.tsx:233
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:224
msgid "Thanks! You're all set."
msgstr ""
@@ -12347,8 +12354,8 @@ msgstr ""
msgid "This email is already associated with your account."
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:291
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:294
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:280
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:283
msgid "This exact phrase"
msgstr "This exact phrase"
@@ -12656,7 +12663,7 @@ msgstr ""
msgid "To disable your email 2FA method, please verify your access to <0>{0}</0>"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:314
#: src/ageAssurance/components/NoAccessScreen.tsx:315
msgid "To log out, <0>click here</0>. Or if youd prefer, you can <1>delete your account</1>."
msgstr "To log out, <0>click here</0>. Or if youd prefer, you can <1>delete your account</1>."
@@ -12951,11 +12958,11 @@ msgstr ""
msgid "Unfortunately, none of your subscribed labelers supports this report type."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:262
#: src/ageAssurance/components/NoAccessScreen.tsx:263
msgid "Unfortunately, the birthdate you have saved to your profile makes you too young to access Bluesky."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:246
#: src/ageAssurance/components/NoAccessScreen.tsx:247
msgid "Unfortunately, your declared age indicates that you are not old enough to access Bluesky in your region."
msgstr ""
@@ -13105,8 +13112,8 @@ msgstr "Unsupported clipboard content"
msgid "Unsupported video type: {mimeType}"
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:347
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:350
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:323
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:326
msgid "Until"
msgstr "Until"
@@ -13150,8 +13157,8 @@ msgstr ""
msgid "Update your email"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:207
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:267
#: src/ageAssurance/components/NoAccessScreen.tsx:208
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:252
#: src/components/dialogs/DeviceLocationRequestDialog.tsx:41
#: src/components/dialogs/DeviceLocationRequestDialog.tsx:106
msgid "Update your location"
@@ -13399,8 +13406,8 @@ msgstr ""
msgid "Verify now"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:438
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:174
#: src/ageAssurance/components/DeviceSignalsNotice.tsx:34
#: src/ageAssurance/components/DeviceSignalsNotice.tsx:52
msgid "Verify now using KWS"
msgstr "Verify now using KWS"
@@ -13886,8 +13893,8 @@ msgstr "Were so excited to have you join us!"
msgid "We're sorry, but based on the data shared by your device, you are not old enough to access Bluesky."
msgstr "We're sorry, but based on the data shared by your device, you are not old enough to access Bluesky."
#: src/ageAssurance/components/NoAccessScreen.tsx:226
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:233
#: src/ageAssurance/components/NoAccessScreen.tsx:227
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:218
msgid "We're sorry, but based on your device's location, you are currently located in a region that requires age assurance."
msgstr ""
@@ -13959,7 +13966,7 @@ msgid "What do you want to call your starter pack?"
msgstr ""
#. Advanced search: Example of an “exact phrase” search
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:296
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:285
msgid "whats up"
msgstr "whats up"
@@ -14125,7 +14132,7 @@ msgstr ""
msgid "You are a trusted verifier"
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:173
#: src/ageAssurance/components/NoAccessScreen.tsx:174
msgid "You are accessing Bluesky from a region that legally requires us to verify your age before allowing you to access the app."
msgstr ""
@@ -14150,8 +14157,8 @@ msgstr ""
msgid "You are currently blocked from using the Go Live feature. To appeal this moderation decision, please submit the form below."
msgstr ""
#: src/ageAssurance/components/NoAccessScreen.tsx:399
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:123
#: src/ageAssurance/components/NoAccessScreen.tsx:400
#: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:124
msgid "You are currently unable to access Bluesky's Age Assurance flow. Please <0>contact our moderation team</0> if you believe this is an error."
msgstr ""
@@ -14628,7 +14635,7 @@ msgstr ""
msgid "You've reached the maximum number of requests allowed. Please try again later."
msgstr ""
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:450
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:426
msgid "Youve reached the maximum of {MAX_FILTERS, plural, one {# filter} other {# filters}}. Add more values to an existing filter instead of creating new ones."
msgstr "Youve reached the maximum of {MAX_FILTERS, plural, one {# filter} other {# filters}}. Add more values to an existing filter instead of creating new ones."
+40 -22
View File
@@ -540,6 +540,24 @@ export function SearchScreenShell({
value={filters.lang ?? ''}
onChange={onChangeLang}
/>
) : showFilters && advancedSearchV2Enabled ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog
disabled={activeTab > 1}
q={searchText}
filters={filters}
onSubmit={onSubmitAdvanced}
/>
<Button
accessibilityRole="button"
size="small"
color="secondary"
shape="round"
label={l`Share this search`}
onPress={onShareSearch}>
<ButtonIcon icon={ShareIcon} />
</Button>
</View>
) : (
<Layout.Header.Slot />
)}
@@ -558,19 +576,19 @@ export function SearchScreenShell({
/>
)}
<View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}>
<View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_sm]}>
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
{showAutocomplete && (
<Button
label={l`Cancel search`}
size="large"
size="small"
variant="ghost"
color="secondary"
shape="round"
style={[a.px_sm]}
onPress={onPressCancelSearch}
hitSlop={HITSLOP_10}>
<ButtonIcon icon={ArrowLeftIcon} />
<ButtonIcon icon={ArrowLeftIcon} size="lg" />
</Button>
)}
<View style={[a.flex_1]}>
@@ -591,6 +609,25 @@ export function SearchScreenShell({
/>
</View>
</View>
{showFilters && !showHeader && advancedSearchV2Enabled ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog
disabled={activeTab > 1}
q={searchText}
filters={filters}
onSubmit={onSubmitAdvanced}
/>
<Button
accessibilityRole="button"
size="small"
color="secondary"
shape="round"
label={l`Share this search`}
onPress={onShareSearch}>
<ButtonIcon icon={ShareIcon} />
</Button>
</View>
) : null}
</View>
{showFilters && !showHeader && !advancedSearchV2Enabled && (
@@ -601,25 +638,6 @@ export function SearchScreenShell({
/>
</View>
)}
{showFilters && advancedSearchV2Enabled ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<AdvancedSearchDialog
q={searchText}
filters={filters}
onSubmit={onSubmitAdvanced}
/>
<Button
accessibilityRole="button"
size="small"
color="secondary"
shape="round"
label={l`Share this search`}
onPress={onShareSearch}>
<ButtonIcon icon={ShareIcon} />
</Button>
</View>
) : null}
</View>
</View>
</Layout.Center>
@@ -41,7 +41,7 @@ export function ClearableDateField({
*/
value={active ? value : ''}
placeholder={l({
message: 'Any time',
message: 'Any date',
comment: 'Placeholder text for a date picker',
})}
accessibilityHint={accessibilityHint}
@@ -19,11 +19,10 @@ export function FollowingDropdown({
const {t: l} = useLingui()
const options: {value: FollowingFilter; label: string}[] = [
{value: 'everyone', label: l`Everyone`},
{value: 'anyone', label: l`Anyone`},
{value: 'following', label: l`People you follow`},
]
const currentLabel =
options.find(o => o.value === value)?.label ?? l`Everyone`
const currentLabel = options.find(o => o.value === value)?.label ?? l`Anyone`
return (
<Menu.Root>
@@ -14,7 +14,7 @@ const emptySerializeState = {
language: '',
replies: 'all' as const,
media: 'all' as const,
following: 'everyone' as const,
following: 'anyone' as const,
dateSince: '',
dateSinceActive: false,
dateUntil: '',
@@ -145,10 +145,10 @@ describe(`AdvancedSearchDialog serialize/parse`, () => {
expect(out.filters.following).toBe('true')
})
it(`leaves the following param unset for everyone`, () => {
it(`leaves the following param unset for anyone`, () => {
const out = serializeAdvancedSearch({
...emptySerializeState,
following: 'everyone',
following: 'anyone',
})
expect(out.filters.following).toBeUndefined()
})
@@ -157,7 +157,7 @@ describe(`AdvancedSearchDialog serialize/parse`, () => {
expect(parseAdvancedSearch('', {following: 'true'}).following).toBe(
'following',
)
expect(parseAdvancedSearch('', {}).following).toBe('everyone')
expect(parseAdvancedSearch('', {}).following).toBe('anyone')
})
it(`strips redundant markers from filter values on serialize`, () => {
@@ -177,7 +177,7 @@ describe(`AdvancedSearchDialog serialize/parse`, () => {
language: '',
replies: 'all',
media: 'all',
following: 'everyone',
following: 'anyone',
dateSince: '',
dateSinceActive: false,
dateUntil: '',
@@ -38,17 +38,18 @@ import {
const MAX_FILTERS = 20
export function AdvancedSearchDialog({
disabled,
q,
filters,
onSubmit,
}: {
disabled: boolean
q: string
filters: SearchFilters
onSubmit: (q: string, filters: SearchFilters) => void
}) {
const ax = useAnalytics()
const {t: l} = useLingui()
const t = useTheme()
const control = Dialog.useDialogControl()
const filtersActive = hasActiveFilters(filters)
const stateKey = useMemo(
@@ -61,9 +62,10 @@ export function AdvancedSearchDialog({
<>
<View style={[a.relative]}>
<Button
disabled={disabled}
label={l`Open advanced search options`}
size="small"
color="secondary"
color={filtersActive ? 'primary_subtle' : 'secondary'}
style={native([a.py_sm, a.px_sm])}
onPress={() => {
ax.metric('search:advanced:press', {
@@ -73,25 +75,9 @@ export function AdvancedSearchDialog({
}}>
<ButtonIcon icon={SettingsSliderIcon} />
<ButtonText>
<Trans>Advanced search</Trans>
<Trans context="search">Filters</Trans>
</ButtonText>
</Button>
{filtersActive && (
<View
accessible={false}
style={[
a.absolute,
a.rounded_full,
{
top: -2,
right: -2,
width: 10,
height: 10,
backgroundColor: t.palette.primary_500,
},
]}
/>
)}
</View>
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
@@ -154,24 +140,6 @@ function DialogInner({
const scrollRef = useRef<ScrollView>(null)
const filtersSectionRef = useRef<View>(null)
const suggestions = [
{
all: l({
message: 'cats dogs',
comment: 'Advanced search: Example of an “all of these words” search',
}),
none: l({
message: 'cows pigs',
comment: 'Advanced search: Example of a “none of these words” search',
}),
},
]
// eslint-disable-next-line react/hook-use-state
const [suggestion] = useState(() =>
Math.floor(Math.random() * suggestions.length),
)
function addFilter() {
if (filters.length >= MAX_FILTERS) return
/*
@@ -262,12 +230,12 @@ function DialogInner({
return (
<Dialog.ScrollableInner
ref={scrollRef}
label={l`Dialog: Set advanced search options`}
label={l`Dialog: Set search filters`}
contentContainerStyle={[a.px_0, a.pt_0]}
header={
<Dialog.Header renderLeft={cancelButton} renderRight={searchButton}>
<Dialog.HeaderText>
<Trans>Advanced search</Trans>
<Trans context="search">Filters</Trans>
</Dialog.HeaderText>
</Dialog.Header>
}>
@@ -279,13 +247,34 @@ function DialogInner({
<ClearableInput
label={l`Search query`}
defaultValue={query}
placeholder={suggestions[suggestion].all}
placeholder={l({
message: 'cats dogs',
comment:
'Advanced search: Example of an “all of these words” search. Paired with “cows pigs”.',
})}
onChangeText={setQuery}
onSubmitEditing={handlePressSearch}
/>
</View>
<View style={[twoColumn ? a.flex_row : a.flex_col, a.gap_xl]}>
<View style={[a.flex_1]}>
<TextField.LabelText>
<Trans>None of these words</Trans>
</TextField.LabelText>
<ClearableInput
label={l`None of these words`}
defaultValue={negatedWords}
placeholder={l({
message: 'cows pigs',
comment:
'Advanced search: Example of a “none of these words” search. Paired with “cats dogs”.',
})}
onChangeText={setNegatedWords}
onSubmitEditing={handlePressSearch}
/>
</View>
<View style={[a.flex_1]}>
<TextField.LabelText>
<Trans>This exact phrase</Trans>
@@ -301,19 +290,6 @@ function DialogInner({
onSubmitEditing={handlePressSearch}
/>
</View>
<View style={[a.flex_1]}>
<TextField.LabelText>
<Trans>None of these words</Trans>
</TextField.LabelText>
<ClearableInput
label={l`None of these words`}
defaultValue={negatedWords}
placeholder={suggestions[suggestion].none}
onChangeText={setNegatedWords}
onSubmitEditing={handlePressSearch}
/>
</View>
</View>
<View>
@@ -15,9 +15,9 @@ export type MediaFilter = 'all' | 'media' | 'video'
/**
* Whether to limit results to authors the user follows. Serializes into the
* `following` sibling param ('following' -> following:true, everyone -> unset).
* `following` sibling param ('following' -> following:true, anyone -> unset).
*/
export type FollowingFilter = 'everyone' | 'following'
export type FollowingFilter = 'anyone' | 'following'
export type FilterField = 'authors' | 'mentions' | 'domains' | 'urls' | 'tags'
@@ -260,7 +260,7 @@ export function parseAdvancedSearch(
language: lang,
replies,
media,
following: filters.following === 'true' ? 'following' : 'everyone',
following: filters.following === 'true' ? 'following' : 'anyone',
since: since && isValidDate(since) ? since : '',
until: until && isValidDate(until) ? until : '',
filters: filterRows,
+5 -1
View File
@@ -1,6 +1,6 @@
import {type ImagePickerAsset} from 'expo-image-picker'
import {ALL_FORMATS, BlobSource, Input} from 'mediabunny'
import {loadMediaBunny} from '#/lib/media/video/mediabunny'
import {logger} from '#/logger'
export function hasWebCodecs(): boolean {
@@ -55,6 +55,7 @@ async function getMetadataWithWebCodecs(
file: File,
blobUrl: string,
): Promise<ImagePickerAsset> {
const {ALL_FORMATS, BlobSource, Input} = await loadMediaBunny()
const input = new Input({
source: new BlobSource(file),
formats: ALL_FORMATS,
@@ -72,6 +73,7 @@ async function getMetadataWithWebCodecs(
return {
uri: blobUrl,
file,
mimeType: file.type,
width: videoTrack.displayWidth,
height: videoTrack.displayHeight,
@@ -92,6 +94,7 @@ async function getMetadataWithBrowserAPIs(
img.onload = () => {
resolve({
uri: blobUrl,
file,
mimeType: 'image/gif',
width: img.width,
height: img.height,
@@ -111,6 +114,7 @@ async function getMetadataWithBrowserAPIs(
video.onloadedmetadata = () => {
resolve({
uri: blobUrl,
file,
mimeType: file.type,
width: video.videoWidth,
height: video.videoHeight,