Compare commits

..

78 Commits

Author SHA1 Message Date
Eric Bailey 8f641f8a1b WIP sorta working op threads 2025-10-14 20:29:43 -05:00
Eric Bailey c428568b0c Move indent calc to metadata, add ui field to readMore, use indent 2025-10-14 16:22:59 -05:00
Eric Bailey df9c289f55 Hide labels on posts unless post is anchor 2025-10-14 16:22:59 -05:00
Eric Bailey 6a84088dba Fix isRoot calc 2025-10-14 16:22:59 -05:00
Eric Bailey 99fbe2b680 WIP OP continuous thread 2025-10-14 16:22:59 -05:00
Eric Bailey 92f58c8723 The counters serve the same purpose, we only need to know the count of the actual replies rendered to the view in order to calculate the replyIndex 2025-10-14 16:21:14 -05:00
Eric Bailey fceec83e08 isLastSiblingByCounts isn't needed, should rely only on the count of replies seen 2025-10-14 16:21:14 -05:00
Eric Bailey b928e27c18 Update comments 2025-10-14 16:21:14 -05:00
Eric Bailey 8d4ce43301 Fix computation of isLastSibling and isLastChild to account for muted or
otherwise hidden replies
2025-10-14 16:21:14 -05:00
Eric Bailey 7280e44ea2 Remove log (#9204) 2025-10-14 11:49:02 -05:00
Eric Bailey 7e6b23bd86 Fix ternary in moderation settings that caused a double title (#9198) 2025-10-14 09:17:34 -05:00
pfrazee d2bc22e9be Nightly source-language update 2025-10-14 02:36:22 +00:00
Eric Bailey 0b50e9f109 Remove remaining usages of old post thread query (#9184)
* Remove remaining usages of old post thread query

* Add PostThreadContext, cache mutator for threadgates on threads, pipe it through

* Replace getPostThread in threadgate query

* Replace in initQuote handling, which isn't even used rn...

* Missing import

* Revert ext change
2025-10-13 18:36:11 -05:00
Samuel Newman 0f6a99c8a5 enable numberOfLines on replyTo component (#9196) 2025-10-13 09:36:06 -07:00
Samuel Newman ea9986f127 fix label spacing (#9195) 2025-10-13 09:35:31 -07:00
Samuel Newman 5222a6e1c3 tidy up post reasons (#9194) 2025-10-13 08:24:02 -07:00
pfrazee b7f505ab00 Nightly source-language update 2025-10-11 02:32:55 +00:00
Eric Bailey 8a30131686 Combine SubtleWebHover and SubtleHover, default to web only for all cases (#9188) 2025-10-10 15:05:20 -05:00
Samuel Newman 69421d1f33 Change anchor text size to lg from xl (#9185) 2025-10-10 14:32:56 -05:00
Eric Bailey 995808e816 Bump alf package to include new colors (#9187) 2025-10-10 14:30:15 -05:00
Samuel Newman 508fa2aced Value prop screen - polish, convert to pager (#9133)
* turn value prop screen into a pager on native

* rm ts-ingore

* fix pager swipe on android

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-10-10 14:05:22 -05:00
Samuel Newman d528ae791f Fix add/edit moderation list form placeholder text (#9074)
* fix placeholder text of modlist dialog

* change header font weight

* fix string
2025-10-10 13:57:42 -05:00
Samuel Newman b15903e0cb move feed context on desktop (#9015) 2025-10-10 13:56:57 -05:00
Samuel Newman 9bea450803 Fix profile label section soft reset scroll (#9103)
* Fix profile label section scrollto

* revert back to headerHeight
2025-10-10 13:56:23 -05:00
Samuel Newman fb5d56422a fix border color of feed/list embeds (#9021) 2025-10-10 13:55:43 -05:00
Samuel Newman ad38d905e4 fix newline backspace behaviour (#9038) 2025-10-10 13:55:16 -05:00
Samuel Newman 2b34721876 fix android admonition flex collapse issue (#9120) 2025-10-10 11:43:23 -05:00
Samuel Newman c928d6b14a Keep the screen awake on the video feed (#9146) 2025-10-10 11:33:09 -05:00
Samuel Newman 0dd71fb087 Add accessibilityRole to single images (#9148) 2025-10-10 11:31:12 -05:00
Samuel Newman 54b8eacba1 Fix chat request buttons not moving with swipe gesture (#9155)
* portal in buttons so they move with swipe

* remove outline style buttons
2025-10-10 11:30:29 -05:00
Samuel Newman b7b47d30bf fix hider alignment in thread (#9168) 2025-10-10 11:26:48 -05:00
Samuel Newman 3561fc8112 move aspect ratio to atom (#9171) 2025-10-10 11:20:41 -05:00
Samuel Newman fe31cf4508 fix background colors (#9174) 2025-10-10 11:17:57 -05:00
Eric Bailey a9efd94983 Placeholder style tweaks (#9107)
* Align colors in loading placeholders

* Same for new skele components

* Increase opacity of disabled replies button slightly
2025-10-10 11:16:40 -05:00
Samuel Newman 6cd6a3447d Tweak greens (#9177)
* tweak greens

* undo test code
2025-10-10 09:15:48 -07:00
Samuel Newman ec8c36e614 add 10% sample rate to sentry (#9182) 2025-10-10 08:31:38 -07:00
Samuel Newman 84cae44cd2 Ship activation experiments (#9170)
* ship onboarding experiments

* delete logged out cta

* delete other loggedoutcta
2025-10-10 08:24:42 -07:00
Eric Bailey 5bf1141b55 Add validation to threadgate records, check for max hidden replies (#9178) 2025-10-10 10:03:47 -05:00
pfrazee e1ee85622d Nightly source-language update 2025-10-10 02:36:26 +00:00
easrng b1dc5179b6 reply button in feeds opens thread (#9143)
* the reply button should open the replies to a post, not present a text input for you to enter your Big Thought into. i think a lot of people don’t even realize that the point has already been written

* Feature gate, fix event

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-10-09 20:29:50 -05:00
Samuel Newman 96d77841e0 Fix language prompt text wrap issue (#9175) 2025-10-09 17:18:24 +03:00
Alex Benzer b38edc52b8 Auto-select search results tab (#9159)
When the user clicks the search button next to "Discover New Feeds" or "Suggested Accounts" on the Explore page, we'll auto-select the respective search results tab (feeds or users).
2025-10-09 05:02:38 -07:00
jim d68fc78e93 Merge pull request #9167 from internet-development/elijaharita/structured-data-name-fallback
fix: show both DisplayName and Handle in google structured data (or only Handle if DisplayName not set)
2025-10-08 14:38:07 -07:00
Elijah Seed-Arita 028ed2f8e2 fix: copy-paste mistake and more idiomatic structured data format 2025-10-08 10:45:30 -07:00
Elijah Seed-Arita 67049b98c9 fix: show both DisplayName and Handle in google structured data (or only Handle if DisplayName not set) 2025-10-08 10:29:44 -07:00
jim 07bcf7460a Merge pull request #9153 from bluesky-social/hailey/self-labels-check-embed
check for self labels when determining if we should hide embeds
2025-10-07 17:14:49 -07:00
Samuel Newman 54f70a576a [16KB] use 16kb-compatible fork of react-native-mmkv (#9150)
* use 16kb-compatible fork of react-native-mmkv

* avoid using package alias

* specify ndk version

* Revert "specify ndk version"

This reverts commit 577393518e.

* add compiler flags to mmkv build (bump version)

* move fork to bsky repo/npm org
2025-10-07 08:06:29 -07:00
Hailey 4a9185b0f5 nit 2025-10-06 20:41:04 -07:00
Hailey 1354bf987d check for self labels when determining if we should hide embeds 2025-10-06 20:39:43 -07:00
Elijah Seed-Arita 7f4e302b85 feat: hide embeds for moderated posts (#9152) 2025-10-06 19:34:26 -07:00
Samuel Newman 5e36992fba patch delete audio compression for 16kb compat (#9149) 2025-10-06 07:42:53 -07:00
Eduardo Rosendo 42a427cfd6 feat(link-meta): Tweaks getLinkMeta to whitelist trusted hostnames (#9139) 2025-10-06 16:36:33 +03:00
pfrazee 0c0c21a0aa Nightly source-language update 2025-10-04 02:30:44 +00:00
Samuel Newman f170313f70 patch sonner-native to remove exit animations (#9092) 2025-10-03 13:08:05 -07:00
Samuel Newman c8d0662690 remove gate (#9130) 2025-10-03 12:44:43 -07:00
Samuel Newman cf4cce2225 Embed testbed with tweaks (#9115)
* Nightly source-language update

* add: test embed page for bskyembeds

* run prettier

* wip

* feat: use local dev for bskyembed test.html

* feat: commit generated files which i'm not sure about

* chore: add basic readme to bskyembed

* Revert "feat: commit generated files which i'm not sure about"

This reverts commit b891efdaa1fd7ddd4417bfd5267e861a25a93b0e.

* chore: revert change to bskyembed/.eslintrc.cjs

* chore: remove generated file from bskyweb/static

* chore: revert messages.po changes

* add dev-snippet command, add dev router

* Update README.md

* Update bskyembed/test.html

* change IS_DEV check to allow localhost

---------

Co-authored-by: APiligrim <28320272+APiligrim@users.noreply.github.com>
Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local>
Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain>
Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
2025-10-03 12:15:26 -07:00
Samuel Newman 0e9e705d99 fix android onboarding dev crash (#9132) 2025-10-03 12:07:21 -07:00
Chenyu 12e09a9bd1 starter pack metrics (#9116)
* starter pack metrics

* emit starterpack:add/remove user event with starterpack uri in starter pack dialog

* rename metadata field
2025-10-03 10:17:59 -07:00
dan 885356256e [Fix Logouts] Persist accounts synchronously (#9109)
* Make persisting synchronous

* Initialize later so persisted is filled
2025-10-03 11:02:52 -05:00
pfrazee b68f80050c Nightly source-language update 2025-10-03 02:34:02 +00:00
dan 1354fd2661 [Fix Logouts] Remove buggy hackfix (#9108)
* Rip out the network hack in favor of bluesky-social/atproto#4238

* Bump api pkg

* Debug code

* Revert "Debug code"

This reverts commit 38445f31f4.

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-10-02 14:01:10 -05:00
Samuel Newman 77bf49f212 Tighten up eslint rule to catch unused const {_} = useLingui() (#9122)
* tighten eslint config to catch unused `useLingui`s

* fix now-invalid uses of _

* remove unused function that produces a ton of eslint warnings

* upgrade react compiler plugin
2025-10-02 09:59:05 -07:00
Eric Bailey 02b189a40e Language selection and suggestion UX improvements (#9067)
* feat: don't retain accepted language suggestion after finishing or exiting post (#8886)

* feat: don't retain accepted language suggestion after finishing or exiting post

* fix: rebase fixes

* fix: rebase fixes

* chore: lint

* Rename onChange for clarity

* Improve logic in composer

* Handle user override more explicitly

* Drill in onSelectLanguage callback into dialog too

* Fix typo

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Make text crystal clear

* Handle multiple languages

---------

Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2025-10-02 09:18:52 -05:00
Daniel Holmgren 5fd52b3d30 add patent pledge link to readme (#9118) 2025-10-01 11:49:06 -07:00
Samuel Newman 1e6a44f2e8 Fix feedfeedback metrics not distinguishing which feed it's from (#9099)
* fix feedfeedback metrics being sent for all feeds

* remove `discover:` metrics
2025-10-01 06:09:12 -07:00
pfrazee 144d61ef76 Nightly source-language update 2025-10-01 02:40:10 +00:00
Eric Bailey 381feacd26 Add StackedButton component (#9086) 2025-09-30 16:18:36 -05:00
dan 5df6036c2c [Web] Fix thread jumps (#9111)
* [Web] Fix thread jumps

* Comment formatting

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-09-30 10:46:31 -05:00
Eric Bailey 3e55e52858 Button tweaks (#9106)
* Tweak button colors

* Semi bold only for tiny buttons
2025-09-30 08:40:21 -05:00
Chenyu f82a6188e6 Update admonition component (#9068)
* update admonition component

* fix linting, adominition alighment

* design tweak

* edge cases for admonition, update storybook

* Update src/components/Admonition.tsx

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* fix mobile version

* change button style

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-09-30 05:27:19 -07:00
Samuel Newman c7ce827bfe fix gap on profile (#9081) 2025-09-30 07:28:50 +03:00
pfrazee 9e0f192546 Nightly source-language update 2025-09-30 02:34:39 +00:00
Samuel Newman d3dbb94689 Catch errors on geolocation request, reduce Sentry logs (#9098) 2025-09-29 23:12:50 +03:00
Samuel Newman 031fa95715 remove root sibling library (#9097) 2025-09-29 14:31:55 -05:00
Samuel Newman ae2c9a832f don't send some "activity no longer available" errors (#9100) 2025-09-29 14:15:38 -05:00
Samuel Newman 02a25d2aa2 Log OTA errors properly (#9101)
* Log OTA errors properly

* filter out network errors
2025-09-29 14:14:39 -05:00
Samuel Newman 2b32fff1d0 Fix link crash (#9102)
* fix link crash

* fix link crash
2025-09-29 14:14:13 -05:00
dan 22bb9c599f Send inferrable interactions to third-party feeds (#9094) 2025-09-29 14:07:21 -05:00
164 changed files with 4990 additions and 2680 deletions
+2 -1
View File
@@ -37,6 +37,7 @@ module.exports = {
'Toast.Action',
'AgeAssuranceAdmonition',
'Span',
'StackedButton',
],
impliedTextProps: [],
suggestedTextWrappers: {
@@ -88,7 +89,7 @@ module.exports = {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{argsIgnorePattern: '^_', varsIgnorePattern: '^_'},
{argsIgnorePattern: '^_', varsIgnorePattern: '^_.+'},
],
'@typescript-eslint/consistent-type-imports': [
'warn',
+2
View File
@@ -70,6 +70,8 @@ Bluesky is an open social network built on the AT Protocol, a flexible technolog
See [./LICENSE](./LICENSE) for the full license.
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).
## P.S.
We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place!
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 125 KiB

+26
View File
@@ -0,0 +1,26 @@
## Run / Test
Install dependencies:
```bash
cd bskyembed
yarn
```
Run the dev server:
```bash
yarn dev
```
You can see the embed homepage at http://localhost:5173
### Testbed
In another terminal window, run the snippet dev script:
```bash
yarn dev-snippet
```
You can then see the testbed page at http://localhost:5173/test
+2
View File
@@ -4,6 +4,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev-snippet": "tsc --project tsconfig.snippet.json && serve -s dist -p 3000 -n",
"build": "tsc && vite build",
"build-snippet": "tsc --project tsconfig.snippet.json",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
@@ -21,6 +22,7 @@
"eslint-config-preact": "^1.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"postcss": "^8.4.38",
"serve": "^14.2.5",
"tailwindcss": "^3.4.3",
"terser": "^5.43.1",
"typescript": "^5.8.3",
+11 -1
View File
@@ -3,9 +3,19 @@ interface Window {
bluesky: {
scan: (element?: Pick<Element, 'querySelectorAll'>) => void
}
BSKY_DEV_EMBED_URL?: string
}
const EMBED_URL = 'https://embed.bsky.app'
/**
* Allow url to be overwritten during development
*/
const IS_DEV =
window.location.protocol === 'file:' ||
window.location.hostname === 'localhost'
const EMBED_URL =
IS_DEV && window.BSKY_DEV_EMBED_URL
? window.BSKY_DEV_EMBED_URL
: 'https://embed.bsky.app'
window.bluesky = window.bluesky || {
scan,
+2 -2
View File
@@ -330,7 +330,7 @@ function ExternalEmbed({
{content.external.thumb && (
<img
src={content.external.thumb}
className="aspect-[1.91/1] object-cover"
className="aspect-[1200/630] object-cover"
/>
)}
<div className="py-3 px-4">
@@ -435,7 +435,7 @@ function StarterPackEmbed({
<Link
href={starterPackHref}
className="w-full rounded-xl overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
<img src={imageUri} className="aspect-[1200/630] object-cover" />
<div className="py-3 px-4">
<div className="flex space-x-2 items-center">
<img src={starterPackIcon} className="w-10 h-10" />
+1054
View File
File diff suppressed because it is too large Load Diff
+32 -1
View File
@@ -1,10 +1,40 @@
import fs from 'node:fs'
import {resolve} from 'node:path'
import preact from '@preact/preset-vite'
import legacy from '@vitejs/plugin-legacy'
import type {UserConfig} from 'vite'
import type {Plugin, UserConfig} from 'vite'
import paths from 'vite-tsconfig-paths'
/**
* World's hackiest router, for dev only. Serves `/post.html` to requests that start with `/embed/`
*/
function devOnlyRouter(): Plugin {
return {
name: 'embed-to-post-html',
configureServer(server) {
server.middlewares.use((req, res, next) => {
const url = req.url || ''
if (!url.startsWith('/embed/')) return next()
const html = fs.readFileSync(
resolve(process.cwd(), 'post.html'),
'utf8',
)
server
.transformIndexHtml(url, html)
.then(transformed => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
res.end(transformed)
})
.catch(next)
})
},
}
}
const config: UserConfig = {
plugins: [
preact(),
@@ -12,6 +42,7 @@ const config: UserConfig = {
legacy({
targets: ['defaults', 'not IE 11'],
}),
devOnlyRouter(),
],
build: {
assetsDir: 'static',
+376 -5
View File
@@ -1447,6 +1447,11 @@
regenerator-runtime "^0.14.1"
systemjs "^6.15.1"
"@zeit/schemas@2.36.0":
version "2.36.0"
resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.36.0.tgz#7a1b53f4091e18d0b404873ea3e3c83589c765f2"
integrity sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -1462,6 +1467,16 @@ acorn@^8.9.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
ajv@8.12.0:
version "8.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -1472,6 +1487,13 @@ ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-align@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
dependencies:
string-width "^4.1.0"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@@ -1507,7 +1529,12 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^5.0.2:
arch@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
arg@5.0.2, arg@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
@@ -1684,6 +1711,20 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
boxen@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.0.0.tgz#9e5f8c26e716793fc96edcf7cf754cdf5e3fbf32"
integrity sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==
dependencies:
ansi-align "^3.0.1"
camelcase "^7.0.0"
chalk "^5.0.1"
cli-boxes "^3.0.0"
string-width "^5.1.2"
type-fest "^2.13.0"
widest-line "^4.0.1"
wrap-ansi "^8.0.1"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -1738,6 +1779,16 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
@@ -1759,6 +1810,11 @@ camelcase-css@^2.0.1:
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
camelcase@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048"
integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==
caniuse-lite@^1.0.30001524:
version "1.0.30001606"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e"
@@ -1769,7 +1825,19 @@ caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.300017
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz#22e9706422ad37aa50556af8c10e40e2d93a8b85"
integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==
chalk@^4.0.0:
chalk-template@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-0.4.0.tgz#692c034d0ed62436b9062c1707fadcd0f753204b"
integrity sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==
dependencies:
chalk "^4.1.2"
chalk@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6"
integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
chalk@^4.0.0, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -1777,6 +1845,11 @@ chalk@^4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^5.0.1:
version "5.6.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea"
integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==
chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
@@ -1792,6 +1865,20 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
cli-boxes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145"
integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==
clipboardy@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092"
integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==
dependencies:
arch "^2.2.0"
execa "^5.1.1"
is-wsl "^2.2.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -1814,11 +1901,36 @@ commander@^4.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
compressible@~2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
dependencies:
mime-db ">= 1.43.0 < 2"
compression@1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79"
integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==
dependencies:
bytes "3.1.2"
compressible "~2.0.18"
debug "2.6.9"
negotiator "~0.6.4"
on-headers "~1.1.0"
safe-buffer "5.2.1"
vary "~1.1.2"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
@@ -1845,6 +1957,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
cross-spawn@^7.0.3:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
css-select@^5.1.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e"
@@ -1893,6 +2014,13 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0"
is-data-view "^1.0.1"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
@@ -1907,6 +2035,11 @@ debug@^4.3.2:
dependencies:
ms "2.1.2"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@@ -2356,6 +2489,21 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
execa@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^6.0.0"
human-signals "^2.1.0"
is-stream "^2.0.0"
merge-stream "^2.0.0"
npm-run-path "^4.0.1"
onetime "^5.1.2"
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -2496,6 +2644,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@
has-symbols "^1.0.3"
hasown "^2.0.0"
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
get-symbol-description@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
@@ -2631,6 +2784,11 @@ he@1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
ignore@^5.2.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
@@ -2662,6 +2820,11 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
internal-slot@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
@@ -2741,6 +2904,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
dependencies:
has-tostringtag "^1.0.0"
is-docker@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -2799,6 +2967,11 @@ is-path-inside@^3.0.3:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-port-reachable@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-port-reachable/-/is-port-reachable-4.0.0.tgz#dac044091ef15319c8ab2f34604d8794181f8c2d"
integrity sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==
is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
@@ -2819,6 +2992,11 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3:
dependencies:
call-bind "^1.0.7"
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
is-string@^1.0.5, is-string@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
@@ -2860,6 +3038,13 @@ is-weakset@^2.0.3:
call-bind "^1.0.7"
get-intrinsic "^1.2.4"
is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
dependencies:
is-docker "^2.0.0"
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
@@ -2932,6 +3117,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
@@ -3047,6 +3237,11 @@ meow@^13.0.0:
resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f"
integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -3060,7 +3255,29 @@ micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.2"
picomatch "^2.3.1"
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
"mime-db@>= 1.43.0 < 2":
version "1.54.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
mime-types@2.1.18:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
dependencies:
mime-db "~1.33.0"
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@3.1.2, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -3074,11 +3291,21 @@ minimatch@^9.0.1:
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4:
version "7.0.4"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -3118,6 +3345,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
negotiator@~0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
node-html-parser@^6.1.12:
version "6.1.13"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4"
@@ -3141,6 +3373,13 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
@@ -3215,6 +3454,11 @@ object.values@^1.1.6, object.values@^1.1.7:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
on-headers@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65"
integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -3222,6 +3466,13 @@ once@^1.3.0:
dependencies:
wrappy "1"
onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
dependencies:
mimic-fn "^2.1.0"
optionator@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
@@ -3265,7 +3516,12 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.1.0:
path-is-inside@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
@@ -3283,6 +3539,11 @@ path-scurry@^1.10.2:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-to-regexp@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -3409,6 +3670,21 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
range-parser@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==
rc@^1.0.1, rc@^1.1.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -3480,6 +3756,21 @@ regexpu-core@^6.2.0:
unicode-match-property-ecmascript "^2.0.0"
unicode-match-property-value-ecmascript "^2.1.0"
registry-auth-token@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20"
integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==
dependencies:
rc "^1.1.6"
safe-buffer "^5.0.1"
registry-url@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
integrity sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==
dependencies:
rc "^1.0.1"
regjsgen@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab"
@@ -3492,6 +3783,11 @@ regjsparser@^0.12.0:
dependencies:
jsesc "~3.0.2"
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -3582,6 +3878,11 @@ safe-array-concat@^1.1.2:
has-symbols "^1.0.3"
isarray "^2.0.5"
safe-buffer@5.2.1, safe-buffer@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-regex-test@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377"
@@ -3603,6 +3904,36 @@ semver@^7.3.7, semver@^7.5.4:
dependencies:
lru-cache "^6.0.0"
serve-handler@6.1.6:
version "6.1.6"
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1"
integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==
dependencies:
bytes "3.0.0"
content-disposition "0.5.2"
mime-types "2.1.18"
minimatch "3.1.2"
path-is-inside "1.0.2"
path-to-regexp "3.3.0"
range-parser "1.2.0"
serve@^14.2.5:
version "14.2.5"
resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.5.tgz#569e333b99a484b3a6d25acce4a569c8c4f96373"
integrity sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==
dependencies:
"@zeit/schemas" "2.36.0"
ajv "8.12.0"
arg "5.0.2"
boxen "7.0.0"
chalk "5.0.1"
chalk-template "0.4.0"
clipboardy "3.0.0"
compression "1.8.1"
is-port-reachable "4.0.0"
serve-handler "6.1.6"
update-check "1.5.4"
set-function-length@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
@@ -3647,6 +3978,11 @@ side-channel@^1.0.4, side-channel@^1.0.6:
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
signal-exit@^3.0.3:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
signal-exit@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
@@ -3791,11 +4127,21 @@ strip-ansi@^7.0.1:
dependencies:
ansi-regex "^6.0.1"
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
sucrase@^3.32.0:
version "3.35.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263"
@@ -3937,6 +4283,11 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^2.13.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
typed-array-buffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3"
@@ -4034,6 +4385,14 @@ update-browserslist-db@^1.0.13, update-browserslist-db@^1.1.3:
escalade "^3.2.0"
picocolors "^1.1.1"
update-check@1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.4.tgz#5b508e259558f1ad7dbc8b4b0457d4c9d28c8743"
integrity sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==
dependencies:
registry-auth-token "3.3.2"
registry-url "3.1.0"
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -4046,6 +4405,11 @@ util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
vite-prerender-plugin@^0.5.3:
version "0.5.11"
resolved "https://registry.yarnpkg.com/vite-prerender-plugin/-/vite-prerender-plugin-0.5.11.tgz#83e4f29e03269dceb763fb5ec2376dcc502aa79f"
@@ -4138,6 +4502,13 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
widest-line@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2"
integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==
dependencies:
string-width "^5.0.1"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
@@ -4147,7 +4518,7 @@ which@^2.0.1:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
+49 -13
View File
@@ -458,6 +458,18 @@ func (srv *Server) WebHome(c echo.Context) error {
return c.Render(http.StatusOK, "home.html", data)
}
// Posts that include these labels will not have embeds passed to the metadata
// template.
var hideEmbedLabels = map[string]bool{
"nudity": true,
"porn": true,
"sexual": true,
"sexual-figurative": true,
"graphic-media": true,
"self-harm": true,
"sensitive": true,
}
func (srv *Server) WebPost(c echo.Context) error {
ctx := c.Request().Context()
data := srv.NewTemplateContext()
@@ -512,19 +524,15 @@ func (srv *Server) WebPost(c echo.Context) error {
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
data["postView"] = postView
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
if postView.Embed != nil {
if postView.Embed.EmbedImages_View != nil {
var thumbUrls []string
for i := range postView.Embed.EmbedImages_View.Images {
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
}
data["imgThumbUrls"] = thumbUrls
} else if postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil {
var thumbUrls []string
for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images {
thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb)
}
data["imgThumbUrls"] = thumbUrls
// If any undesirable labels are set, the embed will not be included in
// metadata
isEmbedHidden := false
for _, label := range postView.Labels {
isNeg := label.Neg != nil && *label.Neg
if hideEmbedLabels[label.Val] && !isNeg {
isEmbedHidden = true
break
}
}
@@ -532,6 +540,34 @@ func (srv *Server) WebPost(c echo.Context) error {
postRecord, ok := postView.Record.Val.(*appbsky.FeedPost)
if ok {
data["postText"] = ExpandPostText(postRecord)
if !isEmbedHidden && postRecord.Labels != nil && postRecord.Labels.LabelDefs_SelfLabels != nil {
for _, label := range postRecord.Labels.LabelDefs_SelfLabels.Values {
if hideEmbedLabels[label.Val] {
isEmbedHidden = true
break
}
}
}
}
}
if postView.Embed != nil && !isEmbedHidden {
hasImages := postView.Embed.EmbedImages_View != nil
hasMedia := postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil
if hasImages {
var thumbUrls []string
for i := range postView.Embed.EmbedImages_View.Images {
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
}
data["imgThumbUrls"] = thumbUrls
} else if hasMedia {
var thumbUrls []string
for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images {
thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb)
}
data["imgThumbUrls"] = thumbUrls
}
}
+5
View File
@@ -62,7 +62,12 @@
"@type": "DiscussionForumPosting",
"author": {
"@type": "Person",
{%- if postView.Author.DisplayName %}
"name": "{{ postView.Author.DisplayName }}",
"alternateName": "@{{ postView.Author.Handle }}",
{% else %}
"name": "@{{ postView.Author.Handle }}",
{% endif -%}
"url": "https://bsky.app/profile/{{ postView.Author.Handle }}"
},
{%- if postText %}
+4
View File
@@ -59,8 +59,12 @@
"dateCreated": "{{ profileView.CreatedAt }}",
"mainEntity": {
"@type": "Person",
{%- if profileView.DisplayName %}
"name": "{{ profileView.DisplayName }}",
"alternateName": "@{{ profileView.Handle }}",
{% else %}
"name": "@{{ profileView.Handle }}",
{% endif -%}
"identifier": "{{ profileView.Did }}",
"description": "{{ profileView.Description }}",
"image": "{{ profileView.Avatar }}",
+8 -12
View File
@@ -1,22 +1,18 @@
import React from 'react'
import {render} from '@testing-library/react-native'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {RootStoreProvider, RootStoreModel} from '../src/state'
import {render} from '@testing-library/react-native'
import {ThemeProvider} from '../src/lib/ThemeContext'
import {type RootStoreModel, RootStoreProvider} from '../src/state'
const customRender = (ui: any, rootStore: RootStoreModel) =>
render(
// eslint-disable-next-line react-native/no-inline-styles
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider value={rootStore}>
<ThemeProvider theme="light">
<SafeAreaProvider>{ui}</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</RootSiblingParent>
<RootStoreProvider value={rootStore}>
<ThemeProvider theme="light">
<SafeAreaProvider>{ui}</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</GestureHandlerRootView>,
)
+5 -5
View File
@@ -74,7 +74,8 @@
"@atproto/api": "^0.17.0",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.2",
"@bsky.app/alf": "^0.1.5",
"@bsky.app/react-native-mmkv": "2.12.5",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.12.5",
@@ -145,6 +146,7 @@
"expo-image-manipulator": "~14.0.7",
"expo-image-picker": "~17.0.8",
"expo-intent-launcher": "~13.0.7",
"expo-keep-awake": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.8",
"expo-localization": "~17.0.7",
@@ -192,12 +194,10 @@
"react-native-gesture-handler": "~2.28.0",
"react-native-get-random-values": "~1.11.0",
"react-native-keyboard-controller": "1.18.5",
"react-native-mmkv": "^2.12.2",
"react-native-pager-view": "6.8.0",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3",
"react-native-reanimated": "^3.19.1",
"react-native-root-siblings": "^5.0.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
@@ -247,7 +247,7 @@
"babel-jest": "^29.7.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "^19.1.0-rc.1",
"babel-plugin-react-compiler": "^19.1.0-rc.3",
"babel-preset-expo": "~54.0.0",
"eslint": "^8.19.0",
"eslint-plugin-bsky-internal": "link:./eslint",
@@ -255,7 +255,7 @@
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"file-loader": "6.2.0",
@@ -0,0 +1,516 @@
diff --git a/node_modules/react-native-compressor/android/build.gradle b/node_modules/react-native-compressor/android/build.gradle
index 5071139..84bee34 100644
--- a/node_modules/react-native-compressor/android/build.gradle
+++ b/node_modules/react-native-compressor/android/build.gradle
@@ -115,7 +115,6 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation 'org.mp4parser:isoparser:1.9.56'
- implementation 'com.github.banketree:AndroidLame-kotlin:v0.0.1'
implementation 'javazoom:jlayer:1.0.1'
}
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt
deleted file mode 100644
index 9292d3e..0000000
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt
+++ /dev/null
@@ -1,264 +0,0 @@
-package com.reactnativecompressor.Audio
-
-
-import android.annotation.SuppressLint
-import com.facebook.react.bridge.Promise
-import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.bridge.ReadableMap
-import com.naman14.androidlame.LameBuilder
-import com.naman14.androidlame.WaveReader
-import com.reactnativecompressor.Utils.MediaCache
-import com.reactnativecompressor.Utils.Utils
-import com.reactnativecompressor.Utils.Utils.addLog
-import javazoom.jl.converter.Converter
-import javazoom.jl.decoder.JavaLayerException
-import java.io.BufferedOutputStream
-import java.io.File
-import java.io.FileNotFoundException
-import java.io.FileOutputStream
-import java.io.IOException
-
-class AudioCompressor {
- companion object {
- val TAG="AudioMain"
- private const val OUTPUT_STREAM_BUFFER = 8192
-
- var outputStream: BufferedOutputStream? = null
- var waveReader: WaveReader? = null
- @JvmStatic
- fun CompressAudio(
- fileUrl: String,
- optionMap: ReadableMap,
- context: ReactApplicationContext,
- promise: Promise,
- ) {
- val realPath = Utils.getRealPath(fileUrl, context)
- var _fileUrl=realPath
- val filePathWithoutFileUri = realPath!!.replace("file://", "")
- try {
- var wavPath=filePathWithoutFileUri;
- var isNonWav:Boolean=false
- if (fileUrl.endsWith(".mp4", ignoreCase = true))
- {
- addLog("mp4 file found")
- val mp3Path= Utils.generateCacheFilePath("mp3", context)
- AudioExtractor().genVideoUsingMuxer(fileUrl, mp3Path, -1, -1, true, false)
- _fileUrl=Utils.slashifyFilePath(mp3Path)
- wavPath= Utils.generateCacheFilePath("wav", context)
- try {
- val converter = Converter()
- converter.convert(mp3Path, wavPath)
- } catch (e: JavaLayerException) {
- addLog("JavaLayerException error"+e.localizedMessage)
- e.printStackTrace();
- }
- isNonWav=true
- }
- else if (!fileUrl.endsWith(".wav", ignoreCase = true))
- {
- addLog("non wav file found")
- wavPath= Utils.generateCacheFilePath("wav", context)
- try {
- val converter = Converter()
- converter.convert(filePathWithoutFileUri, wavPath)
- } catch (e: JavaLayerException) {
- addLog("JavaLayerException error"+e.localizedMessage)
- e.printStackTrace();
- }
- isNonWav=true
- }
-
-
- autoCompressHelper(wavPath,filePathWithoutFileUri, optionMap,context) { mp3Path, finished ->
- if (finished) {
- val returnableFilePath:String="file://$mp3Path"
- addLog("finished: " + returnableFilePath)
- MediaCache.removeCompletedImagePath(fileUrl)
- if(isNonWav)
- {
- File(wavPath).delete()
- }
- promise.resolve(returnableFilePath)
- } else {
- addLog("error: "+mp3Path)
- promise.resolve(_fileUrl)
- }
- }
- } catch (e: Exception) {
- promise.resolve(_fileUrl)
- }
- }
-
- @SuppressLint("WrongConstant")
- private fun autoCompressHelper(
- fileUrl: String,
- actualFileUrl: String,
- optionMap: ReadableMap,
- context: ReactApplicationContext,
- completeCallback: (String, Boolean) -> Unit
- ) {
-
- val options = AudioHelper.fromMap(optionMap)
- val quality = options.quality
-
- var isCompletedCallbackTriggered:Boolean=false
- try {
- var mp3Path = Utils.generateCacheFilePath("mp3", context)
- val input = File(fileUrl)
- val output = File(mp3Path)
-
- val CHUNK_SIZE = 8192
- addLog("Initialising wav reader")
-
- waveReader = WaveReader(input)
-
- try {
- waveReader!!.openWave()
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- addLog("Intitialising encoder")
-
-
- // for bitrate
- var audioBitrate:Int
- if(options.bitrate != -1)
- {
- audioBitrate= options.bitrate/1000
- }
- else
- {
- audioBitrate=AudioHelper.getDestinationBitrateByQuality(actualFileUrl, quality!!)
- Utils.addLog("dest bitrate: $audioBitrate")
- }
-
- var androidLame = LameBuilder();
- androidLame.setOutBitrate(audioBitrate)
-
- // for channels
- var audioChannels:Int
- if(options.channels != -1){
- audioChannels= options.channels!!
- }
- else
- {
- audioChannels=waveReader!!.channels
- }
- androidLame.setOutChannels(audioChannels)
-
- // for sample rate
- androidLame.setInSampleRate(waveReader!!.sampleRate)
- var audioSampleRate:Int
- if(options.samplerate != -1){
- audioSampleRate= options.samplerate!!
- }
- else
- {
- audioSampleRate=waveReader!!.sampleRate
- }
- androidLame.setOutSampleRate(audioSampleRate)
- val androidLameBuild=androidLame.build()
-
- try {
- outputStream = BufferedOutputStream(FileOutputStream(output), OUTPUT_STREAM_BUFFER)
- } catch (e: FileNotFoundException) {
- e.printStackTrace()
- }
-
- var bytesRead = 0
-
- val buffer_l = ShortArray(CHUNK_SIZE)
- val buffer_r = ShortArray(CHUNK_SIZE)
- val mp3Buf = ByteArray(CHUNK_SIZE)
-
- val channels = waveReader!!.channels
-
- addLog("started encoding")
- while (true) {
- try {
- if (channels == 2) {
-
- bytesRead = waveReader!!.read(buffer_l, buffer_r, CHUNK_SIZE)
- addLog("bytes read=$bytesRead")
-
- if (bytesRead > 0) {
-
- var bytesEncoded = 0
- bytesEncoded = androidLameBuild.encode(buffer_l, buffer_r, bytesRead, mp3Buf)
- addLog("bytes encoded=$bytesEncoded")
-
- if (bytesEncoded > 0) {
- try {
- addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
- outputStream!!.write(mp3Buf, 0, bytesEncoded)
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- }
-
- } else
- break
- } else {
-
- bytesRead = waveReader!!.read(buffer_l, CHUNK_SIZE)
- addLog("bytes read=$bytesRead")
-
- if (bytesRead > 0) {
- var bytesEncoded = 0
-
- bytesEncoded = androidLameBuild.encode(buffer_l, buffer_l, bytesRead, mp3Buf)
- addLog("bytes encoded=$bytesEncoded")
-
- if (bytesEncoded > 0) {
- try {
- addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
- outputStream!!.write(mp3Buf, 0, bytesEncoded)
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- }
-
- } else
- break
- }
-
-
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- }
-
- addLog("flushing final mp3buffer")
- val outputMp3buf = androidLameBuild.flush(mp3Buf)
- addLog("flushed $outputMp3buf bytes")
- if (outputMp3buf > 0) {
- try {
- addLog("writing final mp3buffer to outputstream")
- outputStream!!.write(mp3Buf, 0, outputMp3buf)
- addLog("closing output stream")
- outputStream!!.close()
- completeCallback(output.absolutePath, true)
- isCompletedCallbackTriggered=true
- } catch (e: IOException) {
- completeCallback(e.localizedMessage, false)
- e.printStackTrace()
- }
- }
-
- } catch (e: IOException) {
- completeCallback(e.localizedMessage, false)
- }
- if(!isCompletedCallbackTriggered)
- {
- completeCallback("something went wrong", false)
- }
- }
-
-
-
- }
-}
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt
deleted file mode 100644
index c655182..0000000
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.reactnativecompressor.Audio
-
-import android.annotation.SuppressLint
-import android.media.MediaCodec
-import android.media.MediaExtractor
-import android.media.MediaFormat
-import android.media.MediaMetadataRetriever
-import android.media.MediaMuxer
-import android.util.Log
-import java.io.IOException
-import java.nio.ByteBuffer
-
-
-class AudioExtractor {
- /**
- * @param srcPath the path of source video file.
- * @param dstPath the path of destination video file.
- * @param startMs starting time in milliseconds for trimming. Set to
- * negative if starting from beginning.
- * @param endMs end time for trimming in milliseconds. Set to negative if
- * no trimming at the end.
- * @param useAudio true if keep the audio track from the source.
- * @param useVideo true if keep the video track from the source.
- * @throws IOException
- */
- @SuppressLint("NewApi", "WrongConstant")
- @Throws(IOException::class)
- fun genVideoUsingMuxer(srcPath: String?, dstPath: String?, startMs: Int, endMs: Int, useAudio: Boolean, useVideo: Boolean) {
- // Set up MediaExtractor to read from the source.
- val extractor = MediaExtractor()
- extractor.setDataSource(srcPath!!)
- val trackCount = extractor.trackCount
- // Set up MediaMuxer for the destination.
- val muxer: MediaMuxer
- muxer = MediaMuxer(dstPath!!, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
- // Set up the tracks and retrieve the max buffer size for selected
- // tracks.
- val indexMap = HashMap<Int, Int>(trackCount)
- var bufferSize = -1
- for (i in 0 until trackCount) {
- val format = extractor.getTrackFormat(i)
- val mime = format.getString(MediaFormat.KEY_MIME)
- var selectCurrentTrack = false
- if (mime!!.startsWith("audio/") && useAudio) {
- selectCurrentTrack = true
- } else if (mime.startsWith("video/") && useVideo) {
- selectCurrentTrack = true
- }
- if (selectCurrentTrack) {
- extractor.selectTrack(i)
- val dstIndex = muxer.addTrack(format)
- indexMap[i] = dstIndex
- if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
- val newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
- bufferSize = if (newSize > bufferSize) newSize else bufferSize
- }
- }
- }
- if (bufferSize < 0) {
- bufferSize = DEFAULT_BUFFER_SIZE
- }
- // Set up the orientation and starting time for extractor.
- val retrieverSrc = MediaMetadataRetriever()
- retrieverSrc.setDataSource(srcPath)
- val degreesString = retrieverSrc.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
- if (degreesString != null) {
- val degrees = degreesString.toInt()
- if (degrees >= 0) {
- muxer.setOrientationHint(degrees)
- }
- }
- if (startMs > 0) {
- extractor.seekTo((startMs * 1000).toLong(), MediaExtractor.SEEK_TO_CLOSEST_SYNC)
- }
- // Copy the samples from MediaExtractor to MediaMuxer. We will loop
- // for copying each sample and stop when we get to the end of the source
- // file or exceed the end time of the trimming.
- val offset = 0
- var trackIndex = -1
- val dstBuf = ByteBuffer.allocate(bufferSize)
- val bufferInfo = MediaCodec.BufferInfo()
- muxer.start()
- while (true) {
- bufferInfo.offset = offset
- bufferInfo.size = extractor.readSampleData(dstBuf, offset)
- if (bufferInfo.size < 0) {
- Log.d(TAG, "Saw input EOS.")
- bufferInfo.size = 0
- break
- } else {
- bufferInfo.presentationTimeUs = extractor.sampleTime
- if (endMs > 0 && bufferInfo.presentationTimeUs > endMs * 1000) {
- Log.d(TAG, "The current sample is over the trim end time.")
- break
- } else {
- bufferInfo.flags = extractor.sampleFlags
- trackIndex = extractor.sampleTrackIndex
- muxer.writeSampleData(indexMap[trackIndex]!!, dstBuf, bufferInfo)
- extractor.advance()
- }
- }
- }
- muxer.stop()
- muxer.release()
- return
- }
-
- companion object {
- private const val DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024
- private const val TAG = "AudioExtractorDecoder"
- }
-}
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
deleted file mode 100644
index 42040b4..0000000
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.reactnativecompressor.Audio
-
-import android.media.MediaExtractor
-import android.media.MediaFormat
-import com.facebook.react.bridge.ReadableMap
-import com.reactnativecompressor.Utils.Utils
-import java.io.File
-import java.io.IOException
-
-
-class AudioHelper {
-
- var quality: String? = "medium"
- var bitrate: Int = -1
- var samplerate: Int = -1
- var channels: Int = -1
- var progressDivider: Int? = 0
-
- companion object {
- fun fromMap(map: ReadableMap): AudioHelper {
- val options = AudioHelper()
- val iterator = map.keySetIterator()
- while (iterator.hasNextKey()) {
- val key = iterator.nextKey()
- when (key) {
- "quality" -> options.quality = map.getString(key)
- "bitrate" -> {
- val bitrate = map.getInt(key)
- options.bitrate = if (bitrate > 320000 || bitrate < 64000) 64000 else bitrate
- }
- "samplerate" -> options.samplerate = map.getInt(key)
- "channels" -> options.channels = map.getInt(key)
- }
- }
- return options
- }
-
-
- fun getAudioBitrate(path: String): Int {
- val file = File(path)
- val fileSize = file.length() * 8 // size in bits
-
- val mex = MediaExtractor()
- try {
- mex.setDataSource(path)
- } catch (e: IOException) {
- e.printStackTrace()
- }
-
- val mf = mex.getTrackFormat(0)
- val durationUs = mf.getLong(MediaFormat.KEY_DURATION)
- val durationSec = durationUs / 1_000_000.0 // convert duration to seconds
-
- return (fileSize / durationSec).toInt()/1000 // bitrate in bits per second
- }
- fun getDestinationBitrateByQuality(path: String, quality: String): Int {
- val originalBitrate = getAudioBitrate(path)
- var destinationBitrate = originalBitrate
- Utils.addLog("source bitrate: $originalBitrate")
-
- when (quality.lowercase()) {
- "low" -> destinationBitrate = maxOf(64, (originalBitrate * 0.3).toInt())
- "medium" -> destinationBitrate = (originalBitrate * 0.5).toInt()
- "high" -> destinationBitrate = minOf(320, (originalBitrate * 0.7).toInt())
- else -> Utils.addLog("Invalid quality level. Please enter 'low', 'medium', or 'high'.")
- }
-
- return destinationBitrate
- }
-
- }
-}
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
index 446d4fb..f021909 100644
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
+++ b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
@@ -11,7 +11,9 @@ class AudioMain(private val reactContext: ReactApplicationContext) {
promise: Promise) {
try {
- AudioCompressor.CompressAudio(fileUrl,optionMap,reactContext,promise)
+ // Skip compression on Android to avoid libandroidlame dependency
+ // Return the original file URL without compression
+ promise.resolve(fileUrl)
} catch (ex: Exception) {
promise.reject(ex)
}
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
index c14b727..1198908 100644
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
+++ b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
@@ -7,7 +7,6 @@ import android.provider.OpenableColumns
import android.util.Log
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
-import com.reactnativecompressor.Audio.AudioCompressor
import com.reactnativecompressor.Video.VideoCompressor.CompressionListener
import com.reactnativecompressor.Video.VideoCompressor.VideoCompressorClass
import java.io.FileNotFoundException
@@ -152,10 +151,6 @@ object Utils {
}
}
- fun addLog(log: String) {
- Log.d(AudioCompressor.TAG, log)
- }
-
val exifAttributes = arrayOf(
"FNumber",
"ApertureValue",
@@ -0,0 +1,5 @@
# react-native-compressor
Patch file taken from https://github.com/numandev1/react-native-compressor/pull/355#issuecomment-3180870738
This patch removes the audio compression feature on Android from the library. This is because `libandroidlame.so`, the native dependency, does not support 16kb page sizes, and the Play Store has made this mandatory as of 1st Nov 2025.
+22
View File
@@ -0,0 +1,22 @@
diff --git a/node_modules/sonner-native/lib/commonjs/toast.js b/node_modules/sonner-native/lib/commonjs/toast.js
index 121816a..0c3c7bd 100644
--- a/node_modules/sonner-native/lib/commonjs/toast.js
+++ b/node_modules/sonner-native/lib/commonjs/toast.js
@@ -264,7 +264,7 @@ const Toast = exports.Toast = /*#__PURE__*/React.forwardRef(({
...toastSwipeHandlerProps,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
entering: entering,
- exiting: exiting,
+ exiting: _reactNative.Platform.OS === 'android' ? undefined : exiting,
children: jsx
})
});
@@ -274,7 +274,7 @@ const Toast = exports.Toast = /*#__PURE__*/React.forwardRef(({
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
style: [unstyled ? undefined : elevationStyle, defaultStyles.toast, toastStyleCtx, styles?.toast, style, wiggleAnimationStyle],
entering: entering,
- exiting: exiting,
+ exiting: _reactNative.Platform.OS === 'android' ? undefined : exiting,
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
style: [defaultStyles.toastContent, toastContentStyleCtx, styles?.toastContent],
children: [promiseOptions || variant === 'loading' ? 'loading' in icons ? icons.loading : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {}) : icon ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
+3
View File
@@ -0,0 +1,3 @@
# sonner-native+0.21.0.patch
Removes Reanimated exit layout animations from the toasts. This was causing crashes if the toast was hidden while you were scrolling a flatlist.
+61 -60
View File
@@ -4,7 +4,6 @@ import '#/view/icons'
import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
import {
initialWindowMetrics,
SafeAreaProvider,
@@ -84,7 +83,11 @@ if (isIOS) {
}
if (isAndroid) {
// iOS is handled by the config plugin -sfn
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP)
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP,
).catch(error =>
logger.debug('Could not lock orientation', {safeMessage: error}),
)
}
/**
@@ -133,64 +136,62 @@ function InnerApp() {
<ThemeProvider theme={theme}>
<ContextMenuProvider>
<Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent>
<VideoVolumeProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
</RootSiblingParent>
<VideoVolumeProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<ServiceAccountManager>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<GestureHandlerRootView
style={s.h100pct}>
<GlobalGestureEventsProvider>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</GlobalGestureEventsProvider>
</GestureHandlerRootView>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceAccountManager>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
</Splash>
</ContextMenuProvider>
</ThemeProvider>
+54 -57
View File
@@ -3,7 +3,6 @@ import '#/view/icons'
import './style.css'
import React, {useEffect, useState} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -111,62 +110,60 @@ function InnerApp() {
<Alf theme={theme}>
<ThemeProvider theme={theme}>
<ContextMenuProvider>
<RootSiblingParent>
<VideoVolumeProvider>
<ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</ActiveVideoProvider>
</VideoVolumeProvider>
</RootSiblingParent>
<VideoVolumeProvider>
<ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<PolicyUpdateOverlayProvider>
<StatsigProvider>
<AgeAssuranceProvider>
<ComposerProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<HomeBadgeProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<ServiceConfigProvider>
<EmailVerificationProvider>
<HideBottomBarBorderProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
<ToastOutlet />
</IntentDialogProvider>
</HideBottomBarBorderProvider>
</EmailVerificationProvider>
</ServiceConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HomeBadgeProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</ComposerProvider>
</AgeAssuranceProvider>
</StatsigProvider>
</PolicyUpdateOverlayProvider>
</QueryProvider>
</React.Fragment>
</ActiveVideoProvider>
</VideoVolumeProvider>
</ContextMenuProvider>
</ThemeProvider>
</Alf>
+12 -1
View File
@@ -1,6 +1,7 @@
import {type StyleProp, type ViewStyle} from 'react-native'
import {atoms as baseAtoms} from '@bsky.app/alf'
import {CARD_ASPECT_RATIO} from '#/lib/constants'
import {native, platform, web} from '#/alf/util/platform'
import * as Layout from '#/components/Layout'
@@ -31,6 +32,16 @@ export const atoms = {
backgroundColor: 'transparent',
},
/**
* Aspect ratios
*/
aspect_square: {
aspectRatio: 1,
},
aspect_card: {
aspectRatio: CARD_ASPECT_RATIO,
},
/*
* Transition
*/
@@ -67,7 +78,7 @@ export const atoms = {
}),
/*
* Animaations
* Animations
*/
fade_in: web({
animation: 'fadeIn ease-out 0.15s',
+10 -4
View File
@@ -1,14 +1,20 @@
import * as SystemUI from 'expo-system-ui'
import {type Theme} from '@bsky.app/alf'
import {logger} from '#/logger'
import {isAndroid} from '#/platform/detection'
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
if (isAndroid) {
if (themeType === 'theme') {
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
} else {
SystemUI.setBackgroundColorAsync('black')
try {
if (themeType === 'theme') {
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
} else {
SystemUI.setBackgroundColorAsync('black')
}
} catch (error) {
// Can reject with 'The current activity is no longer available' - no big deal
logger.debug('Could not set system UI theme', {safeMessage: error})
}
}
}
+1 -1
View File
@@ -172,7 +172,7 @@ function AccountItem({
</View>
{isCurrentAccount ? (
<Check size="sm" style={[{color: t.palette.positive_600}]} />
<Check size="sm" style={[{color: t.palette.positive_500}]} />
) : (
<Chevron size="sm" style={[t.atoms.text]} />
)}
+44 -24
View File
@@ -3,17 +3,13 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button as BaseButton, type ButtonProps} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Text as BaseText, type TextProps} from '#/components/Typography'
export const colors = {
warning: {
light: '#DFBC00',
dark: '#BFAF1F',
},
warning: '#FFC404',
}
type Context = {
@@ -29,29 +25,44 @@ export function Icon() {
const t = useTheme()
const {type} = useContext(Context)
const Icon = {
info: InfoIcon,
tip: TipIcon,
info: CircleInfoIcon,
tip: CircleInfoIcon,
warning: WarningIcon,
error: ErrorIcon,
error: CircleXIcon,
}[type]
const fill = {
info: t.atoms.text_contrast_medium.color,
tip: t.palette.primary_500,
warning: colors.warning.light,
warning: colors.warning,
error: t.palette.negative_500,
}[type]
return <Icon fill={fill} size="md" />
}
export function Content({
children,
style,
...rest
}: {
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
return (
<View
style={[a.gap_sm, a.flex_1, {minHeight: 20}, a.justify_center, style]}
{...rest}>
{children}
</View>
)
}
export function Text({
children,
style,
...rest
}: Pick<TextProps, 'children' | 'style'>) {
return (
<BaseText
{...rest}
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
<BaseText {...rest} style={[a.text_sm, a.leading_snug, a.pr_md, style]}>
{children}
</BaseText>
)
@@ -60,17 +71,23 @@ export function Text({
export function Button({
children,
...props
}: Omit<ButtonProps, 'size' | 'variant' | 'color'>) {
}: Omit<ButtonProps, 'size' | 'variant'>) {
return (
<BaseButton size="tiny" variant="outline" color="secondary" {...props}>
<BaseButton size="tiny" {...props}>
{children}
</BaseButton>
)
}
export function Row({children}: {children: React.ReactNode}) {
export function Row({
children,
style,
}: {
children: React.ReactNode
style?: StyleProp<ViewStyle>
}) {
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
<View style={[a.w_full, a.flex_row, a.align_start, a.gap_sm, style]}>
{children}
</View>
)
@@ -88,19 +105,20 @@ export function Outer({
const t = useTheme()
const {gtMobile} = useBreakpoints()
const borderColor = {
info: t.atoms.border_contrast_low.borderColor,
tip: t.atoms.border_contrast_low.borderColor,
warning: t.atoms.border_contrast_low.borderColor,
error: t.atoms.border_contrast_low.borderColor,
info: t.atoms.border_contrast_high.borderColor,
tip: t.palette.primary_500,
warning: colors.warning,
error: t.palette.negative_500,
}[type]
return (
<Context.Provider value={{type}}>
<View
style={[
gtMobile ? a.p_md : a.p_sm,
a.p_md,
a.rounded_sm,
a.border,
t.atoms.bg_contrast_25,
t.atoms.bg,
{borderColor},
style,
]}>
@@ -123,7 +141,9 @@ export function Admonition({
<Outer type={type} style={style}>
<Row>
<Icon />
<Text>{children}</Text>
<Content>
<Text>{children}</Text>
</Content>
</Row>
</Outer>
)
+53 -41
View File
@@ -274,18 +274,10 @@ export const Button = React.forwardRef<View, ButtonProps>(
} else if (color === 'primary_subtle') {
if (!disabled) {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.primary_50,
dim: t.palette.primary_100,
dark: t.palette.primary_100,
}),
backgroundColor: t.palette.primary_50,
})
hoverStyles.push({
backgroundColor: select(t.name, {
light: t.palette.primary_100,
dim: t.palette.primary_200,
dark: t.palette.primary_200,
}),
backgroundColor: t.palette.primary_100,
})
} else {
baseStyles.push({
@@ -295,18 +287,10 @@ export const Button = React.forwardRef<View, ButtonProps>(
} else if (color === 'negative_subtle') {
if (!disabled) {
baseStyles.push({
backgroundColor: select(t.name, {
light: t.palette.negative_50,
dim: t.palette.negative_100,
dark: t.palette.negative_100,
}),
backgroundColor: t.palette.negative_50,
})
hoverStyles.push({
backgroundColor: select(t.name, {
light: t.palette.negative_100,
dim: t.palette.negative_200,
dark: t.palette.negative_200,
}),
backgroundColor: t.palette.negative_100,
})
} else {
baseStyles.push({
@@ -618,37 +602,21 @@ export function useSharedButtonTextStyles() {
} else if (color === 'primary_subtle') {
if (!disabled) {
baseStyles.push({
color: select(t.name, {
light: t.palette.primary_600,
dim: t.palette.primary_800,
dark: t.palette.primary_800,
}),
color: t.palette.primary_600,
})
} else {
baseStyles.push({
color: select(t.name, {
light: t.palette.primary_200,
dim: t.palette.primary_200,
dark: t.palette.primary_200,
}),
color: t.palette.primary_200,
})
}
} else if (color === 'negative_subtle') {
if (!disabled) {
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_600,
dim: t.palette.negative_800,
dark: t.palette.negative_800,
}),
color: t.palette.negative_600,
})
} else {
baseStyles.push({
color: select(t.name, {
light: t.palette.negative_200,
dim: t.palette.negative_200,
dark: t.palette.negative_200,
}),
color: t.palette.negative_200,
})
}
}
@@ -755,7 +723,7 @@ export function useSharedButtonTextStyles() {
} else if (size === 'small') {
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold)
}
return StyleSheet.flatten(baseStyles)
@@ -869,3 +837,47 @@ export function ButtonIcon({
</View>
)
}
export type StackedButtonProps = Omit<
ButtonProps,
keyof VariantProps | 'children'
> &
Pick<VariantProps, 'color'> & {
children: React.ReactNode
icon: React.ComponentType<SVGIconProps>
}
export function StackedButton({children, ...props}: StackedButtonProps) {
return (
<Button
{...props}
size="tiny"
style={[
a.flex_col,
{
height: 72,
paddingHorizontal: 16,
borderRadius: 20,
gap: 4,
},
props.style,
]}>
<StackedButtonInnerText icon={props.icon}>
{children}
</StackedButtonInnerText>
</Button>
)
}
function StackedButtonInnerText({
children,
icon: Icon,
}: Pick<StackedButtonProps, 'icon' | 'children'>) {
const textStyles = useSharedButtonTextStyles()
return (
<>
<Icon width={24} fill={textStyles.color} />
<ButtonText>{children}</ButtonText>
</>
)
}
+1 -1
View File
@@ -62,7 +62,7 @@ export function HeaderText({
style?: StyleProp<TextStyle>
}) {
return (
<Text style={[a.text_lg, a.text_center, a.font_bold, style]}>
<Text style={[a.text_lg, a.text_center, a.font_semi_bold, style]}>
{children}
</Text>
)
+2 -2
View File
@@ -258,7 +258,7 @@ export function InterestTabs({
t.atoms.border_contrast_low,
t.atoms.bg,
a.h_full,
{aspectRatio: 1},
a.aspect_square,
a.rounded_full,
]}>
<ButtonIcon icon={ArrowLeft} />
@@ -292,7 +292,7 @@ export function InterestTabs({
t.atoms.border_contrast_low,
t.atoms.bg,
a.h_full,
{aspectRatio: 1},
a.aspect_square,
a.rounded_full,
]}>
<ButtonIcon icon={ArrowRight} />
+28
View File
@@ -0,0 +1,28 @@
import {useId} from 'react'
import {useKeepAwake} from 'expo-keep-awake'
import {useIsFocused} from '@react-navigation/native'
/**
* Stops the screen from sleeping. Only applies to the current screen.
*
* Note: Expo keeps the screen permanently awake when in dev mode, so
* you'll only see this do anything when in production.
*
* @platform ios, android
*/
export function KeepAwake({enabled = true}) {
const isFocused = useIsFocused()
if (enabled && isFocused) {
return <KeepAwakeInner />
} else {
return null
}
}
function KeepAwakeInner() {
const id = useId()
// if you don't pass an explicit ID, any `useKeepAwake` hook unmounting disables them all.
// very strange behaviour, but easily fixed by passing a unique ID -sfn
useKeepAwake(id)
return null
}
+3
View File
@@ -0,0 +1,3 @@
export function KeepAwake() {
return null
}
+2 -2
View File
@@ -165,8 +165,8 @@ export function useLink({
if (isNative && screen !== 'NotFound') {
const state = navigation.getState()
// if screen is not in the current navigator, it means it's
// most likely a tab screen
if (!state.routeNames.includes(screen)) {
// most likely a tab screen. note: state can be undefined
if (!state?.routeNames.includes(screen)) {
const parent = navigation.getParent()
if (
parent &&
-82
View File
@@ -1,82 +0,0 @@
import {View, type ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {type Gate} from '#/lib/statsig/gates'
import {useGate} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
interface LoggedOutCTAProps {
style?: ViewStyle
gateName: Gate
}
export function LoggedOutCTA({style, gateName}: LoggedOutCTAProps) {
const {hasSession} = useSession()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const gate = useGate()
const t = useTheme()
const {_} = useLingui()
// Only show for logged-out users on web
if (hasSession || !isWeb) {
return null
}
// Check gate at the last possible moment to avoid counting users as exposed when they won't see the element
if (!gate(gateName)) {
return null
}
return (
<View style={[a.pb_md, style]}>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.px_lg,
a.py_md,
a.rounded_md,
a.mb_xs,
t.atoms.bg_contrast_25,
]}>
<View style={[a.flex_row, a.align_center, a.flex_1, a.pr_md]}>
<Logo width={30} style={[a.mr_md]} />
<View style={[a.flex_1]}>
<Text style={[a.text_lg, a.font_semi_bold, a.leading_snug]}>
<Trans>Join Bluesky</Trans>
</Text>
<Text
style={[
a.text_md,
a.font_medium,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
<Trans>The open social network.</Trans>
</Text>
</View>
</View>
<Button
onPress={() => {
requestSwitchToAccount({requestedAccount: 'new'})
}}
label={_(msg`Create account`)}
size="small"
variant="solid"
color="primary">
<ButtonText>
<Trans>Create account</Trans>
</ButtonText>
</Button>
</View>
</View>
)
}
+3 -2
View File
@@ -87,7 +87,7 @@ export function ImageItem({
}) {
const t = useTheme()
return (
<View style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}>
<View style={[a.relative, a.flex_1, a.aspect_square, {maxWidth: 100}]}>
<Image
key={thumbnail}
source={{uri: thumbnail}}
@@ -131,7 +131,8 @@ export function VideoItem({
style={[
{backgroundColor: 'black'},
a.flex_1,
{aspectRatio: 1, maxWidth: 100},
a.aspect_square,
{maxWidth: 100},
a.justify_center,
a.align_center,
]}>
@@ -97,9 +97,7 @@ export const ExternalEmbed = ({
]}>
{imageUri && !embedPlayerParams ? (
<Image
style={{
aspectRatio: 1.91,
}}
style={[a.aspect_card]}
source={{uri: imageUri}}
accessibilityIgnoresInvertColors
/>
+1 -1
View File
@@ -17,7 +17,7 @@ export function FeedEmbed({
return (
<FeedCard.Link
view={embed.view}
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
style={[a.border, t.atoms.border_contrast_low, a.p_md, a.rounded_sm]}>
<FeedCard.Outer>
<FeedCard.Header>
<FeedCard.Avatar src={embed.view.avatar} />
+1 -1
View File
@@ -17,7 +17,7 @@ export function ListEmbed({
return (
<ListCard.Default
view={embed.view}
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}
style={[a.border, t.atoms.border_contrast_low, a.p_md, a.rounded_sm]}
/>
)
}
+2 -2
View File
@@ -23,7 +23,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {RichText} from '#/components/RichText'
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import {SubtleHover} from '#/components/SubtleHover'
import * as bsky from '#/types/bsky'
import {
type Embed as TEmbed,
@@ -284,7 +284,7 @@ export function QuoteEmbed({
childContainerStyle={[a.pt_sm]}>
{({active}) => (
<>
{!active && <SubtleWebHover hover={hover} style={[a.rounded_md]} />}
{!active && <SubtleHover hover={hover} style={[a.rounded_md]} />}
<Link
style={[!active && a.p_md]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
+3 -1
View File
@@ -57,7 +57,9 @@ export function PostRepliedTo({
size="xs"
style={[t.atoms.text_contrast_medium, {top: -1}]}
/>
<Text style={textStyle}>{label}</Text>
<Text style={[a.flex_1, textStyle]} numberOfLines={1}>
{label}
</Text>
</View>
)
}
@@ -5,8 +5,8 @@ import {t} from '@lingui/macro'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
@@ -16,7 +16,6 @@ export function DiscoverDebug({
feedContext: string | undefined
}) {
const {currentAccount} = useSession()
const {gtMobile} = useBreakpoints()
const gate = useGate()
const isDiscoverDebugUser =
IS_INTERNAL ||
@@ -30,15 +29,11 @@ export function DiscoverDebug({
<Pressable
accessible={false}
hitSlop={10}
style={[
a.absolute,
{zIndex: 1000, maxWidth: 65, bottom: -4},
gtMobile ? a.right_0 : a.left_0,
]}
style={[a.absolute, {zIndex: 1000, maxWidth: 65, bottom: -4}, a.left_0]}
onPress={e => {
e.stopPropagation()
Clipboard.setStringAsync(feedContext)
Toast.show(t`Copied to clipboard`, 'clipboard-check')
Toast.show(t`Copied to clipboard`)
}}>
<Text
numberOfLines={1}
@@ -46,7 +46,12 @@ import {
useProfileBlockMutationQueue,
useProfileMuteMutationQueue,
} from '#/state/queries/profile'
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
import {
InvalidInteractionSettingsError,
MAX_HIDDEN_REPLIES,
MaxHiddenRepliesError,
useToggleReplyVisibilityMutation,
} from '#/state/queries/threadgate'
import {useRequireAuth, useSession} from '#/state/session'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
import * as Toast from '#/view/com/util/Toast'
@@ -339,10 +344,30 @@ let PostMenuItems = ({
: _(msg({message: 'Reply visibility updated', context: 'toast'})),
)
} catch (e: any) {
Toast.show(
_(msg({message: 'Updating reply visibility failed', context: 'toast'})),
)
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
if (e instanceof MaxHiddenRepliesError) {
Toast.show(
_(
msg({
message: `You can hide a maximum of ${MAX_HIDDEN_REPLIES} replies.`,
context: 'toast',
}),
),
)
} else if (e instanceof InvalidInteractionSettingsError) {
Toast.show(
_(msg({message: 'Invalid interaction settings.', context: 'toast'})),
)
} else {
Toast.show(
_(
msg({
message: 'Updating reply visibility failed',
context: 'toast',
}),
),
)
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
}
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ let RepostButton = ({
<PostControlButton
testID="repostBtn"
active={isReposted}
activeColor={t.palette.positive_600}
activeColor={t.palette.positive_500}
big={big}
onPress={onPress}
onLongPress={onLongPress}
@@ -47,7 +47,7 @@ export const RepostButton = ({
<PostControlButton
testID="repostBtn"
active={isReposted}
activeColor={t.palette.positive_600}
activeColor={t.palette.positive_500}
label={props.accessibilityLabel}
big={big}
{...props}>
@@ -100,7 +100,7 @@ export const RepostButton = ({
<PostControlButton
onPress={() => requireAuth(() => {})}
active={isReposted}
activeColor={t.palette.positive_600}
activeColor={t.palette.positive_500}
label={_(msg`Repost or quote post`)}
big={big}>
<PostControlButtonIcon icon={Repost} />
@@ -24,7 +24,6 @@ import type * as bsky from '#/types/bsky'
export function RecentChats({postUri}: {postUri: string}) {
const control = useDialogContext()
const {_} = useLingui()
const {currentAccount} = useSession()
const {data} = useListConvosQuery({status: 'accepted'})
const convos = data?.pages[0]?.convos?.slice(0, 10)
@@ -136,7 +136,9 @@ let ShareMenuItems = ({
{hideInPWI && (
<Menu.Group>
<Menu.ContainerItem>
<Admonition type="warning" style={[a.flex_1, a.border_0, a.p_0]}>
<Admonition
type="warning"
style={[a.flex_1, a.border_0, a.p_0, a.bg_transparent]}>
<Trans>This post is only visible to logged-in users.</Trans>
</Admonition>
</Menu.ContainerItem>
+1 -1
View File
@@ -210,7 +210,7 @@ let PostControls = ({
a.flex_1,
a.align_start,
{marginLeft: big ? -2 : -6},
replyDisabled ? {opacity: 0.5} : undefined,
replyDisabled ? {opacity: 0.6} : undefined,
]}>
<PostControlButton
testID="replyBtn"
+3 -3
View File
@@ -32,7 +32,7 @@ export function Text({blend, style}: TextStyleProp & SkeletonProps) {
<View
style={[
a.rounded_md,
t.atoms.bg_contrast_25,
t.atoms.bg_contrast_50,
{
height: lineHeight * 0.7,
opacity: blend ? 0.6 : 1,
@@ -57,7 +57,7 @@ export function Circle({
a.justify_center,
a.align_center,
a.rounded_full,
t.atoms.bg_contrast_25,
t.atoms.bg_contrast_50,
{
width: size,
height: size,
@@ -80,7 +80,7 @@ export function Pill({
<View
style={[
a.rounded_full,
t.atoms.bg_contrast_25,
t.atoms.bg_contrast_50,
{
width: size * 1.618,
height: size,
+1 -1
View File
@@ -91,8 +91,8 @@ function ShareDialogInner({
source={{uri: imageUrl}}
style={[
a.rounded_sm,
a.aspect_card,
{
aspectRatio: 1200 / 630,
transform: [{scale: gtMobile ? 0.85 : 1}],
marginTop: gtMobile ? -20 : 0,
},
@@ -191,7 +191,7 @@ export function Embed({
<Link starterPack={starterPack}>
<Image
source={imageUri}
style={[a.w_full, {aspectRatio: 1.91}]}
style={[a.w_full, a.aspect_card]}
accessibilityIgnoresInvertColors={true}
/>
<View style={[a.px_sm, a.py_md]}>
@@ -13,6 +13,7 @@ import {useLingui} from '@lingui/react'
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {
@@ -150,8 +151,10 @@ export function WizardProfileCard({
if (profile.did === targetProfileDid) return
if (!included) {
logger.metric('starterPack:addUser', {})
dispatch({type: 'AddProfile', profile})
} else {
logger.metric('starterPack:removeUser', {})
dispatch({type: 'RemoveProfile', profileDid: profile.did})
}
}
+18 -3
View File
@@ -1,8 +1,15 @@
import {View} from 'react-native'
import {isTouchDevice} from '#/lib/browser'
import {isNative, isWeb} from '#/platform/detection'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
export function SubtleHover({
style,
hover,
web = true,
native = false,
}: ViewStyleProp & {hover: boolean; web?: boolean; native?: boolean}) {
const t = useTheme()
let opacity: number
@@ -18,17 +25,25 @@ export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
break
}
return (
const el = (
<View
style={[
a.absolute,
a.inset_0,
a.pointer_events_none,
a.transition_opacity,
t.atoms.bg_contrast_25,
t.atoms.bg_contrast_50,
style,
{opacity: hover ? opacity : 0},
]}
/>
)
if (isWeb && web) {
return isTouchDevice ? null : el
} else if (isNative && native) {
return el
}
return null
}
-5
View File
@@ -1,5 +0,0 @@
import {type ViewStyleProp} from '#/alf'
export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
return null
}
-49
View File
@@ -1,49 +0,0 @@
import {StyleSheet, View} from 'react-native'
import {isTouchDevice} from '#/lib/browser'
import {useTheme, type ViewStyleProp} from '#/alf'
export function SubtleWebHover({
style,
hover,
}: ViewStyleProp & {hover: boolean}) {
const t = useTheme()
if (isTouchDevice) {
return null
}
let opacity = 0.5
switch (t.name) {
case 'dark':
opacity = 0.4
break
case 'dim':
opacity = 0.45
break
case 'light':
opacity = 0.5
break
}
return (
<View
style={[
t.atoms.bg_contrast_25,
styles.container,
{opacity: hover ? opacity : 0},
style,
]}
/>
)
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
top: 0,
pointerEvents: 'none',
// @ts-ignore web only
transition: '0.15s ease-in-out opacity',
},
})
@@ -159,7 +159,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
a.pt_lg,
a.pb_md,
]}>
<SuccessIcon size="sm" fill={t.palette.positive_600} />
<SuccessIcon size="sm" fill={t.palette.positive_500} />
<Text style={[a.text_xl, a.font_bold]}>
<Trans>Success</Trans>
</Text>
@@ -281,7 +281,7 @@ export function Update(_props: ScreenProps<ScreenID.Update>) {
<Divider />
<View style={[a.gap_sm]}>
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
<Check fill={t.palette.positive_600} size="xs" />
<Check fill={t.palette.positive_500} size="xs" />
<Text style={[a.text_md, a.font_bold]}>
<Trans>Success!</Trans>
</Text>
@@ -176,7 +176,7 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
<View style={[a.gap_sm]}>
<Text style={[a.text_xl, a.font_bold]}>
<Span style={{top: 1}}>
<Check size="sm" fill={t.palette.positive_600} />
<Check size="sm" fill={t.palette.positive_500} />
</Span>
{' '}
<Trans>Email verification complete!</Trans>
@@ -202,7 +202,7 @@ export function Verify({config, showScreen}: ScreenProps<ScreenID.Verify>) {
state.mutationStatus === 'success' ? (
<>
<Span style={{top: 1}}>
<Check size="sm" fill={t.palette.positive_600} />
<Check size="sm" fill={t.palette.positive_500} />
</Span>
{' '}
<Trans>Email sent!</Trans>
+2 -1
View File
@@ -300,7 +300,8 @@ export function GifPreview({
a.flex_1,
a.mb_sm,
a.rounded_sm,
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
a.aspect_card,
{opacity: pressed ? 0.8 : 1},
t.atoms.bg_contrast_25,
]}
source={{
@@ -13,6 +13,7 @@ import isEqual from 'lodash.isequal'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useMyListsQuery} from '#/state/queries/my-lists'
import {useGetPost} from '#/state/queries/post'
import {
createPostgateQueryKey,
getPostgateRecord,
@@ -25,12 +26,15 @@ import {
} from '#/state/queries/postgate/util'
import {
createThreadgateViewQueryKey,
getThreadgateView,
type ThreadgateAllowUISetting,
threadgateViewToAllowUISetting,
useSetThreadgateAllowMutation,
useThreadgateViewQuery,
} from '#/state/queries/threadgate'
import {
PostThreadContextProvider,
usePostThreadContext,
} from '#/state/queries/usePostThread'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
@@ -133,10 +137,13 @@ export type PostInteractionSettingsDialogProps = {
export function PostInteractionSettingsDialog(
props: PostInteractionSettingsDialogProps,
) {
const postThreadContext = usePostThreadContext()
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<PostInteractionSettingsDialogControlledInner {...props} />
<PostThreadContextProvider context={postThreadContext}>
<PostInteractionSettingsDialogControlledInner {...props} />
</PostThreadContextProvider>
</Dialog.Outer>
)
}
@@ -558,6 +565,7 @@ export function usePrefetchPostInteractionSettings({
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const getPost = useGetPost()
return React.useCallback(async () => {
try {
@@ -570,7 +578,10 @@ export function usePrefetchPostInteractionSettings({
}),
queryClient.prefetchQuery({
queryKey: createThreadgateViewQueryKey(rootPostUri),
queryFn: () => getThreadgateView({agent, postUri: rootPostUri}),
queryFn: async () => {
const post = await getPost({uri: rootPostUri})
return post.threadgate ?? null
},
staleTime: STALE.SECONDS.THIRTY,
}),
])
@@ -579,5 +590,5 @@ export function usePrefetchPostInteractionSettings({
safeMessage: e.message,
})
}
}, [queryClient, agent, postUri, rootPostUri])
}, [queryClient, agent, postUri, rootPostUri, getPost])
}
+4 -1
View File
@@ -11,6 +11,7 @@ import {useQueryClient} from '@tanstack/react-query'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {
invalidateActorStarterPacksWithMembershipQuery,
@@ -47,7 +48,6 @@ export function StarterPackDialog({
targetDid,
enabled,
}: StarterPackDialogProps) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const requireEmailVerification = useRequireEmailVerification()
@@ -295,6 +295,7 @@ function StarterPackItem({
if (!starterPack.list?.uri || isPendingRefresh) return
const listUri = starterPack.list.uri
const starterPackUri = starterPack.uri
setIsPendingRefresh(true)
@@ -303,6 +304,7 @@ function StarterPackItem({
listUri: listUri,
actorDid: targetDid,
})
logger.metric('starterPack:addUser', {starterPack: starterPackUri})
} else {
if (!starterPackWithMembership.listItem?.uri) {
console.error('Cannot remove: missing membership URI')
@@ -314,6 +316,7 @@ function StarterPackItem({
actorDid: targetDid,
membershipUri: starterPackWithMembership.listItem.uri,
})
logger.metric('starterPack:removeUser', {starterPack: starterPackUri})
}
}
@@ -349,6 +349,14 @@ function DialogInner({
? _(msg`Create user list`)
: _(msg`Create moderation list`)
const displayNamePlaceholder = isCurateList
? _(msg`e.g. Great Posters`)
: _(msg`e.g. Spammers`)
const descriptionPlaceholder = isCurateList
? _(msg`e.g. The posters who never miss.`)
: _(msg`e.g. Users that repeatedly reply with ads.`)
return (
<Dialog.ScrollableInner
label={title}
@@ -389,7 +397,7 @@ function DialogInner({
defaultValue={displayName}
onChangeText={onChangeDisplayName}
label={_(msg`Name`)}
placeholder={_(msg`e.g. Great Posters`)}
placeholder={displayNamePlaceholder}
testID="editListNameInput"
/>
</TextField.Root>
@@ -426,7 +434,7 @@ function DialogInner({
onChangeText={onChangeDescription}
multiline
label={_(msg`Description`)}
placeholder={_(msg`e.g. The posters that never miss.`)}
placeholder={descriptionPlaceholder}
testID="editProfileDescriptionInput"
/>
</TextField.Root>
+1 -1
View File
@@ -102,7 +102,7 @@ export function LiveStatus({
style={[
t.atoms.bg_contrast_25,
a.w_full,
{aspectRatio: 1.91},
a.aspect_card,
android([
a.overflow_hidden,
{
@@ -32,7 +32,9 @@ export interface LabelsOnMeDialogProps {
export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Outer
control={props.control}
nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<LabelsOnMeDialogInner {...props} />
</Dialog.Outer>
@@ -24,7 +24,9 @@ export interface ModerationDetailsDialogProps {
export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Outer
control={props.control}
nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<ModerationDetailsDialogInner {...props} />
</Dialog.Outer>
+3
View File
@@ -34,6 +34,7 @@ interface Props extends ComponentProps<typeof Link> {
modui: ModerationUI
profile: AppBskyActorDefs.ProfileViewBasic
interpretFilterAsBlur?: boolean
hiderStyle?: StyleProp<ViewStyle>
}
export function PostHider({
@@ -42,6 +43,7 @@ export function PostHider({
disabled,
modui,
style,
hiderStyle,
children,
iconSize,
iconStyles,
@@ -100,6 +102,7 @@ export function PostHider({
},
override ? {paddingBottom: 0} : undefined,
t.atoms.bg,
hiderStyle,
]}>
<ModerationDetailsDialog control={control} modcause={blur} />
<Pressable
@@ -6,6 +6,7 @@ import * as Pills from '#/components/Pills'
export function ProfileHeaderAlerts({
moderation,
style,
}: {
moderation: ModerationDecision
style?: StyleProp<ViewStyle>
@@ -16,7 +17,7 @@ export function ProfileHeaderAlerts({
}
return (
<Pills.Row size="lg">
<Pills.Row size="lg" style={style}>
{modui.alerts.filter(unique).map(cause => (
<Pills.Label
size="lg"
@@ -219,10 +219,13 @@ function Inner(props: ReportDialogProps) {
<Admonition.Outer type="error">
<Admonition.Row>
<Admonition.Icon />
<Admonition.Text>
<Trans>Something went wrong, please try again</Trans>
</Admonition.Text>
<Admonition.Content>
<Admonition.Text>
<Trans>Something went wrong, please try again</Trans>
</Admonition.Text>
</Admonition.Content>
<Admonition.Button
color="negative_subtle"
label={_(msg`Retry loading report options`)}
onPress={() => refetchLabelers()}>
<ButtonText>
+2 -2
View File
@@ -21,12 +21,12 @@ export const ENV: string = process.env.EXPO_PUBLIC_ENV
export const IS_TESTFLIGHT = ENV === 'testflight'
/**
* Indicates whether the app is __DEV__
* Indicates whether the app is `__DEV__`
*/
export const IS_DEV = __DEV__
/**
* Indicates whether the app is __DEV__ or TestFlight
* Indicates whether the app is `__DEV__` or TestFlight
*/
export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
+1
View File
@@ -17,6 +17,7 @@ export const EMBED_SERVICE = 'https://embed.bsky.app'
export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
export const STARTER_PACK_MAX_SIZE = 150
export const CARD_ASPECT_RATIO = 1200 / 630
// HACK
// Yes, this is exactly what it looks like. It's a hard-coded constant
+1 -1
View File
@@ -51,7 +51,7 @@ export function useIntentHandler() {
}
const urlp = new URL(url)
const [_, intent, intentType] = urlp.pathname.split('/')
const [__, intent, intentType] = urlp.pathname.split('/')
// On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the
// intent check. On web, we have to check the first part of the path since we have an actual hostname
+9 -4
View File
@@ -10,6 +10,7 @@ import {
useUpdates,
} from 'expo-updates'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {IS_TESTFLIGHT} from '#/env'
@@ -145,8 +146,10 @@ export function useOTAUpdates() {
} else {
logger.debug('No update available.')
}
} catch (e) {
logger.error('OTA Update Error', {error: `${e}`})
} catch (err) {
if (!isNetworkError(err)) {
logger.error('OTA Update Error', {safeMessage: err})
}
}
}, 10e3)
}, [])
@@ -154,8 +157,10 @@ export function useOTAUpdates() {
const onIsTestFlight = React.useCallback(async () => {
try {
await updateTestflight()
} catch (e: any) {
logger.error('Internal OTA Update Error', {error: `${e}`})
} catch (err: any) {
if (!isNetworkError(err)) {
logger.error('Internal OTA Update Error', {safeMessage: err})
}
}
}, [])
+4 -1
View File
@@ -62,7 +62,10 @@ export async function getLinkMeta(
likelyType,
url,
}
if (likelyType !== LikelyType.HTML) {
const htmlExemptedHostnames: string[] = ['storage.courtlistener.com']
const isExemptedFromHtmlCheck = htmlExemptedHostnames.includes(urlp.hostname)
// Skip early return only for hosts exempted from the HTML check
if (likelyType !== LikelyType.HTML && !isExemptedFromHtmlCheck) {
return meta
}
@@ -1,3 +1,5 @@
import {type ModerationUI} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import type * as bsky from '#/types/bsky'
@@ -5,9 +7,10 @@ import type * as bsky from '#/types/bsky'
export function createSanitizedDisplayName(
profile: bsky.profile.AnyProfileView,
noAt = false,
moderation?: ModerationUI,
) {
if (profile.displayName != null && profile.displayName !== '') {
return sanitizeDisplayName(profile.displayName)
return sanitizeDisplayName(profile.displayName, moderation)
} else {
return sanitizeHandle(profile.handle, noAt ? '' : '@')
}
+4 -4
View File
@@ -67,7 +67,7 @@ export type CommonNavigatorParams = {
InterestsSettings: undefined
AboutSettings: undefined
AppIconSettings: undefined
Search: {q?: string}
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
Hashtag: {tag: string; author?: string}
Topic: {topic: string}
MessagesConversation: {conversation: string; embed?: string; accept?: true}
@@ -102,7 +102,7 @@ export type HomeTabNavigatorParams = CommonNavigatorParams & {
}
export type SearchTabNavigatorParams = CommonNavigatorParams & {
Search: {q?: string}
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
@@ -119,7 +119,7 @@ export type MessagesTabNavigatorParams = CommonNavigatorParams & {
export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined
Search: {q?: string}
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
Feeds: undefined
Notifications: undefined
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
@@ -129,7 +129,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined
Home: undefined
SearchTab: undefined
Search: {q?: string}
Search: {q?: string; tab?: 'user' | 'profile' | 'feed'}
Feeds: undefined
NotificationsTab: undefined
Notifications: undefined
+1 -5
View File
@@ -1,18 +1,14 @@
export type Gate =
// Keep this alphabetic please.
| 'alt_share_icon'
| 'cta_above_post_heading'
| 'cta_above_post_replies'
| 'debug_show_feedcontext'
| 'debug_subscriptions'
| 'disable_onboarding_policy_update_notice'
| 'explore_show_suggested_feeds'
| 'feed_reply_button_open_thread'
| 'old_postonboarding'
| 'onboarding_add_video_feed'
| 'onboarding_suggested_accounts'
| 'onboarding_value_prop'
| 'post_follow_profile_suggested_accounts'
| 'remove_show_latest_button'
| 'test_gate_1'
| 'test_gate_2'
| 'welcome_modal'
+11 -11
View File
@@ -105,7 +105,7 @@ export function parseEmbedPlayerFromUrl(
urlp.hostname === 'm.youtube.com' ||
urlp.hostname === 'music.youtube.com'
) {
const [_, page, shortOrLiveVideoId] = urlp.pathname.split('/')
const [__, page, shortOrLiveVideoId] = urlp.pathname.split('/')
const isShorts = page === 'shorts'
const isLive = page === 'live'
@@ -137,7 +137,7 @@ export function parseEmbedPlayerFromUrl(
window.location.hostname
: 'localhost'
const [_, channelOrVideo, clipOrId, id] = urlp.pathname.split('/')
const [__, channelOrVideo, clipOrId, id] = urlp.pathname.split('/')
if (channelOrVideo === 'videos') {
return {
@@ -162,7 +162,7 @@ export function parseEmbedPlayerFromUrl(
// spotify
if (urlp.hostname === 'open.spotify.com') {
const [_, typeOrLocale, idOrType, id] = urlp.pathname.split('/')
const [__, typeOrLocale, idOrType, id] = urlp.pathname.split('/')
if (idOrType) {
if (typeOrLocale === 'playlist' || idOrType === 'playlist') {
@@ -210,7 +210,7 @@ export function parseEmbedPlayerFromUrl(
urlp.hostname === 'soundcloud.com' ||
urlp.hostname === 'www.soundcloud.com'
) {
const [_, user, trackOrSets, set] = urlp.pathname.split('/')
const [__, user, trackOrSets, set] = urlp.pathname.split('/')
if (user && trackOrSets) {
if (trackOrSets === 'sets' && set) {
@@ -270,7 +270,7 @@ export function parseEmbedPlayerFromUrl(
}
if (urlp.hostname === 'vimeo.com' || urlp.hostname === 'www.vimeo.com') {
const [_, videoId] = urlp.pathname.split('/')
const [__, videoId] = urlp.pathname.split('/')
if (videoId) {
return {
type: 'vimeo_video',
@@ -281,7 +281,7 @@ export function parseEmbedPlayerFromUrl(
}
if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') {
const [_, gifs, nameAndId] = urlp.pathname.split('/')
const [__, gifs, nameAndId] = urlp.pathname.split('/')
/*
* nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name)
@@ -309,7 +309,7 @@ export function parseEmbedPlayerFromUrl(
// These can include (presumably) a tracking id in the path name, so we have to check for that as well
if (giphyRegex.test(urlp.hostname)) {
// We can link directly to the gif, if its a proper link
const [_, media, trackingOrId, idOrFilename, filename] =
const [__, media, trackingOrId, idOrFilename, filename] =
urlp.pathname.split('/')
if (media === 'media') {
@@ -338,7 +338,7 @@ export function parseEmbedPlayerFromUrl(
// Finally, we should see if it is a link to i.giphy.com. These links don't necessarily end in .gif but can also
// be .webp
if (urlp.hostname === 'i.giphy.com' || urlp.hostname === 'www.i.giphy.com') {
const [_, mediaOrFilename, filename] = urlp.pathname.split('/')
const [__, mediaOrFilename, filename] = urlp.pathname.split('/')
if (mediaOrFilename === 'media' && filename) {
const gifId = filename.split('.')[0]
@@ -389,7 +389,7 @@ export function parseEmbedPlayerFromUrl(
const path_components = urlp.pathname.slice(1, i + 1).split('/')
if (path_components.length === 4) {
// discard username - it's not relevant
const [photos, _, albums, id] = path_components
const [photos, __, albums, id] = path_components
if (photos === 'photos' && albums === 'albums') {
// this at least has the shape of a valid photo-album URL!
return {
@@ -417,7 +417,7 @@ export function parseEmbedPlayerFromUrl(
// link shortened flickr path
if (urlp.hostname === 'flic.kr') {
const b58alph = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
let [_, type, idBase58Enc] = urlp.pathname.split('/')
let [__, type, idBase58Enc] = urlp.pathname.split('/')
let id = 0n
for (const char of idBase58Enc) {
const nextIdx = b58alph.indexOf(char)
@@ -528,7 +528,7 @@ export function parseTenorGif(urlp: URL):
return {success: false}
}
let [_, id, filename] = urlp.pathname.split('/')
let [__, id, filename] = urlp.pathname.split('/')
if (!id || !filename) {
return {success: false}
-17
View File
@@ -62,23 +62,6 @@ export function useWarnMaxGraphemeCount({
}, [splitter, maxCount, text])
}
// https://stackoverflow.com/a/52171480
export function toHashCode(str: string, seed = 0): number {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i)
h1 = Math.imul(h1 ^ ch, 2654435761)
h2 = Math.imul(h2 ^ ch, 1597334677)
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
}
export function countLines(str: string | undefined): number {
if (!str) return 0
return str.match(/\n/g)?.length ?? 0
+1 -1
View File
@@ -46,7 +46,7 @@ export function parseStarterPackUri(uri?: string): {
} else {
const url = new URL(uri)
const parts = url.pathname.split('/')
const [_, path, name, rkey] = parts
const [__, path, name, rkey] = parts
if (parts.length !== 4) return null
if (path !== 'starter-pack' && path !== 'start') return null
File diff suppressed because it is too large Load Diff
+17 -5
View File
@@ -175,19 +175,24 @@ export type MetricEvents = {
'feed:suggestion:press': {
feedUrl: string
}
'discover:showMore': {
'feed:showMore': {
feed: string
feedContext: string
}
'discover:showLess': {
'feed:showLess': {
feed: string
feedContext: string
}
'discover:clickthrough': {
'feed:clickthrough': {
feed: string
count: number
}
'discover:engaged': {
'feed:engaged': {
feed: string
count: number
}
'discover:seen': {
'feed:seen': {
feed: string
count: number
}
@@ -321,6 +326,12 @@ export type MetricEvents = {
| 'ChatsList'
| 'SendViaChatDialog'
}
'starterPack:addUser': {
starterPack?: string
}
'starterPack:removeUser': {
starterPack?: string
}
'starterPack:share': {
starterPack: string
shareType: 'link' | 'qrcode'
@@ -352,6 +363,7 @@ export type MetricEvents = {
'feed:interstitial:feedCard:press': {}
'profile:header:suggestedFollowsCard:press': {}
'profile:addToStarterPack': {}
'test:all:always': {}
'test:all:sometimes': {}
+1
View File
@@ -29,4 +29,5 @@ init({
* @see https://docs.sentry.io/platforms/react-native/configuration/options/#attach-stacktrace
*/
attachStacktrace: false,
sampleRate: env.IS_INTERNAL ? 1.0 : 0.1,
})
+194 -176
View File
@@ -42,11 +42,14 @@ import {Trash_Stroke2_Corner0_Rounded} from '#/components/icons/Trash'
import {Link} from '#/components/Link'
import {useMenuControl} from '#/components/Menu'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {createPortalGroup} from '#/components/Portal'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import type * as bsky from '#/types/bsky'
export const ChatListItemPortal = createPortalGroup()
export let ChatListItem = ({
convo,
showMenu = true,
@@ -331,200 +334,215 @@ function ChatListItemReady({
const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
return (
<GestureActionView actions={actions}>
<View
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
// @ts-expect-error web only
onFocus={onFocus}
onBlur={onMouseLeave}
style={[a.relative, t.atoms.bg]}>
<ChatListItemPortal.Provider>
<GestureActionView actions={actions}>
<View
style={[
a.z_10,
a.absolute,
{top: tokens.space.md, left: tokens.space.lg},
]}>
<PreviewableUserAvatar
profile={profile}
size={52}
moderation={moderation.ui('avatar')}
/>
</View>
<Link
to={`/messages/${convo.id}`}
label={displayName}
accessibilityHint={
!isDeletedAccount
? _(msg`Go to conversation with ${profile.handle}`)
: _(
msg`This conversation is with a deleted or a deactivated account. Press for options`,
)
}
accessibilityActions={
isNative
? [
{name: 'magicTap', label: _(msg`Open conversation options`)},
{name: 'longpress', label: _(msg`Open conversation options`)},
]
: undefined
}
onPress={onPress}
onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress}>
{({hovered, pressed, focused}) => (
<View
style={[
a.flex_row,
isDeletedAccount ? a.align_center : a.align_start,
a.flex_1,
a.px_lg,
a.py_md,
a.gap_md,
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
]}>
{/* Avatar goes here */}
<View style={{width: 52, height: 52}} />
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
// @ts-expect-error web only
onFocus={onFocus}
onBlur={onMouseLeave}
style={[a.relative, t.atoms.bg]}>
<View
style={[
a.z_10,
a.absolute,
{top: tokens.space.md, left: tokens.space.lg},
]}>
<PreviewableUserAvatar
profile={profile}
size={52}
moderation={moderation.ui('avatar')}
/>
</View>
<Link
to={`/messages/${convo.id}`}
label={displayName}
accessibilityHint={
!isDeletedAccount
? _(msg`Go to conversation with ${profile.handle}`)
: _(
msg`This conversation is with a deleted or a deactivated account. Press for options`,
)
}
accessibilityActions={
isNative
? [
{
name: 'magicTap',
label: _(msg`Open conversation options`),
},
{
name: 'longpress',
label: _(msg`Open conversation options`),
},
]
: undefined
}
onPress={onPress}
onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress}>
{({hovered, pressed, focused}) => (
<View
style={[a.flex_1, a.justify_center, web({paddingRight: 45})]}>
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}>
<View style={[a.flex_shrink]}>
style={[
a.flex_row,
isDeletedAccount ? a.align_center : a.align_start,
a.flex_1,
a.px_lg,
a.py_md,
a.gap_md,
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
]}>
{/* Avatar goes here */}
<View style={{width: 52, height: 52}} />
<View
style={[a.flex_1, a.justify_center, web({paddingRight: 45})]}>
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}>
<View style={[a.flex_shrink]}>
<Text
emoji
numberOfLines={1}
style={[
a.text_md,
t.atoms.text,
a.font_semi_bold,
{lineHeight: 21},
isDimStyle && t.atoms.text_contrast_medium,
]}>
{displayName}
</Text>
</View>
{verification.showBadge && (
<View style={[a.pl_xs, a.self_center]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
{lastMessageSentAt && (
<View style={[a.pl_xs]}>
<TimeElapsed timestamp={lastMessageSentAt}>
{({timeElapsed}) => (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
&middot; {timeElapsed}
</Text>
)}
</TimeElapsed>
</View>
)}
{(convo.muted || moderation.blocked) && (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
{' '}
&middot;{' '}
<BellStroke
size="xs"
style={[t.atoms.text_contrast_medium]}
/>
</Text>
)}
</View>
{!isDeletedAccount && (
<Text
emoji
numberOfLines={1}
style={[
a.text_md,
t.atoms.text,
a.font_semi_bold,
{lineHeight: 21},
isDimStyle && t.atoms.text_contrast_medium,
]}>
{displayName}
</Text>
</View>
{verification.showBadge && (
<View style={[a.pl_xs, a.self_center]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
{lastMessageSentAt && (
<View style={[a.pl_xs]}>
<TimeElapsed timestamp={lastMessageSentAt}>
{({timeElapsed}) => (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
&middot; {timeElapsed}
</Text>
)}
</TimeElapsed>
</View>
)}
{(convo.muted || moderation.blocked) && (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
a.pb_xs,
]}>
{' '}
&middot;{' '}
<BellStroke
size="xs"
style={[t.atoms.text_contrast_medium]}
/>
@{profile.handle}
</Text>
)}
<Text
emoji
numberOfLines={2}
style={[
a.text_sm,
a.leading_snug,
hasUnread ? a.font_semi_bold : t.atoms.text_contrast_high,
isDimStyle && t.atoms.text_contrast_medium,
]}>
{lastMessage}
</Text>
<PostAlerts
modui={moderation.ui('contentList')}
size="lg"
style={[a.pt_xs]}
/>
{children}
</View>
{!isDeletedAccount && (
<Text
numberOfLines={1}
style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}>
@{profile.handle}
</Text>
{hasUnread && (
<View
style={[
a.absolute,
a.rounded_full,
{
backgroundColor: isDimStyle
? t.palette.contrast_200
: t.palette.primary_500,
height: 7,
width: 7,
top: 15,
right: 12,
},
]}
/>
)}
<Text
emoji
numberOfLines={2}
style={[
a.text_sm,
a.leading_snug,
hasUnread ? a.font_semi_bold : t.atoms.text_contrast_high,
isDimStyle && t.atoms.text_contrast_medium,
]}>
{lastMessage}
</Text>
<PostAlerts
modui={moderation.ui('contentList')}
size="lg"
style={[a.pt_xs]}
/>
{children}
</View>
)}
</Link>
{hasUnread && (
<View
style={[
a.absolute,
a.rounded_full,
{
backgroundColor: isDimStyle
? t.palette.contrast_200
: t.palette.primary_500,
height: 7,
width: 7,
top: 15,
right: 12,
},
]}
/>
)}
</View>
<ChatListItemPortal.Outlet />
{showMenu && (
<ConvoMenu
convo={convo}
profile={profile}
control={menuControl}
currentScreen="list"
showMarkAsRead={convo.unreadCount > 0}
hideTrigger={isNative}
blockInfo={blockInfo}
style={[
a.absolute,
a.h_full,
a.self_end,
a.justify_center,
{
right: tokens.space.lg,
opacity:
!gtMobile || showActions || menuControl.isOpen ? 1 : 0,
},
]}
latestReportableMessage={latestReportableMessage}
/>
)}
</Link>
{showMenu && (
<ConvoMenu
convo={convo}
profile={profile}
control={menuControl}
<LeaveConvoPrompt
control={leaveConvoControl}
convoId={convo.id}
currentScreen="list"
showMarkAsRead={convo.unreadCount > 0}
hideTrigger={isNative}
blockInfo={blockInfo}
style={[
a.absolute,
a.h_full,
a.self_end,
a.justify_center,
{
right: tokens.space.lg,
opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0,
},
]}
latestReportableMessage={latestReportableMessage}
/>
)}
<LeaveConvoPrompt
control={leaveConvoControl}
convoId={convo.id}
currentScreen="list"
/>
</View>
</GestureActionView>
</View>
</GestureActionView>
</ChatListItemPortal.Provider>
)
}
@@ -46,7 +46,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
label={_(msg`Block or report`)}
convo={convoState.convo}
profile={otherUser}
color="negative"
color="negative_subtle"
size="small"
currentScreen="conversation"
/>
@@ -70,8 +70,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
<AcceptChatButton
onAcceptConvo={onAcceptChat}
convo={convoState.convo}
color="primary"
variant="outline"
color="primary_subtle"
size="small"
currentScreen="conversation"
/>
@@ -36,7 +36,6 @@ export function RejectMenu({
convo,
profile,
size = 'tiny',
variant = 'outline',
color = 'secondary',
label,
showDeleteConvo,
@@ -117,7 +116,6 @@ export function RejectMenu({
label={triggerProps.accessibilityLabel}
style={[a.flex_1]}
color={color}
variant={variant}
size={size}>
<ButtonText>
{label || (
@@ -129,7 +127,7 @@ export function RejectMenu({
</Button>
)}
</Menu.Trigger>
<Menu.Outer>
<Menu.Outer showCancel>
<Menu.Group>
{showDeleteConvo && (
<Menu.Item
@@ -181,7 +179,6 @@ export function RejectMenu({
export function AcceptChatButton({
convo,
size = 'tiny',
variant = 'solid',
color = 'secondary_inverted',
label,
currentScreen,
@@ -248,7 +245,6 @@ export function AcceptChatButton({
{...props}
label={label || _(msg`Accept chat request`)}
size={size}
variant={variant}
color={color}
style={a.flex_1}
onPress={onPressAccept}>
@@ -266,7 +262,6 @@ export function AcceptChatButton({
export function DeleteChatButton({
convo,
size = 'tiny',
variant = 'outline',
color = 'secondary',
label,
currentScreen,
@@ -315,7 +310,6 @@ export function DeleteChatButton({
<Button
label={label || _(msg`Delete chat`)}
size={size}
variant={variant}
color={color}
style={a.flex_1}
onPress={onPressDelete}
@@ -7,7 +7,7 @@ import {useSession} from '#/state/session'
import {atoms as a, tokens} from '#/alf'
import {KnownFollowers} from '#/components/KnownFollowers'
import {Text} from '#/components/Typography'
import {ChatListItem} from './ChatListItem'
import {ChatListItem, ChatListItemPortal} from './ChatListItem'
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
@@ -42,37 +42,40 @@ export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
<Trans comment="Accept a chat request">Accept Request</Trans>
</Text>
</View>
{/* then, this gets absolutely positioned on top of the spacer */}
<ChatListItemPortal.Portal>
<View
style={[
a.absolute,
a.pr_md,
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
{
bottom: tokens.space.md,
paddingLeft: tokens.space.lg + 52 + tokens.space.md,
},
]}>
{!isDeletedAccount ? (
<>
<AcceptChatButton convo={convo} currentScreen="list" />
<RejectMenu
convo={convo}
profile={otherUser}
showDeleteConvo
currentScreen="list"
/>
</>
) : (
<>
<DeleteChatButton convo={convo} currentScreen="list" />
<View style={a.flex_1} />
</>
)}
</View>
</ChatListItemPortal.Portal>
</ChatListItem>
<View
style={[
a.absolute,
a.pr_md,
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
{
bottom: tokens.space.md,
paddingLeft: tokens.space.lg + 52 + tokens.space.md,
},
]}>
{!isDeletedAccount ? (
<>
<AcceptChatButton convo={convo} currentScreen="list" />
<RejectMenu
convo={convo}
profile={otherUser}
showDeleteConvo
currentScreen="list"
/>
</>
) : (
<>
<DeleteChatButton convo={convo} currentScreen="list" />
<View style={a.flex_1} />
</>
)}
</View>
</View>
)
}
+16 -27
View File
@@ -328,19 +328,21 @@ export function ModerationScreenInner({
</Link>
</View>
{declaredAge === undefined && (
<>
<Text
style={[
a.pt_2xl,
a.pb_md,
a.text_md,
a.font_semi_bold,
t.atoms.text_contrast_high,
]}>
<Trans>Content filters</Trans>
</Text>
{(!isDeclaredUnderage || declaredAge === undefined) && (
<Text
style={[
a.pt_2xl,
a.pb_md,
a.text_md,
a.font_semi_bold,
t.atoms.text_contrast_high,
]}>
<Trans>Content filters</Trans>
</Text>
)}
{declaredAge === undefined ? (
<>
<Button
label={_(msg`Confirm your birthdate`)}
size="small"
@@ -360,21 +362,8 @@ export function ModerationScreenInner({
<BirthDateSettingsDialog control={birthdateDialogControl} />
</>
)}
{!isDeclaredUnderage && (
) : !isDeclaredUnderage ? (
<>
<Text
style={[
a.pt_2xl,
a.pb_md,
a.text_md,
a.font_semi_bold,
t.atoms.text_contrast_high,
]}>
<Trans>Content filters</Trans>
</Text>
<AgeAssuranceAdmonition style={[a.pb_md]}>
<Trans>
You must complete age assurance in order to access the settings
@@ -466,7 +455,7 @@ export function ModerationScreenInner({
</View>
</View>
</>
)}
) : null}
<Text
style={[
+10 -12
View File
@@ -1,10 +1,11 @@
import React, {useState} from 'react'
import {ScrollView, View} from 'react-native'
import {Dimensions} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isWeb} from '#/platform/detection'
import {isAndroid, isWeb} from '#/platform/detection'
import {useOnboardingDispatch} from '#/state/shell'
import {Context} from '#/screens/Onboarding/state'
import {
@@ -58,13 +59,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
aria-label={dialogLabel}
accessibilityLabel={dialogLabel}
accessibilityHint={_(msg`Customizes your Bluesky experience`)}
style={[
// @ts-ignore web only -prf
isWeb ? a.fixed : a.absolute,
a.inset_0,
a.flex_1,
t.atoms.bg,
]}>
style={[isWeb ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
{__DEV__ && (
<Button
variant="ghost"
@@ -77,10 +72,10 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
a.absolute,
a.z_10,
{
left: '50%',
left: isWeb ? '50%' : Dimensions.get('window').width / 2 - 45,
top: insets.top + 2,
transform: [{translateX: '-50%'}],
},
web({transform: [{translateX: '-50%'}]}),
]}>
<ButtonText>[DEV] Clear</ButtonText>
</Button>
@@ -134,7 +129,8 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
<ScrollView
ref={scrollview}
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
contentContainerStyle={{borderWidth: 0}}
contentContainerStyle={{borderWidth: 0, minHeight: '100%'}}
showsVerticalScrollIndicator={!isAndroid}
scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}}
// @ts-expect-error web only --prf
dataSet={{'stable-gutters': 1}}>
@@ -172,7 +168,9 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
</View>
</View>
<View style={[a.w_full, a.mb_5xl, a.pt_md]}>{children}</View>
<View style={[a.w_full, a.h_full, a.mb_5xl, a.pt_md]}>
{children}
</View>
<View style={{height: 100 + footerHeight}} />
</View>
@@ -0,0 +1,55 @@
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
export function useValuePropText(step: 0 | 1 | 2) {
const {_} = useLingui()
return [
{
title: _(msg`Free your feed`),
description: _(
msg`No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you.`,
),
alt: _(
msg`A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens`,
),
},
{
title: _(msg`Find your people`),
description: _(
msg`Ditch the trolls and clickbait. Find real people and conversations that matter to you.`,
),
alt: _(
msg`Your profile picture surrounded by concentric circles of other users' profile pictures`,
),
},
{
title: _(msg`Forget the noise`),
description: _(
msg`No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention.`,
),
alt: _(
msg`An illustration of several Bluesky posts alongside repost, like, and comment icons`,
),
},
][step]
}
export function Dot({active}: {active: boolean}) {
const t = useTheme()
return (
<View
style={[
a.rounded_full,
{width: 8, height: 8},
active
? {backgroundColor: t.palette.primary_500}
: t.atoms.bg_contrast_50,
]}
/>
)
}
@@ -0,0 +1,127 @@
import {useRef, useState} from 'react'
import {View} from 'react-native'
import PagerView from 'react-native-pager-view'
import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {PROP_1, PROP_2, PROP_3} from './images'
import {Dot, useValuePropText} from './ValuePropositionPager.shared'
export function ValuePropositionPager({
step,
setStep,
avatarUri,
}: {
step: 0 | 1 | 2
setStep: (step: 0 | 1 | 2) => void
avatarUri?: string
}) {
const t = useTheme()
const [activePage, setActivePage] = useState(step)
const ref = useRef<PagerView>(null)
if (step !== activePage) {
setActivePage(step)
ref.current?.setPage(step)
}
const images = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]]
return (
<View style={[a.h_full, {marginHorizontal: tokens.space.xl * -1}]}>
<PagerView
ref={ref}
style={[a.flex_1]}
initialPage={step}
onPageSelected={evt => {
const page = evt.nativeEvent.position as 0 | 1 | 2
if (step !== page) {
setActivePage(page)
setStep(page)
}
}}>
{([0, 1, 2] as const).map(page => (
<Page
key={page}
page={page}
image={images[page]}
avatarUri={avatarUri}
/>
))}
</PagerView>
</View>
)
}
function Page({
page,
image,
avatarUri,
}: {
page: 0 | 1 | 2
image: string
avatarUri?: string
}) {
const {_} = useLingui()
const t = useTheme()
const {title, description, alt} = useValuePropText(page)
return (
<View key={page}>
<View
style={[
a.relative,
a.align_center,
a.justify_center,
a.pointer_events_none,
]}>
<Image
source={image}
style={[a.w_full, a.aspect_square]}
alt={alt}
accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background
/>
{page === 1 && (
<Image
source={avatarUri}
style={[
a.z_10,
a.absolute,
a.rounded_full,
{
width: `${(80 / 393) * 100}%`,
height: `${(80 / 393) * 100}%`,
},
]}
accessibilityIgnoresInvertColors
alt={_(msg`Your profile picture`)}
/>
)}
</View>
<View style={[a.mt_4xl, a.gap_2xl, a.px_xl, a.align_center]}>
<View style={[a.flex_row, a.gap_sm]}>
<Dot active={page === 0} />
<Dot active={page === 1} />
<Dot active={page === 2} />
</View>
<View style={[a.gap_sm]}>
<Text style={[a.font_bold, a.text_3xl, a.text_center]}>{title}</Text>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_md,
a.leading_snug,
a.text_center,
]}>
{description}
</Text>
</View>
</View>
</View>
)
}
@@ -0,0 +1,80 @@
import {View} from 'react-native'
import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {PROP_1, PROP_2, PROP_3} from './images'
import {Dot, useValuePropText} from './ValuePropositionPager.shared'
export function ValuePropositionPager({
step,
avatarUri,
}: {
step: 0 | 1 | 2
avatarUri?: string
}) {
const t = useTheme()
const {_} = useLingui()
const image = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]][step]
const {title, description, alt} = useValuePropText(step)
return (
<View>
<View
style={[
a.relative,
a.align_center,
a.justify_center,
a.pointer_events_none,
]}>
<Image
source={image}
style={[a.w_full, {aspectRatio: 1}]}
alt={alt}
accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background
/>
{step === 1 && (
<Image
source={avatarUri}
style={[
a.z_10,
a.absolute,
a.rounded_full,
{
width: `${(80 / 393) * 100}%`,
height: `${(80 / 393) * 100}%`,
},
]}
accessibilityIgnoresInvertColors
alt={_(msg`Your profile picture`)}
/>
)}
</View>
<View style={[a.mt_4xl, a.gap_2xl, a.align_center]}>
<View style={[a.flex_row, a.gap_sm]}>
<Dot active={step === 0} />
<Dot active={step === 1} />
<Dot active={step === 2} />
</View>
<View style={[a.gap_sm]}>
<Text style={[a.font_bold, a.text_3xl, a.text_center]}>{title}</Text>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_md,
a.leading_snug,
a.text_center,
]}>
{description}
</Text>
</View>
</View>
</View>
)
}
@@ -0,0 +1,28 @@
import {platform} from '#/alf'
export const PROP_1 = {
light: platform({
native: require('../../../../assets/images/onboarding/value_prop_1_light.webp'),
web: require('../../../../assets/images/onboarding/value_prop_1_light_borderless.webp'),
}),
dim: platform({
native: require('../../../../assets/images/onboarding/value_prop_1_dim.webp'),
web: require('../../../../assets/images/onboarding/value_prop_1_dim_borderless.webp'),
}),
dark: platform({
native: require('../../../../assets/images/onboarding/value_prop_1_dark.webp'),
web: require('../../../../assets/images/onboarding/value_prop_1_dark_borderless.webp'),
}),
} as const
export const PROP_2 = {
light: require('../../../../assets/images/onboarding/value_prop_2_light.webp'),
dim: require('../../../../assets/images/onboarding/value_prop_2_dim.webp'),
dark: require('../../../../assets/images/onboarding/value_prop_2_dark.webp'),
} as const
export const PROP_3 = {
light: require('../../../../assets/images/onboarding/value_prop_3_light.webp'),
dim: require('../../../../assets/images/onboarding/value_prop_3_dim.webp'),
dark: require('../../../../assets/images/onboarding/value_prop_3_dark.webp'),
} as const
@@ -1,12 +1,5 @@
import {useCallback, useContext, useState} from 'react'
import {View} from 'react-native'
import Animated, {
Easing,
LayoutAnimationConfig,
SlideInRight,
SlideOutLeft,
} from 'react-native-reanimated'
import {Image} from 'expo-image'
import {
type AppBskyActorDefs,
type AppBskyActorProfile,
@@ -29,7 +22,7 @@ import {
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {isWeb} from '#/platform/detection'
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {getAllListMembers} from '#/state/queries/list-members'
import {preferencesQueryKey} from '#/state/queries/preferences'
@@ -49,14 +42,7 @@ import {
} from '#/screens/Onboarding/Layout'
import {Context, type OnboardingState} from '#/screens/Onboarding/state'
import {bulkWriteFollows} from '#/screens/Onboarding/util'
import {
atoms as a,
native,
platform,
tokens,
useBreakpoints,
useTheme,
} from '#/alf'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {IconCircle} from '#/components/IconCircle'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
@@ -67,9 +53,9 @@ import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {ValuePropositionPager} from './ValuePropositionPager'
export function StepFinished() {
const {_} = useLingui()
const {state, dispatch} = useContext(Context)
const onboardDispatch = useOnboardingDispatch()
const [saving, setSaving] = useState(false)
@@ -276,33 +262,6 @@ export function StepFinished() {
)
}
const PROP_1 = {
light: platform({
native: require('../../../assets/images/onboarding/value_prop_1_light.webp'),
web: require('../../../assets/images/onboarding/value_prop_1_light_borderless.webp'),
}),
dim: platform({
native: require('../../../assets/images/onboarding/value_prop_1_dim.webp'),
web: require('../../../assets/images/onboarding/value_prop_1_dim_borderless.webp'),
}),
dark: platform({
native: require('../../../assets/images/onboarding/value_prop_1_dark.webp'),
web: require('../../../assets/images/onboarding/value_prop_1_dark_borderless.webp'),
}),
} as const
const PROP_2 = {
light: require('../../../assets/images/onboarding/value_prop_2_light.webp'),
dim: require('../../../assets/images/onboarding/value_prop_2_dim.webp'),
dark: require('../../../assets/images/onboarding/value_prop_2_dark.webp'),
} as const
const PROP_3 = {
light: require('../../../assets/images/onboarding/value_prop_3_light.webp'),
dim: require('../../../assets/images/onboarding/value_prop_3_dim.webp'),
dark: require('../../../assets/images/onboarding/value_prop_3_dark.webp'),
} as const
function ValueProposition({
finishOnboarding,
saving,
@@ -313,12 +272,9 @@ function ValueProposition({
state: OnboardingState
}) {
const [subStep, setSubStep] = useState<0 | 1 | 2>(0)
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const image = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]][subStep]
const onPress = () => {
if (subStep === 2) {
finishOnboarding() // has its own metrics
@@ -331,36 +287,6 @@ function ValueProposition({
}
}
const {title, description, alt} = [
{
title: _(msg`Free your feed`),
description: _(
msg`No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you.`,
),
alt: _(
msg`A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens`,
),
},
{
title: _(msg`Find your people`),
description: _(
msg`Ditch the trolls and clickbait. Find real people and conversations that matter to you.`,
),
alt: _(
msg`Your profile picture surrounded by concentric circles of other users' profile pictures`,
),
},
{
title: _(msg`Forget the noise`),
description: _(
msg`No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention.`,
),
alt: _(
msg`An illustration of several Bluesky posts alongside repost, like, and comment icons`,
),
},
][subStep]
return (
<>
{!gtMobile && (
@@ -383,75 +309,15 @@ function ValueProposition({
</OnboardingHeaderSlot.Portal>
)}
<LayoutAnimationConfig skipEntering skipExiting>
<Animated.View
key={subStep}
entering={native(
SlideInRight.easing(Easing.out(Easing.exp)).duration(500),
)}
exiting={native(
SlideOutLeft.easing(Easing.out(Easing.exp)).duration(500),
)}>
<View
style={[
a.relative,
a.align_center,
a.justify_center,
isNative && {marginHorizontal: tokens.space.xl * -1},
a.pointer_events_none,
]}>
<Image
source={image}
style={[a.w_full, {aspectRatio: 1}]}
alt={alt}
accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background
/>
{subStep === 1 && (
<Image
source={state.profileStepResults.imageUri}
style={[
a.z_10,
a.absolute,
a.rounded_full,
{
width: `${(80 / 393) * 100}%`,
height: `${(80 / 393) * 100}%`,
},
]}
accessibilityIgnoresInvertColors
alt={_(msg`Your profile picture`)}
/>
)}
</View>
<View style={[a.mt_4xl, a.gap_2xl, a.align_center]}>
<View style={[a.flex_row, a.gap_sm]}>
<Dot active={subStep === 0} />
<Dot active={subStep === 1} />
<Dot active={subStep === 2} />
</View>
<View style={[a.gap_sm]}>
<Text style={[a.font_bold, a.text_3xl, a.text_center]}>
{title}
</Text>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_md,
a.leading_snug,
a.text_center,
]}>
{description}
</Text>
</View>
</View>
</Animated.View>
</LayoutAnimationConfig>
<ValuePropositionPager
step={subStep}
setStep={ss => setSubStep(ss)}
avatarUri={state.profileStepResults.imageUri}
/>
<OnboardingControls.Portal>
<View style={gtMobile && [a.gap_md, a.flex_row]}>
{gtMobile && (
{gtMobile && (isWeb ? subStep !== 2 : true) && (
<Button
disabled={saving}
color="secondary"
@@ -493,23 +359,6 @@ function ValueProposition({
)
}
function Dot({active}: {active: boolean}) {
const t = useTheme()
const {_} = useLingui()
return (
<View
style={[
a.rounded_full,
{width: 8, height: 8},
active
? {backgroundColor: t.palette.primary_500}
: t.atoms.bg_contrast_50,
]}
/>
)
}
function LegacyFinalStep({
finishOnboarding,
saving,
+6 -9
View File
@@ -2,7 +2,6 @@ import {useMemo, useReducer} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {
Layout,
OnboardingControls,
@@ -13,21 +12,19 @@ import {StepFinished} from '#/screens/Onboarding/StepFinished'
import {StepInterests} from '#/screens/Onboarding/StepInterests'
import {StepProfile} from '#/screens/Onboarding/StepProfile'
import {Portal} from '#/components/Portal'
import {ENV} from '#/env'
import {StepSuggestedAccounts} from './StepSuggestedAccounts'
export function Onboarding() {
const {_} = useLingui()
const gate = useGate()
const showValueProp = ENV !== 'e2e' && gate('onboarding_value_prop')
const showSuggestedAccounts =
ENV !== 'e2e' && gate('onboarding_suggested_accounts')
const [state, dispatch] = useReducer(reducer, {
...initialState,
totalSteps: showSuggestedAccounts ? 4 : 3,
totalSteps: 4,
experiments: {
onboarding_suggested_accounts: showSuggestedAccounts,
onboarding_value_prop: showValueProp,
// let's leave this flag logic in for now to avoid rebase churn
// TODO: remove this flag logic once we've finished with all experiments -sfn
onboarding_suggested_accounts: true,
onboarding_value_prop: true,
},
})
+4 -4
View File
@@ -7,7 +7,7 @@ import {
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {usePostQuery} from '#/state/queries/post'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
import * as Layout from '#/components/Layout'
@@ -17,11 +17,11 @@ export const PostLikedByScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {data: post} = usePostThreadQuery(uri)
const {data: post} = usePostQuery(uri)
let likeCount
if (post?.thread.type === 'post') {
likeCount = post.thread.post.likeCount
if (post) {
likeCount = post.likeCount
}
useFocusEffect(
+4 -4
View File
@@ -7,7 +7,7 @@ import {
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {usePostQuery} from '#/state/queries/post'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
import * as Layout from '#/components/Layout'
@@ -17,11 +17,11 @@ export const PostQuotesScreen = ({route}: Props) => {
const setMinimalShellMode = useSetMinimalShellMode()
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const {data: post} = usePostThreadQuery(uri)
const {data: post} = usePostQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.quoteCount
if (post) {
quoteCount = post.quoteCount
}
useFocusEffect(
+4 -4
View File
@@ -7,7 +7,7 @@ import {
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {usePostThreadQuery} from '#/state/queries/post-thread'
import {usePostQuery} from '#/state/queries/post'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
import * as Layout from '#/components/Layout'
@@ -17,11 +17,11 @@ export const PostRepostedByScreen = ({route}: Props) => {
const {name, rkey} = route.params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const setMinimalShellMode = useSetMinimalShellMode()
const {data: post} = usePostThreadQuery(uri)
const {data: post} = usePostQuery(uri)
let quoteCount
if (post?.thread.type === 'post') {
quoteCount = post.thread.post.repostCount
if (post) {
quoteCount = post.repostCount
}
useFocusEffect(
@@ -39,13 +39,12 @@ import {
OUTER_SPACE,
REPLY_LINE_WIDTH,
} from '#/screens/PostThread/const'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {colors} from '#/components/Admonition'
import {Button} from '#/components/Button'
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
import {InlineLinkText, Link} from '#/components/Link'
import {LoggedOutCTA} from '#/components/LoggedOutCTA'
import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts'
@@ -180,7 +179,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
const {_} = useLingui()
const {openComposer} = useOpenComposer()
const {currentAccount, hasSession} = useSession()
const {gtTablet} = useBreakpoints()
const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession)
const formatPostStatCount = useFormatPostStatCount()
@@ -315,8 +313,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
},
isRoot && [a.pt_lg],
]}>
{/* Show CTA for logged-out visitors - hide on desktop and check gate */}
{!gtTablet && <LoggedOutCTA gateName="cta_above_post_heading" />}
<View style={[a.flex_row, a.gap_md, a.pb_md]}>
<View collapsable={false}>
<PreviewableUserAvatar
@@ -395,7 +391,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
enableTags
selectable
value={richText}
style={[a.flex_1, a.text_xl]}
style={[a.flex_1, a.text_lg]}
authorHandle={post.author.handle}
shouldProxyLinks={true}
/>
@@ -621,7 +617,7 @@ function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) {
if (!isBackdated) return null
const orange = t.name === 'light' ? colors.warning.dark : colors.warning.light
const orange = colors.warning
return (
<>

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