Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9abf028c85 | |||
| 38445f31f4 | |||
| 4e418c0d05 | |||
| 3fe5cc1d5e | |||
| 7f4e3091f7 | |||
| 6114b33ce2 | |||
| 305ea8bdfb | |||
| a322ae412a | |||
| caa7736035 | |||
| 655964ec0b | |||
| 66526a8e10 | |||
| 8cded435d1 | |||
| bd17f04810 | |||
| bdc58045cf | |||
| 0482e917b6 | |||
| 0bc2355226 | |||
| 46a97fadcb | |||
| e90cfdc849 | |||
| 3bc906b911 | |||
| 7916c73b09 | |||
| 6d85fe05d1 | |||
| be324432bd | |||
| 133bc2921e | |||
| b90f807416 | |||
| fbb85e109c | |||
| 9d6f51ff88 | |||
| 7b10dcf249 | |||
| 93f90bec97 | |||
| 31eb41d21b | |||
| 336e7b5372 | |||
| 9dcd8f81db | |||
| cbcce8c960 | |||
| 2c6752f24a | |||
| 14ee7920f4 | |||
| 0ddd09eb9b | |||
| 5211b26903 | |||
| 6983efee7f | |||
| ac71ea235f | |||
| 5e78c947a0 | |||
| 952abc82f4 | |||
| b9a41f30a5 | |||
| 6a4c56dfa9 | |||
| 836d16fec9 | |||
| 58ffe365ae | |||
| 426f025940 | |||
| 20ff3e3ba0 | |||
| 5b8631d188 | |||
| 5be753ecdf | |||
| d7c180db0e | |||
| f5079b8158 | |||
| c7591ef058 | |||
| 372e20efa3 | |||
| 9e8bceec60 | |||
| 6432667f60 | |||
| 4a1b1f17f4 | |||
| 7574a745d1 | |||
| 53e43a957c | |||
| bb30b592d7 | |||
| bbc55eb0d7 | |||
| 3e2c181c40 | |||
| d496a22352 | |||
| d8413b09f8 | |||
| 5ec6db5297 | |||
| 2b4da7f536 | |||
| 5cd4a3362d | |||
| 834486c97d | |||
| e073bdac6e | |||
| 73d72814c5 | |||
| daed047bb4 | |||
| ee3e083938 | |||
| 0f089060d2 | |||
| 01ba0d0ed6 | |||
| c7f8da8f97 | |||
| 76cd645a40 | |||
| 124cfb57c7 | |||
| ac0c4ef2d9 | |||
| 917256713f | |||
| c26906e15c | |||
| 0c4b908020 | |||
| 457cd3d0fb | |||
| bc827dc97f | |||
| c129108b78 | |||
| 0b480bdaf8 | |||
| 535d4d6cf7 | |||
| 04b869714e | |||
| 9738f4858d | |||
| 0dafc2ccd6 | |||
| f8ae0540a0 | |||
| 625b4e61db | |||
| 0b02d9d9a7 |
@@ -33,3 +33,9 @@ EXPO_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Bitdrift API key. If undefined, Bitdrift will be disabled.
|
||||
EXPO_PUBLIC_BITDRIFT_API_KEY=
|
||||
|
||||
# bapp-config web worker URL
|
||||
BAPP_CONFIG_DEV_URL=
|
||||
|
||||
# Dev-only passthrough value for bapp-config web worker
|
||||
BAPP_CONFIG_DEV_BYPASS_SECRET=
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: build-and-push-link-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'bskylink/**'
|
||||
- 'Dockerfile.bskylink'
|
||||
- '.github/workflows/build-and-push-link-aws.yaml'
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
@@ -45,7 +50,7 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
file: ./Dockerfile.bskylink
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: build-and-push-ogcard-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'bskyogcard/**'
|
||||
- 'Dockerfile.bskyogcard'
|
||||
- '.github/workflows/build-and-push-ogcard-aws.yaml'
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
@@ -45,7 +50,7 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
file: ./Dockerfile.bskyogcard
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -53,12 +53,12 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.4'
|
||||
xcode-version: "16.4"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
version: 1.14.3
|
||||
version: 1.16.2
|
||||
|
||||
- name: 💾 Cache Pods
|
||||
uses: actions/cache@v3
|
||||
|
||||
@@ -192,12 +192,12 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
xcode-version: "16.2"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
version: 1.14.3
|
||||
version: 1.16.2
|
||||
|
||||
- name: 💾 Cache Pods
|
||||
uses: actions/cache@v3
|
||||
@@ -293,8 +293,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
@@ -332,7 +332,7 @@ jobs:
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
format: "MM-DD-HH-mm-ss"
|
||||
|
||||
- name: 🚀 Upload Artifact
|
||||
id: upload-artifact
|
||||
|
||||
@@ -26,8 +26,9 @@ COPY --from=build /app /app
|
||||
RUN mkdir /app/data && chown node /app/data
|
||||
|
||||
VOLUME /app/data
|
||||
EXPOSE 3000
|
||||
EXPOSE 3000 3001
|
||||
ENV CARD_PORT=3000
|
||||
ENV CARD_METRICS_PORT=3001
|
||||
ENV NODE_ENV=production
|
||||
# potential perf issues w/ io_uring on this version of node
|
||||
ENV UV_USE_IO_URING=0
|
||||
|
||||
@@ -33,4 +33,9 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "You're ready to go!"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
label: "NUX, if applicable"
|
||||
text: "Close"
|
||||
optional: true
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Following"
|
||||
|
||||
@@ -30,4 +30,9 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "You're ready to go!"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
label: "NUX, if applicable"
|
||||
text: "Close"
|
||||
optional: true
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Following"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
|
||||
import {
|
||||
|
||||
@@ -43,6 +43,7 @@ module.exports = function (_config) {
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
newArchEnabled: false,
|
||||
ios: {
|
||||
supportsTablet: false,
|
||||
bundleIdentifier: 'xyz.blueskyweb.app',
|
||||
@@ -102,6 +103,7 @@ module.exports = function (_config) {
|
||||
'zh-Hans',
|
||||
'zh-Hant',
|
||||
],
|
||||
UIDesignRequiresCompatibility: true,
|
||||
},
|
||||
associatedDomains: ASSOCIATED_DOMAINS,
|
||||
entitlements: {
|
||||
@@ -207,13 +209,12 @@ module.exports = function (_config) {
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '15.1',
|
||||
newArchEnabled: false,
|
||||
buildReactNativeFromSource: true,
|
||||
},
|
||||
android: {
|
||||
compileSdkVersion: 35,
|
||||
targetSdkVersion: 35,
|
||||
buildToolsVersion: '35.0.0',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -360,6 +361,7 @@ module.exports = function (_config) {
|
||||
},
|
||||
],
|
||||
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
||||
['expo-location'],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M9.7 16.895a4 4 0 0 1 4.6 0l3.7 2.6V6.5a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v12.995l3.7-2.6Zm10.3 2.6c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2 2 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v12.995Z"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#405168" d="M14.2 2.625c.834 0 1.482 0 2.001.042.523.043.949.131 1.331.326a3.38 3.38 0 0 1 1.475 1.475c.195.382.283.807.326 1.33.042.52.042 1.168.042 2.002v11.09c0 .495 0 .893-.027 1.199-.028.301-.087.585-.26.809-.249.323-.63.518-1.037.533-.282.01-.547-.107-.808-.26-.265-.154-.588-.385-.991-.673l-3.54-2.528c-.36-.258-.461-.322-.559-.347a.6.6 0 0 0-.306 0c-.098.025-.199.09-.559.347l-3.54 2.528c-.403.288-.726.519-.991.674-.261.152-.526.269-.808.259a1.38 1.38 0 0 1-1.038-.534c-.172-.223-.231-.507-.259-.808a7 7 0 0 1-.024-.528l-.003-.67V7.8c0-.834 0-1.482.042-2.001.043-.523.13-.949.325-1.331a3.38 3.38 0 0 1 1.476-1.475c.382-.195.808-.283 1.33-.326.52-.042 1.168-.042 2.002-.042h4.4Zm-4.4.75c-.846 0-1.458 0-1.94.04-.477.039-.792.114-1.051.246A2.63 2.63 0 0 0 5.66 4.81c-.132.259-.208.574-.247 1.051-.04.482-.039 1.094-.039 1.94v11.09l.003.658c.003.186.01.34.021.473.025.267.07.37.106.418a.63.63 0 0 0 .472.243c.059.002.168-.022.4-.158.23-.133.52-.34.935-.636l3.54-2.529c.308-.22.543-.396.81-.464.222-.056.454-.056.676 0 .267.068.5.244.81.464l3.54 2.529c.414.296.704.503.933.636.233.137.343.16.402.158a.63.63 0 0 0 .472-.243c.036-.048.081-.15.106-.419.024-.263.024-.62.024-1.13V7.8c0-.846 0-1.458-.04-1.94-.039-.477-.114-.792-.246-1.051A2.63 2.63 0 0 0 17.19 3.66c-.259-.132-.575-.207-1.051-.246-.482-.04-1.094-.04-1.94-.04H9.8Zm4.056 4.238a.375.375 0 0 1 .53.53L12.53 10l1.857 1.856a.375.375 0 0 1-.53.53L12 10.53l-1.856 1.857a.375.375 0 0 1-.53-.53L11.47 10 9.613 8.144a.375.375 0 0 1 .53-.53L12 9.47l1.856-1.857Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#006AFF" d="M16 2.5a4 4 0 0 1 4 4v12.995c0 1.62-1.825 2.567-3.15 1.636l-3.7-2.6a2 2 0 0 0-2.3 0l-3.7 2.6C5.825 22.062 4 21.115 4 19.495V6.5a4 4 0 0 1 4-4h8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 250 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.591 21.806h.002l.001-.002.006-.004.018-.014a10 10 0 0 0 .304-.235 26 26 0 0 0 3.333-3.196C18.048 16.29 20 13.305 20 10a8 8 0 1 0-16 0c0 3.305 1.952 6.29 3.745 8.355a26 26 0 0 0 3.333 3.196 16 16 0 0 0 .304.235l.018.014.006.004.002.002a1 1 0 0 0 1.183 0Zm-.593-9.306a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2a8 8 0 0 1 8 8c0 3.305-1.953 6.29-3.745 8.355a26 26 0 0 1-3.333 3.197q-.152.12-.237.184l-.067.05-.018.014-.005.004-.002.002h-.001c-.003-.004-.042-.055-.592-.806l.592.807a1 1 0 0 1-1.184 0v-.001l-.003-.002-.005-.004-.018-.014-.067-.05a24 24 0 0 1-1.066-.877 26 26 0 0 1-2.504-2.503C5.953 16.29 4 13.305 4 10a8 8 0 0 1 8-8Zm0 2a6 6 0 0 0-6 6c0 2.56 1.547 5.076 3.255 7.044A24 24 0 0 0 12 19.723a24 24 0 0 0 2.745-2.679C16.453 15.076 18 12.56 18 10a6 6 0 0 0-6-6Zm-.002 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm0 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"/></svg>
|
||||
|
After Width: | Height: | Size: 656 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#080B12" d="M20.002 7a2 2 0 0 0-2-2h-12a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v1.918l3.375-2.7a1 1 0 0 1 .625-.218h5a2 2 0 0 0 2-2V7Zm2 8a4 4 0 0 1-4 4h-4.648l-4.727 3.781A1.001 1.001 0 0 1 7.002 22v-3h-1a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 355 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#080B12" d="M22.002 15a4 4 0 0 1-4 4h-4.648l-4.727 3.781A1.001 1.001 0 0 1 7.002 22v-3h-1a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 114 KiB |
@@ -1,29 +1,62 @@
|
||||
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'
|
||||
|
||||
async function main() {
|
||||
const env = readEnv()
|
||||
const cfg = envToCfg(env)
|
||||
if (cfg.db.migrationUrl) {
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.migrationUrl,
|
||||
schema: cfg.db.schema,
|
||||
try {
|
||||
httpLogger.info('Starting blink service')
|
||||
|
||||
const env = readEnv()
|
||||
const cfg = envToCfg(env)
|
||||
|
||||
httpLogger.info(
|
||||
{
|
||||
port: cfg.service.port,
|
||||
safelinkEnabled: cfg.service.safelinkEnabled,
|
||||
hasDbUrl: !!cfg.db.url,
|
||||
hasDbMigrationUrl: !!cfg.db.migrationUrl,
|
||||
},
|
||||
'Configuration loaded',
|
||||
)
|
||||
|
||||
if (cfg.db.migrationUrl) {
|
||||
httpLogger.info('Running database migrations')
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.migrationUrl,
|
||||
schema: cfg.db.schema,
|
||||
})
|
||||
await migrateDb.migrateToLatestOrThrow()
|
||||
await migrateDb.close()
|
||||
httpLogger.info('Database migrations completed')
|
||||
}
|
||||
|
||||
httpLogger.info('Creating LinkService')
|
||||
const link = await LinkService.create(cfg)
|
||||
|
||||
if (link.ctx.cfg.service.safelinkEnabled) {
|
||||
httpLogger.info('Starting Safelink client')
|
||||
link.ctx.safelinkClient.runFetchEvents()
|
||||
}
|
||||
|
||||
await link.start()
|
||||
httpLogger.info('Link service is running')
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
httpLogger.info('Link service is stopping')
|
||||
await link.destroy()
|
||||
httpLogger.info('Link service is stopped')
|
||||
})
|
||||
await migrateDb.migrateToLatestOrThrow()
|
||||
await migrateDb.close()
|
||||
} catch (error) {
|
||||
httpLogger.error(
|
||||
{
|
||||
error: String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
'Failed to start blink service',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const link = await LinkService.create(cfg)
|
||||
|
||||
if (link.ctx.cfg.service.safelinkEnabled) {
|
||||
link.ctx.safelinkClient.runFetchEvents()
|
||||
}
|
||||
|
||||
await link.start()
|
||||
httpLogger.info('link service is running')
|
||||
process.on('SIGTERM', async () => {
|
||||
httpLogger.info('link service is stopping')
|
||||
await link.destroy()
|
||||
httpLogger.info('link service is stopped')
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
main().catch(error => {
|
||||
console.error('Unhandled startup error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -34,6 +34,16 @@ export class Database {
|
||||
|
||||
static postgres(opts: PgOptions): Database {
|
||||
const {schema, url, txLockNonce} = opts
|
||||
log.info(
|
||||
{
|
||||
schema,
|
||||
poolSize: opts.poolSize,
|
||||
poolMaxUses: opts.poolMaxUses,
|
||||
poolIdleTimeoutMs: opts.poolIdleTimeoutMs,
|
||||
},
|
||||
'Creating database connection',
|
||||
)
|
||||
|
||||
const pool =
|
||||
opts.pool ??
|
||||
new Pg.Pool({
|
||||
|
||||
@@ -14,13 +14,16 @@
|
||||
"@atproto/common": "^0.4.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"express": "^4.19.2",
|
||||
"express-prom-bundle": "^7.0.0",
|
||||
"http-terminator": "^3.2.0",
|
||||
"pino": "^9.2.0",
|
||||
"prom-client": "^15.1.3",
|
||||
"react": "^18.3.1",
|
||||
"satori": "^0.10.13",
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
|
||||
@@ -6,6 +6,7 @@ export type Config = {
|
||||
|
||||
export type ServiceConfig = {
|
||||
port: number
|
||||
metricsPort: number
|
||||
version?: string
|
||||
appviewUrl: string
|
||||
originVerify?: string
|
||||
@@ -13,6 +14,7 @@ export type ServiceConfig = {
|
||||
|
||||
export type Environment = {
|
||||
port?: number
|
||||
metricsPort?: number
|
||||
version?: string
|
||||
appviewUrl?: string
|
||||
originVerify?: string
|
||||
@@ -21,6 +23,7 @@ export type Environment = {
|
||||
export const readEnv = (): Environment => {
|
||||
return {
|
||||
port: envInt('CARD_PORT'),
|
||||
metricsPort: envInt('CARD_METRICS_PORT'),
|
||||
version: envStr('CARD_VERSION'),
|
||||
appviewUrl: envStr('CARD_APPVIEW_URL'),
|
||||
originVerify: envStr('CARD_ORIGIN_VERIFY'),
|
||||
@@ -30,6 +33,7 @@ export const readEnv = (): Environment => {
|
||||
export const envToCfg = (env: Environment): Config => {
|
||||
const serviceCfg: ServiceConfig = {
|
||||
port: env.port ?? 3000,
|
||||
metricsPort: env.metricsPort ?? 3001,
|
||||
version: env.version,
|
||||
appviewUrl: env.appviewUrl ?? 'https://api.bsky.app',
|
||||
originVerify: env.originVerify,
|
||||
|
||||
@@ -2,7 +2,9 @@ import events from 'node:events'
|
||||
import type http from 'node:http'
|
||||
|
||||
import express from 'express'
|
||||
import promBundle from 'express-prom-bundle'
|
||||
import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
|
||||
import {register} from 'prom-client'
|
||||
|
||||
import {type Config} from './config.js'
|
||||
import {AppContext} from './context.js'
|
||||
@@ -13,7 +15,9 @@ export * from './logger.js'
|
||||
|
||||
export class CardService {
|
||||
public server?: http.Server
|
||||
public metricsServer?: http.Server
|
||||
private terminator?: HttpTerminator
|
||||
private metricsTerminator?: HttpTerminator
|
||||
|
||||
constructor(
|
||||
public app: express.Application,
|
||||
@@ -24,6 +28,30 @@ export class CardService {
|
||||
let app = express()
|
||||
|
||||
const ctx = await AppContext.fromConfig(cfg)
|
||||
|
||||
// Add Prometheus middleware for automatic HTTP instrumentation
|
||||
const metricsMiddleware = promBundle({
|
||||
includeMethod: true,
|
||||
includePath: true,
|
||||
includeStatusCode: true,
|
||||
includeUp: true,
|
||||
promClient: {
|
||||
collectDefaultMetrics: {},
|
||||
},
|
||||
|
||||
autoregister: false,
|
||||
normalizePath: req => {
|
||||
// If we have a matched route, use its path (with :params) instead of the full URL path
|
||||
if (req.route) {
|
||||
return req.route.path
|
||||
}
|
||||
|
||||
// Group all unmatched paths together to reduce cardinality
|
||||
return '<unmatched>'
|
||||
},
|
||||
})
|
||||
app.use(metricsMiddleware)
|
||||
|
||||
app = routes(ctx, app)
|
||||
app.use(errorHandler)
|
||||
|
||||
@@ -31,14 +59,49 @@ export class CardService {
|
||||
}
|
||||
|
||||
async start() {
|
||||
// Start main application server
|
||||
this.server = this.app.listen(this.ctx.cfg.service.port)
|
||||
this.server.keepAliveTimeout = 90000
|
||||
this.terminator = createHttpTerminator({server: this.server})
|
||||
this.terminator = createHttpTerminator({
|
||||
server: this.server,
|
||||
gracefulTerminationTimeout: 15000, // 15s timeout for in-flight requests
|
||||
})
|
||||
await events.once(this.server, 'listening')
|
||||
|
||||
// Start separate metrics server
|
||||
const metricsApp = express()
|
||||
metricsApp.get('/metrics', async (_req, res) => {
|
||||
res.set('Content-Type', register.contentType)
|
||||
res.end(await register.metrics())
|
||||
})
|
||||
|
||||
this.metricsServer = metricsApp.listen(this.ctx.cfg.service.metricsPort)
|
||||
this.metricsTerminator = createHttpTerminator({
|
||||
server: this.metricsServer,
|
||||
gracefulTerminationTimeout: 2000, // 2s timeout for metrics server
|
||||
})
|
||||
await events.once(this.metricsServer, 'listening')
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
const startTime = Date.now()
|
||||
|
||||
this.ctx.abortController.abort()
|
||||
await this.terminator?.terminate()
|
||||
|
||||
const shutdownPromises = []
|
||||
|
||||
if (this.terminator) {
|
||||
shutdownPromises.push(this.terminator.terminate())
|
||||
}
|
||||
|
||||
if (this.metricsTerminator) {
|
||||
shutdownPromises.push(this.metricsTerminator.terminate())
|
||||
}
|
||||
|
||||
await Promise.all(shutdownPromises)
|
||||
|
||||
const elapsed = Date.now() - startTime
|
||||
const {httpLogger} = await import('./logger.js')
|
||||
httpLogger.info(`Graceful shutdown completed in ${elapsed}ms`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,11 @@
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@opentelemetry/api@^1.4.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
|
||||
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
|
||||
|
||||
"@resvg/resvg-js-android-arm-eabi@2.6.2":
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz#e761e0b688127db64879f455178c92468a9aeabe"
|
||||
@@ -229,6 +234,58 @@
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"
|
||||
integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@*":
|
||||
version "3.4.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
|
||||
integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/express-serve-static-core@^4.17.33":
|
||||
version "4.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267"
|
||||
integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express@^4.17.21":
|
||||
version "4.17.23"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef"
|
||||
integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^4.17.33"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/http-errors@*":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
|
||||
integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
|
||||
integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
|
||||
|
||||
"@types/node@*":
|
||||
version "24.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.5.2.tgz#52ceb83f50fe0fcfdfbd2a9fab6db2e9e7ef6446"
|
||||
integrity sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==
|
||||
dependencies:
|
||||
undici-types "~7.12.0"
|
||||
|
||||
"@types/node@^20.14.3":
|
||||
version "20.14.3"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-20.14.3.tgz"
|
||||
@@ -236,6 +293,33 @@
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
|
||||
integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/send@*":
|
||||
version "0.17.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74"
|
||||
integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==
|
||||
dependencies:
|
||||
"@types/mime" "^1"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.15.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877"
|
||||
integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==
|
||||
dependencies:
|
||||
"@types/http-errors" "*"
|
||||
"@types/node" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
|
||||
@@ -288,6 +372,11 @@ base64-js@^1.3.1:
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
bintrees@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8"
|
||||
integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==
|
||||
|
||||
body-parser@1.20.2:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz"
|
||||
@@ -306,6 +395,24 @@ body-parser@1.20.2:
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
body-parser@1.20.3:
|
||||
version "1.20.3"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
|
||||
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.5"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.4.24"
|
||||
on-finished "2.4.1"
|
||||
qs "6.13.0"
|
||||
raw-body "2.5.2"
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
boolean@^3.1.4:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz"
|
||||
@@ -324,6 +431,14 @@ bytes@3.1.2:
|
||||
resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz"
|
||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||
|
||||
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
call-bind@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz"
|
||||
@@ -335,6 +450,14 @@ call-bind@^1.0.7:
|
||||
get-intrinsic "^1.2.4"
|
||||
set-function-length "^1.2.1"
|
||||
|
||||
call-bound@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
|
||||
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
get-intrinsic "^1.3.0"
|
||||
|
||||
camelize@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz"
|
||||
@@ -393,6 +516,11 @@ cookie@0.6.0:
|
||||
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
|
||||
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||
|
||||
cookie@0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
|
||||
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
@@ -463,6 +591,15 @@ diff@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||
@@ -478,6 +615,11 @@ encodeurl@~1.0.2:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
|
||||
encodeurl@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
es-define-property@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz"
|
||||
@@ -485,11 +627,23 @@ es-define-property@^1.0.0:
|
||||
dependencies:
|
||||
get-intrinsic "^1.2.4"
|
||||
|
||||
es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
escape-html@^1.0.3, escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
|
||||
@@ -510,6 +664,53 @@ events@^3.3.0:
|
||||
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
|
||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||
|
||||
express-prom-bundle@^7.0.0:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/express-prom-bundle/-/express-prom-bundle-7.0.2.tgz#73a83b9033639dae81c78906a9df78844f3c93fa"
|
||||
integrity sha512-ffFV4HGHvCKnkNJFqm42sYztRJE5mLgOj8MpGey1HOatuFhtcwXoBD2m5gca7ZbcyjkIf7lOH5ZdrhlrBf0sGw==
|
||||
dependencies:
|
||||
"@types/express" "^4.17.21"
|
||||
express "^4.18.2"
|
||||
on-finished "^2.3.0"
|
||||
url-value-parser "^2.0.0"
|
||||
|
||||
express@^4.18.2:
|
||||
version "4.21.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
|
||||
integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.20.3"
|
||||
content-disposition "0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.7.1"
|
||||
cookie-signature "1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
encodeurl "~2.0.0"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
finalhandler "1.3.1"
|
||||
fresh "0.5.2"
|
||||
http-errors "2.0.0"
|
||||
merge-descriptors "1.0.3"
|
||||
methods "~1.1.2"
|
||||
on-finished "2.4.1"
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "0.1.12"
|
||||
proxy-addr "~2.0.7"
|
||||
qs "6.13.0"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.2.1"
|
||||
send "0.19.0"
|
||||
serve-static "1.16.2"
|
||||
setprototypeof "1.2.0"
|
||||
statuses "2.0.1"
|
||||
type-is "~1.6.18"
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
express@^4.19.2:
|
||||
version "4.19.2"
|
||||
resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz"
|
||||
@@ -577,6 +778,19 @@ finalhandler@1.2.0:
|
||||
statuses "2.0.1"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
finalhandler@1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
|
||||
integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
encodeurl "~2.0.0"
|
||||
escape-html "~1.0.3"
|
||||
on-finished "2.4.1"
|
||||
parseurl "~1.3.3"
|
||||
statuses "2.0.1"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
forwarded@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
|
||||
@@ -612,6 +826,30 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
|
||||
has-symbols "^1.0.3"
|
||||
hasown "^2.0.0"
|
||||
|
||||
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.1"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
gopd@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz"
|
||||
@@ -619,6 +857,11 @@ gopd@^1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
@@ -646,7 +889,12 @@ has-symbols@^1.0.3:
|
||||
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz"
|
||||
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
||||
|
||||
hasown@^2.0.0:
|
||||
has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
hasown@^2.0.0, hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
@@ -747,6 +995,11 @@ make-error@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||
@@ -757,6 +1010,11 @@ merge-descriptors@1.0.1:
|
||||
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
|
||||
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
|
||||
|
||||
merge-descriptors@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
|
||||
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
|
||||
|
||||
methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
|
||||
@@ -811,12 +1069,17 @@ object-inspect@^1.13.1:
|
||||
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz"
|
||||
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
|
||||
|
||||
object-inspect@^1.13.3:
|
||||
version "1.13.4"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
|
||||
integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
|
||||
|
||||
on-exit-leak-free@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz"
|
||||
integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
|
||||
|
||||
on-finished@2.4.1:
|
||||
on-finished@2.4.1, on-finished@^2.3.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz"
|
||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||
@@ -860,6 +1123,11 @@ parseurl@~1.3.3:
|
||||
resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
|
||||
path-to-regexp@0.1.12:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
|
||||
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
|
||||
|
||||
path-to-regexp@0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
||||
@@ -932,6 +1200,14 @@ process@^0.11.10:
|
||||
resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
|
||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
prom-client@^15.1.3:
|
||||
version "15.1.3"
|
||||
resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2"
|
||||
integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.4.0"
|
||||
tdigest "^0.1.1"
|
||||
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz"
|
||||
@@ -947,6 +1223,13 @@ qs@6.11.0:
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
qs@6.13.0:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
|
||||
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
|
||||
dependencies:
|
||||
side-channel "^1.0.6"
|
||||
|
||||
quick-format-unescaped@^4.0.3:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz"
|
||||
@@ -1054,6 +1337,25 @@ send@0.18.0:
|
||||
range-parser "~1.2.1"
|
||||
statuses "2.0.1"
|
||||
|
||||
send@0.19.0:
|
||||
version "0.19.0"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
|
||||
integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
fresh "0.5.2"
|
||||
http-errors "2.0.0"
|
||||
mime "1.6.0"
|
||||
ms "2.1.3"
|
||||
on-finished "2.4.1"
|
||||
range-parser "~1.2.1"
|
||||
statuses "2.0.1"
|
||||
|
||||
serve-static@1.15.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz"
|
||||
@@ -1064,6 +1366,16 @@ serve-static@1.15.0:
|
||||
parseurl "~1.3.3"
|
||||
send "0.18.0"
|
||||
|
||||
serve-static@1.16.2:
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
|
||||
integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
|
||||
dependencies:
|
||||
encodeurl "~2.0.0"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.3"
|
||||
send "0.19.0"
|
||||
|
||||
set-function-length@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz"
|
||||
@@ -1081,6 +1393,35 @@ setprototypeof@1.2.0:
|
||||
resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
side-channel-list@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
||||
integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
object-inspect "^1.13.3"
|
||||
|
||||
side-channel-map@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
|
||||
integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
|
||||
dependencies:
|
||||
call-bound "^1.0.2"
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.5"
|
||||
object-inspect "^1.13.3"
|
||||
|
||||
side-channel-weakmap@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
|
||||
integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
|
||||
dependencies:
|
||||
call-bound "^1.0.2"
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.5"
|
||||
object-inspect "^1.13.3"
|
||||
side-channel-map "^1.0.1"
|
||||
|
||||
side-channel@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz"
|
||||
@@ -1091,6 +1432,17 @@ side-channel@^1.0.4:
|
||||
get-intrinsic "^1.2.4"
|
||||
object-inspect "^1.13.1"
|
||||
|
||||
side-channel@^1.0.6:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
|
||||
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
object-inspect "^1.13.3"
|
||||
side-channel-list "^1.0.0"
|
||||
side-channel-map "^1.0.1"
|
||||
side-channel-weakmap "^1.0.2"
|
||||
|
||||
sonic-boom@^3.7.0:
|
||||
version "3.8.1"
|
||||
resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz"
|
||||
@@ -1127,6 +1479,13 @@ string_decoder@^1.3.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
tdigest@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced"
|
||||
integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==
|
||||
dependencies:
|
||||
bintrees "1.0.2"
|
||||
|
||||
thread-stream@^2.6.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz"
|
||||
@@ -1220,6 +1579,11 @@ undici-types@~5.26.4:
|
||||
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
undici-types@~7.12.0:
|
||||
version "7.12.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.12.0.tgz#15c5c7475c2a3ba30659529f5cdb4674b622fafb"
|
||||
integrity sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==
|
||||
|
||||
unicode-trie@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz"
|
||||
@@ -1238,6 +1602,11 @@ unpipe@1.0.0, unpipe@~1.0.0:
|
||||
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||
|
||||
url-value-parser@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/url-value-parser/-/url-value-parser-2.2.0.tgz#f38ae8cd24604ec69bc219d66929ddbbd93a2b32"
|
||||
integrity sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==
|
||||
|
||||
utils-merge@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
|
||||
|
||||
@@ -313,7 +313,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/search", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebFeed)
|
||||
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/labeler/liked-by", server.WebGeneric)
|
||||
|
||||
@@ -331,6 +331,9 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/starter-pack-short/:code", server.WebGeneric)
|
||||
e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
|
||||
|
||||
// bookmarks
|
||||
e.GET("/saved", server.WebGeneric)
|
||||
|
||||
// ipcc
|
||||
e.GET("/ipcc", server.WebIpCC)
|
||||
|
||||
@@ -391,6 +394,7 @@ func (srv *Server) Shutdown() error {
|
||||
func (srv *Server) NewTemplateContext() pongo2.Context {
|
||||
return pongo2.Context{
|
||||
"staticCDNHost": srv.cfg.staticCDNHost,
|
||||
"favicon": fmt.Sprintf("%s/static/favicon.png", srv.cfg.staticCDNHost),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,20 +489,26 @@ func (srv *Server) WebPost(c echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
if !unauthedViewingOkay {
|
||||
// Provide minimal OpenGraph data for auth-required posts
|
||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||
data["requiresAuth"] = true
|
||||
data["profileHandle"] = pv.Handle
|
||||
if pv.DisplayName != nil {
|
||||
data["profileDisplayName"] = *pv.DisplayName
|
||||
}
|
||||
return c.Render(http.StatusOK, "post.html", data)
|
||||
}
|
||||
did := pv.Did
|
||||
data["did"] = did
|
||||
|
||||
// then fetch the post thread (with extra context)
|
||||
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
|
||||
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", pv.Did, rkey)
|
||||
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri)
|
||||
if err != nil {
|
||||
log.Warnf("failed to fetch post: %s\t%v", uri, err)
|
||||
return c.Render(http.StatusOK, "post.html", data)
|
||||
}
|
||||
req := c.Request()
|
||||
|
||||
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
|
||||
data["postView"] = postView
|
||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||
@@ -590,16 +600,70 @@ func (srv *Server) WebProfile(c echo.Context) error {
|
||||
unauthedViewingOkay = false
|
||||
}
|
||||
}
|
||||
if !unauthedViewingOkay {
|
||||
return c.Render(http.StatusOK, "profile.html", data)
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
data["profileView"] = pv
|
||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||
data["requestHost"] = req.Host
|
||||
|
||||
if !unauthedViewingOkay {
|
||||
data["requiresAuth"] = true
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "profile.html", data)
|
||||
}
|
||||
|
||||
func (srv *Server) WebFeed(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
data := srv.NewTemplateContext()
|
||||
|
||||
// sanity check arguments. don't 4xx, just let app handle if not expected format
|
||||
rkeyParam := c.Param("rkey")
|
||||
rkey, err := syntax.ParseRecordKey(rkeyParam)
|
||||
if err != nil {
|
||||
return c.Render(http.StatusOK, "feed.html", data)
|
||||
}
|
||||
handleOrDIDParam := c.Param("handleOrDID")
|
||||
handleOrDID, err := syntax.ParseAtIdentifier(handleOrDIDParam)
|
||||
if err != nil {
|
||||
return c.Render(http.StatusOK, "feed.html", data)
|
||||
}
|
||||
|
||||
identifier := handleOrDID.Normalize().String()
|
||||
|
||||
// requires two fetches: first fetch profile to get DID
|
||||
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier)
|
||||
if err != nil {
|
||||
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
|
||||
return c.Render(http.StatusOK, "feed.html", data)
|
||||
}
|
||||
unauthedViewingOkay := true
|
||||
for _, label := range pv.Labels {
|
||||
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
||||
unauthedViewingOkay = false
|
||||
}
|
||||
}
|
||||
|
||||
if !unauthedViewingOkay {
|
||||
return c.Render(http.StatusOK, "feed.html", data)
|
||||
}
|
||||
did := pv.Did
|
||||
data["did"] = did
|
||||
|
||||
// then fetch the feed generator
|
||||
feedURI := fmt.Sprintf("at://%s/app.bsky.feed.generator/%s", did, rkey)
|
||||
fgv, err := appbsky.FeedGetFeedGenerator(ctx, srv.xrpcc, feedURI)
|
||||
if err != nil {
|
||||
log.Warnf("failed to fetch feed generator: %s\t%v", feedURI, err)
|
||||
return c.Render(http.StatusOK, "feed.html", data)
|
||||
}
|
||||
req := c.Request()
|
||||
data["feedView"] = fgv.View
|
||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||
|
||||
return c.Render(http.StatusOK, "feed.html", data)
|
||||
}
|
||||
|
||||
type IPCCRequest struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@
|
||||
<meta name="theme-color">
|
||||
<meta name="application-name" content="Bluesky">
|
||||
<meta name="generator" content="bskyweb">
|
||||
<meta property="og:site_name" content="Bluesky Social" />
|
||||
<meta property="og:site_name" content="Bluesky Social">
|
||||
<meta property="og:logo" content="{{ favicon }}">
|
||||
<meta name="twitter:site" content="@bluesky" />
|
||||
<link type="application/activity+json" href="" />
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head_title %}
|
||||
{%- if feedView -%}
|
||||
{{ feedView.DisplayName }} by @{{ feedView.Creator.Handle }} | Bluesky Feed
|
||||
{%- else -%}
|
||||
Bluesky
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block html_head_extra -%}
|
||||
{%- if feedView -%}
|
||||
<meta property="og:site_name" content="Bluesky Social">
|
||||
<meta property="og:type" content="website">
|
||||
{%- if requestURI %}
|
||||
<meta property="og:url" content="{{ requestURI }}">
|
||||
<link rel="canonical" href="{{ requestURI|canonicalize_url }}" />
|
||||
{% endif -%}
|
||||
|
||||
{%- if feedView.DisplayName %}
|
||||
<meta property="og:title" content="{{ feedView.DisplayName }} by @{{ feedView.Creator.Handle }}">
|
||||
{% else %}
|
||||
<meta property="og:title" content="Feed by @{{ feedView.Creator.Handle }}">
|
||||
{% endif -%}
|
||||
|
||||
{%- if feedView.Description %}
|
||||
<meta name="description" content="{{ feedView.Description }}">
|
||||
<meta property="og:description" content="{{ feedView.Description }}">
|
||||
<meta property="twitter:description" content="{{ feedView.Description }}">
|
||||
{% endif -%}
|
||||
|
||||
{%- if feedView.Avatar %}
|
||||
<meta property="og:image" content="{{ feedView.Avatar }}">
|
||||
<meta property="twitter:image" content="{{ feedView.Avatar }}">
|
||||
<meta name="twitter:card" content="summary">
|
||||
{% endif %}
|
||||
|
||||
<meta name="twitter:label1" content="Created by">
|
||||
<meta name="twitter:value1" content="@{{ feedView.Creator.Handle }}">
|
||||
|
||||
<link rel="alternate" href="{{ feedView.Uri }}" />
|
||||
{% endif -%}
|
||||
{%- endblock %}
|
||||
|
||||
{% block noscript_extra -%}
|
||||
{%- if feedView -%}
|
||||
<div id="bsky_feed_summary">
|
||||
<h3>Feed</h3>
|
||||
<p id="bsky_feed_name">{{ feedView.DisplayName }}</p>
|
||||
<p id="bsky_feed_creator">{{ feedView.Creator.Handle }}</p>
|
||||
<p id="bsky_feed_description">{{ feedView.Description }}</p>
|
||||
</div>
|
||||
{% endif -%}
|
||||
{%- endblock %}
|
||||
@@ -3,6 +3,8 @@
|
||||
{% block head_title %}
|
||||
{%- if postView -%}
|
||||
@{{ postView.Author.Handle }} on Bluesky
|
||||
{%- elif requiresAuth and profileHandle -%}
|
||||
@{{ profileHandle }} on Bluesky
|
||||
{%- else -%}
|
||||
Bluesky
|
||||
{%- endif -%}
|
||||
@@ -11,7 +13,7 @@
|
||||
{% block html_head_extra -%}
|
||||
{%- if postView -%}
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="profile:username" content="{{ profileView.Handle }}">
|
||||
<meta property="profile:username" content="{{ postView.Author.Handle }}">
|
||||
{%- if requestURI %}
|
||||
<meta property="og:url" content="{{ requestURI }}">
|
||||
<link rel="canonical" href="{{ requestURI|canonicalize_url }}" />
|
||||
@@ -32,17 +34,79 @@
|
||||
<meta property="twitter:image" content="{{ imgThumbUrl }}">
|
||||
{% endfor %}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{%- elif postView.Author.Avatar %}
|
||||
<meta name="twitter:card" content="summary">
|
||||
{% else %}
|
||||
<meta property="og:image" content="{{ postView.Author.Avatar }}">
|
||||
<meta property="twitter:image" content="{{ postView.Author.Avatar }}">
|
||||
<meta name="twitter:card" content="summary">
|
||||
{% endif %}
|
||||
<meta name="twitter:label1" content="Posted At">
|
||||
<meta name="twitter:value1" content="{{ postView.IndexedAt }}">
|
||||
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
|
||||
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
|
||||
{%- if postView.LikeCount %}
|
||||
<meta name="twitter:label2" content="Likes">
|
||||
<meta name="twitter:value2" content="{{ postView.LikeCount }}">
|
||||
{% endif -%}
|
||||
{%- if postView.ReplyCount %}
|
||||
<meta name="twitter:label3" content="Replies">
|
||||
<meta name="twitter:value3" content="{{ postView.ReplyCount }}">
|
||||
{% endif -%}
|
||||
{%- if postView.RepostCount %}
|
||||
<meta name="twitter:label4" content="Reposts">
|
||||
<meta name="twitter:value4" content="{{ postView.RepostCount }}">
|
||||
{% endif -%}
|
||||
<meta property="article:published_time" content="{{ postView.IndexedAt }}">
|
||||
<link rel="alternate" type="application/json+oembed" href="https://embed.bsky.app/oembed?format=json&url={{ postView.Uri | urlencode }}" />
|
||||
<link rel="alternate" href="{{ postView.Uri }}" />
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "DiscussionForumPosting",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "{{ postView.Author.DisplayName }}",
|
||||
"url": "https://bsky.app/profile/{{ postView.Author.Handle }}"
|
||||
},
|
||||
{%- if postText %}
|
||||
"text": "{{ postText }}",
|
||||
{% endif %}
|
||||
{%- if imageThumbUrls %}
|
||||
"image": "{{ imageThumbUrls[0] }}",
|
||||
{% endif %}
|
||||
"datePublished": "{{ postView.IndexedAt }}",
|
||||
"interactionStatistic": [
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/LikeAction",
|
||||
"userInteractionCount": {{ postView.LikeCount }}
|
||||
},
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/CommentAction",
|
||||
"userInteractionCount": {{ postView.ReplyCount }}
|
||||
},
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/ShareAction",
|
||||
"userInteractionCount": {{ postView.RepostCount + postView.QuoteCount }}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{%- elif requiresAuth and profileHandle -%}
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="profile:username" content="{{ profileHandle }}">
|
||||
{%- if requestURI %}
|
||||
<meta property="og:url" content="{{ requestURI }}">
|
||||
<link rel="canonical" href="{{ requestURI|canonicalize_url }}" />
|
||||
{% endif -%}
|
||||
{%- if profileDisplayName %}
|
||||
<meta property="og:title" content="{{ profileDisplayName }} (@{{ profileHandle }})">
|
||||
{% else %}
|
||||
<meta property="og:title" content="@{{ profileHandle }}">
|
||||
{% endif -%}
|
||||
<meta name="description" content="This post requires authentication to view.">
|
||||
<meta property="og:description" content="This post requires authentication to view.">
|
||||
<meta property="twitter:description" content="This post requires authentication to view.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
{% endif -%}
|
||||
{%- endblock %}
|
||||
|
||||
@@ -56,5 +120,11 @@
|
||||
<p id="bsky_post_text">{{ postText }}</p>
|
||||
<p id="bsky_post_indexedat">{{ postView.IndexedAt }}</p>
|
||||
</div>
|
||||
{%- elif requiresAuth and profileHandle -%}
|
||||
<div id="bsky_post_summary">
|
||||
<h3>Post</h3>
|
||||
<p id="bsky_handle">{{ profileHandle }}</p>
|
||||
<p id="bsky_post_text">This post requires authentication to view.</p>
|
||||
</div>
|
||||
{% endif -%}
|
||||
{%- endblock %}
|
||||
|
||||
@@ -9,36 +9,83 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block html_head_extra -%}
|
||||
{%- if profileView -%}
|
||||
<meta property="og:site_name" content="Bluesky Social">
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="profile:username" content="{{ profileView.Handle }}">
|
||||
{%- if requestURI %}
|
||||
{%- if requestURI %}
|
||||
<meta property="og:url" content="{{ requestURI }}">
|
||||
<link rel="canonical" href="{{ requestURI|canonicalize_url }}" />
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
|
||||
{%- if profileView -%}
|
||||
<meta property="profile:username" content="{{ profileView.Handle }}">
|
||||
{%- if profileView.DisplayName %}
|
||||
<meta property="og:title" content="{{ profileView.DisplayName }} (@{{ profileView.Handle }})">
|
||||
{% else %}
|
||||
<meta property="og:title" content="{{ profileView.Handle }}">
|
||||
{% endif -%}
|
||||
{%- if profileView.Description %}
|
||||
<meta name="description" content="{{ profileView.Description }}">
|
||||
<meta property="og:description" content="{{ profileView.Description }}">
|
||||
{% endif -%}
|
||||
{%- if profileView.Banner %}
|
||||
<meta property="og:image" content="{{ profileView.Banner }}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{%- elif profileView.Avatar -%}
|
||||
{# Don't use avatar image in cards; usually looks bad #}
|
||||
<meta name="twitter:card" content="summary">
|
||||
{% endif %}
|
||||
|
||||
<link rel="alternate" href="at://{{ profileView.Did }}/app.bsky.actor.profile/self" />
|
||||
|
||||
<meta name="twitter:label1" content="Account DID">
|
||||
<meta name="twitter:value1" content="{{ profileView.Did }}">
|
||||
|
||||
{%- if requestHost %}
|
||||
<link rel="alternate" type="application/rss+xml" href="https://{{ requestHost }}/profile/{{ profileView.Did }}/rss">
|
||||
{% endif %}
|
||||
<link rel="alternate" href="at://{{ profileView.Did }}/app.bsky.actor.profile/self" />
|
||||
|
||||
{# Only show details if auth isn't required #}
|
||||
{% if not requiresAuth %}
|
||||
{%- if profileView.Description %}
|
||||
<meta name="description" content="{{ profileView.Description }}">
|
||||
<meta property="og:description" content="{{ profileView.Description }}">
|
||||
{% endif -%}
|
||||
{%- if profileView.Banner %}
|
||||
<meta property="og:image" content="{{ profileView.Banner }}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{%- elif profileView.Avatar -%}
|
||||
{# Don't use avatar image in cards; usually looks bad #}
|
||||
<meta name="twitter:card" content="summary">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<meta name="description" content="This profile requires authentication to view.">
|
||||
<meta property="og:description" content="This profile requires authentication to view.">
|
||||
<meta property="twitter:description" content="This profile requires authentication to view.">
|
||||
{% endif %}
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfilePage",
|
||||
"dateCreated": "{{ profileView.CreatedAt }}",
|
||||
"mainEntity": {
|
||||
"@type": "Person",
|
||||
"name": "{{ profileView.DisplayName }}",
|
||||
"alternateName": "@{{ profileView.Handle }}",
|
||||
"identifier": "{{ profileView.Did }}",
|
||||
"description": "{{ profileView.Description }}",
|
||||
"image": "{{ profileView.Avatar }}",
|
||||
"interactionStatistic": [
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/FollowAction",
|
||||
"userInteractionCount": {{ profileView.FollowersCount }}
|
||||
}
|
||||
],
|
||||
"agentInteractionStatistic": [
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/FollowAction",
|
||||
"userInteractionCount": {{ profileView.FollowsCount }}
|
||||
},
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/WriteAction",
|
||||
"userInteractionCount": {{ profileView.PostsCount }}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endif -%}
|
||||
{%- endblock %}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ After you do `yarn ios` and `yarn android` once, you can later just run `yarn we
|
||||
### Tips
|
||||
|
||||
- Copy the `.env.example` to `.env` and fill in any necessary tokens. (The Sentry token is NOT required; see instructions below if you want to enable Sentry.)
|
||||
- To run on the device, add `--device` to the command (e.g. `yarn android --device`). To build in production mode (slower build, faster app), also add `--variant release`.
|
||||
- To run on the device, add `--device` to the command (e.g. `yarn android --device`). To build in production mode (slower build, faster app), also add `--variant release` on Android or `--configuration Release` on iOS.
|
||||
- If you want to use Expo EAS on your own builds without ejecting from Expo, make sure to change the `owner` and `extra.eas.projectId` properties. If you do not have an Expo account, you may remove these properties.
|
||||
- `npx react-native info` Checks what has been installed.
|
||||
- If the Android simulator frequently hangs or is very sluggish, [bump its memory limit](https://stackoverflow.com/a/40068396)
|
||||
@@ -164,7 +164,6 @@ See [testing.md](./testing.md).
|
||||
- TextEncoder / TextDecoder
|
||||
- react-native-url-polyfill
|
||||
- Array#findLast (on web)
|
||||
- atob (on native)
|
||||
|
||||
### Sentry sourcemaps
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"build": {
|
||||
"base": {
|
||||
"node": "18.18.2"
|
||||
"node": "20.19.4"
|
||||
},
|
||||
"development": {
|
||||
"extends": "base",
|
||||
|
||||
@@ -33,7 +33,7 @@ jest.mock('react-native-safe-area-context', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('expo-file-system', () => ({
|
||||
jest.mock('expo-file-system/legacy', () => ({
|
||||
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
||||
deleteAsync: jest.fn(),
|
||||
createDownloadResumable: jest.fn(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ExpoModulesCore
|
||||
import React
|
||||
import UIKit
|
||||
|
||||
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ExpoModulesCore
|
||||
import React
|
||||
|
||||
// This view will be used as a native component. Make sure to inherit from `ExpoView`
|
||||
// to apply the proper styling (e.g. border radius and shadows).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.107.0",
|
||||
"version": "1.109.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -71,9 +71,10 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.16.2",
|
||||
"@atproto/api": "^0.17.0",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@bsky.app/alf": "^0.1.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
@@ -92,15 +93,13 @@
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||
"@miblanchard/react-native-slider": "^2.6.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.7.1",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-native-menu/menu": "^1.2.3",
|
||||
"@react-native-picker/picker": "2.11.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.8.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^7.3.13",
|
||||
"@react-navigation/drawer": "^7.3.12",
|
||||
"@react-navigation/native": "^7.1.9",
|
||||
"@react-navigation/native-stack": "^7.3.13",
|
||||
"@sentry/react-native": "~6.14.0",
|
||||
"@sentry/react-native": "~6.20.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
@@ -123,44 +122,43 @@
|
||||
"array.prototype.findlast": "^1.2.3",
|
||||
"await-lock": "^2.2.2",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"base64-js": "^1.5.1",
|
||||
"bcp-47": "^2.1.0",
|
||||
"bcp-47-match": "^2.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"deprecated-react-native-prop-types": "^5.0.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "53.0.11",
|
||||
"expo-application": "~6.1.4",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-build-properties": "~0.14.6",
|
||||
"expo-camera": "~16.1.8",
|
||||
"expo-clipboard": "~7.1.4",
|
||||
"expo-dev-client": "~5.2.0",
|
||||
"expo-device": "~7.1.4",
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "^2.4.0",
|
||||
"expo": "^54.0.9",
|
||||
"expo-application": "~7.0.7",
|
||||
"expo-blur": "~15.0.7",
|
||||
"expo-build-properties": "~1.0.9",
|
||||
"expo-camera": "~17.0.8",
|
||||
"expo-clipboard": "~8.0.7",
|
||||
"expo-dev-client": "~6.0.12",
|
||||
"expo-device": "~8.0.8",
|
||||
"expo-file-system": "~19.0.14",
|
||||
"expo-font": "~14.0.8",
|
||||
"expo-haptics": "~15.0.7",
|
||||
"expo-image": "~3.0.8",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "17.0.5",
|
||||
"expo-intent-launcher": "^12.1.5",
|
||||
"expo-linear-gradient": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "~16.1.5",
|
||||
"expo-media-library": "~17.1.7",
|
||||
"expo-notifications": "~0.31.3",
|
||||
"expo-screen-orientation": "~8.1.7",
|
||||
"expo-sharing": "~13.1.5",
|
||||
"expo-splash-screen": "~0.30.9",
|
||||
"expo-system-ui": "~5.0.8",
|
||||
"expo-task-manager": "~13.1.5",
|
||||
"expo-updates": "~0.28.14",
|
||||
"expo-video": "~2.2.1",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"expo-image-manipulator": "~14.0.7",
|
||||
"expo-image-picker": "~17.0.8",
|
||||
"expo-intent-launcher": "~13.0.7",
|
||||
"expo-linear-gradient": "~15.0.7",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-localization": "~17.0.7",
|
||||
"expo-location": "~19.0.7",
|
||||
"expo-media-library": "~18.2.0",
|
||||
"expo-notifications": "~0.32.11",
|
||||
"expo-screen-orientation": "~9.0.7",
|
||||
"expo-sharing": "~14.0.7",
|
||||
"expo-splash-screen": "~31.0.10",
|
||||
"expo-system-ui": "~6.0.7",
|
||||
"expo-task-manager": "~14.0.7",
|
||||
"expo-updates": "~29.0.11",
|
||||
"expo-video": "~3.0.11",
|
||||
"expo-web-browser": "~15.0.7",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"hls.js": "^1.6.2",
|
||||
@@ -178,41 +176,40 @@
|
||||
"patch-package": "^6.5.1",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"psl": "^1.9.0",
|
||||
"radix-ui": "^1.2.0",
|
||||
"react": "19.0.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.1.0",
|
||||
"react-compiler-runtime": "^19.1.0-rc.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-is": "19",
|
||||
"react-keyed-flatten-children": "^5.0.0",
|
||||
"react-native": "^0.79.3",
|
||||
"react-native-compressor": "^1.11.0",
|
||||
"react-native-date-picker": "^5.0.12",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-compressor": "^1.13.0",
|
||||
"react-native-date-picker": "^5.0.13",
|
||||
"react-native-device-attest": "^0.1.6",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.17.5",
|
||||
"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.17.5",
|
||||
"react-native-reanimated": "^3.19.1",
|
||||
"react-native-root-siblings": "^5.0.1",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "^4.11.1",
|
||||
"react-native-svg": "15.12.0",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-uitextview": "^1.4.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "^13.13.5",
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-responsive": "^10.0.1",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"sonner": "^2.0.7",
|
||||
"sonner-native": "^0.21.0",
|
||||
@@ -220,21 +217,20 @@
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
"tldts": "^6.1.46",
|
||||
"zeego": "^1.6.2",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.167",
|
||||
"@atproto/dev-env": "^0.3.172",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@expo/config-plugins": "~10.0.2",
|
||||
"@expo/config-plugins": "~54.0.1",
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/eslint-config": "^0.79.3",
|
||||
"@react-native/typescript-config": "^0.79.3",
|
||||
"@react-native/babel-preset": "0.81.4",
|
||||
"@react-native/eslint-config": "^0.81.4",
|
||||
"@react-native/typescript-config": "^0.81.4",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
@@ -244,15 +240,15 @@
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/lodash.shuffle": "^4.2.7",
|
||||
"@types/psl": "^1.1.1",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react-responsive": "^8.0.5",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"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-preset-expo": "~13.1.11",
|
||||
"babel-preset-expo": "~54.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-bsky-internal": "link:./eslint",
|
||||
"eslint-plugin-ft-flow": "^2.0.3",
|
||||
@@ -266,7 +262,7 @@
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~53.0.7",
|
||||
"jest-expo": "~54.0.12",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
@@ -276,15 +272,14 @@
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript": "^5.9.2",
|
||||
"webpack-bundle-analyzer": "^4.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@expo/image-utils": "0.6.3",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"@react-native/babel-preset": "0.81.4",
|
||||
"@react-native/normalize-colors": "0.81.4",
|
||||
"**/@expo/image-utils": "0.8.7",
|
||||
"**/expo-constants": "18.0.8",
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/zod": "3.23.8",
|
||||
"**/multiformats": "9.9.0"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts b/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts
|
||||
index 1c788ab..d30f330 100644
|
||||
--- a/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts
|
||||
+++ b/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts
|
||||
@@ -6,7 +6,7 @@ type Callback = (...args: any[]) => any;
|
||||
* https://gist.github.com/JakeCoxon/c7ebf6e6496f8468226fd36b596e1985
|
||||
*/
|
||||
export const useStableCallback = (callback: Callback) => {
|
||||
- const callbackRef = useRef<Callback>();
|
||||
+ const callbackRef = useRef<Callback>(undefined);
|
||||
const memoCallback = useCallback(
|
||||
(...args: any) => callbackRef.current && callbackRef.current(...args),
|
||||
[]
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt b/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
|
||||
index 4ed2307..ede1181 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
|
||||
@@ -54,7 +54,7 @@ class PasteTextInputManager(context: ReactApplicationContext) : ReactTextInputMa
|
||||
}
|
||||
|
||||
override fun getExportedCustomBubblingEventTypeConstants(): MutableMap<String, Any> {
|
||||
- val map = super.getExportedCustomBubblingEventTypeConstants()!!
|
||||
+ val map = super.getExportedCustomBubblingEventTypeConstants().toMutableMap()
|
||||
map["onPaste"] = MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onPaste")
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
index c282ade..8777755 100755
|
||||
--- a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
+++ b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
@@ -215,7 +215,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) {
|
||||
|
||||
const isHermes = assets.find(asset => asset.endsWith('.hbc'));
|
||||
const windowsCallback = process.platform === "win32" ? 'node ' : '';
|
||||
- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, {
|
||||
+ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} --dist ${process.env.SENTRY_DIST}`, {
|
||||
env: {
|
||||
...process.env,
|
||||
[SENTRY_PROJECT]: sentryProject,
|
||||
@@ -0,0 +1,33 @@
|
||||
diff --git a/node_modules/@sentry/react-native/dist/js/tools/sentryMetroSerializer.js b/node_modules/@sentry/react-native/dist/js/tools/sentryMetroSerializer.js
|
||||
index d7f2350..e9c0368 100644
|
||||
--- a/node_modules/@sentry/react-native/dist/js/tools/sentryMetroSerializer.js
|
||||
+++ b/node_modules/@sentry/react-native/dist/js/tools/sentryMetroSerializer.js
|
||||
@@ -12,12 +12,9 @@ exports.createSentryMetroSerializer = exports.unstable_beforeAssetSerializationP
|
||||
const crypto = require("crypto");
|
||||
const utils_1 = require("./utils");
|
||||
const utils_2 = require("./vendor/metro/utils");
|
||||
-let countLines;
|
||||
-try {
|
||||
- countLines = require('metro/private/lib/countLines');
|
||||
-}
|
||||
-catch (e) {
|
||||
- countLines = require('metro/src/lib/countLines');
|
||||
+const newline = /\r\n?|\n|\u2028|\u2029/g;
|
||||
+function countLines(string) {
|
||||
+ return (string.match(newline) || []).length + 1;
|
||||
}
|
||||
const DEBUG_ID_PLACE_HOLDER = '__debug_id_place_holder__';
|
||||
const DEBUG_ID_MODULE_PATH = '__debugid__';
|
||||
diff --git a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
index b3783b5..d5e3e45 100755
|
||||
--- a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
+++ b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
@@ -218,7 +218,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) {
|
||||
|
||||
const isHermes = assets.find(asset => asset.endsWith('.hbc'));
|
||||
const windowsCallback = process.platform === "win32" ? 'node ' : '';
|
||||
- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, {
|
||||
+ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} --dist ${process.env.SENTRY_DIST}`, {
|
||||
env: {
|
||||
...process.env,
|
||||
[SENTRY_PROJECT]: sentryProject,
|
||||
@@ -0,0 +1,9 @@
|
||||
# @sentry/react-native/scripts/expo-upload-sourcemaps.js patch
|
||||
|
||||
Lets us specify the output directory for the sourcemaps via an environment variable.
|
||||
|
||||
# @sentry/react-native/dist/js/tools/sentryMetroSerializer.js patch
|
||||
|
||||
Patch of this: https://github.com/getsentry/sentry-react-native/issues/5180#issuecomment-3311772038
|
||||
|
||||
Will be fixed in an upcoming release of @sentry/react-native - remove when available.
|
||||
@@ -1,19 +0,0 @@
|
||||
diff --git a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
index f1255e8..a9b49e5 100644
|
||||
--- a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
+++ b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
@@ -112,11 +112,9 @@ class MediaLibraryModule : Module() {
|
||||
}
|
||||
|
||||
AsyncFunction("createAssetAsync") { localUri: String, albumId: String?, promise: Promise ->
|
||||
- throwUnlessPermissionsGranted {
|
||||
- withModuleScope(promise) {
|
||||
- CreateAssetWithAlbumId(context, localUri, promise, true, albumId)
|
||||
- .execute()
|
||||
- }
|
||||
+ withModuleScope(promise) {
|
||||
+ CreateAssetWithAlbumId(context, localUri, promise, true, albumId)
|
||||
+ .execute()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
index 2dc1db2..ee844e8 100644
|
||||
--- a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
+++ b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
@@ -105,7 +105,7 @@ class MediaLibraryModule : Module() {
|
||||
}
|
||||
|
||||
AsyncFunction("createAssetAsync") Coroutine { localUri: String, albumId: String? ->
|
||||
- requireSystemPermissions()
|
||||
+ // requireSystemPermissions()
|
||||
return@Coroutine createAssetWithAlbumId(context, localUri, true, albumId)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/Scro
|
||||
index d029337..0f63ea3 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
@@ -1003,6 +1003,11 @@ - (void)_adjustForMaintainVisibleContentPosition
|
||||
@@ -1038,6 +1038,11 @@ - (void)_adjustForMaintainVisibleContentPosition
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
diff --git a/node_modules/react-native-date-picker/ios/RNDatePicker.h b/node_modules/react-native-date-picker/ios/RNDatePicker.h
|
||||
index 480746e..470dc3a 100644
|
||||
--- a/node_modules/react-native-date-picker/ios/RNDatePicker.h
|
||||
+++ b/node_modules/react-native-date-picker/ios/RNDatePicker.h
|
||||
@@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_END
|
||||
#else
|
||||
#import "DatePicker.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
+#include <string>
|
||||
|
||||
@interface RNDatePicker : DatePicker
|
||||
|
||||
@@ -22,4 +23,3 @@ NS_ASSUME_NONNULL_END
|
||||
@end
|
||||
|
||||
#endif
|
||||
-
|
||||
@@ -0,0 +1,38 @@
|
||||
diff --git a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
|
||||
index efa71f9..a23c624 100644
|
||||
--- a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
|
||||
+++ b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
|
||||
@@ -124,15 +124,21 @@ export function Drawer({
|
||||
}
|
||||
onTransitionEnd?.(!open);
|
||||
});
|
||||
+ const animatingTo = useSharedValue(null)
|
||||
const toggleDrawer = React.useCallback((open, velocity) => {
|
||||
'worklet';
|
||||
|
||||
+ if (animatingTo.value === (open ? 'open' : 'close')) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
const translateX = getDrawerTranslationX(open);
|
||||
if (velocity === undefined) {
|
||||
runOnJS(onAnimationStart)(open);
|
||||
}
|
||||
touchStartX.value = 0;
|
||||
touchX.value = 0;
|
||||
+ animatingTo.value = open ? 'open' : 'close';
|
||||
translationX.value = withSpring(translateX, {
|
||||
velocity,
|
||||
stiffness: 1000,
|
||||
@@ -142,7 +148,10 @@ export function Drawer({
|
||||
restDisplacementThreshold: 0.01,
|
||||
restSpeedThreshold: 0.01,
|
||||
reduceMotion: ReduceMotion.Never
|
||||
- }, finished => runOnJS(onAnimationEnd)(open, finished));
|
||||
+ }, finished => {
|
||||
+ animatingTo.value = null;
|
||||
+ runOnJS(onAnimationEnd)(open, finished)
|
||||
+ });
|
||||
if (open) {
|
||||
runOnJS(onOpen)();
|
||||
} else {
|
||||
@@ -1,36 +0,0 @@
|
||||
diff --git a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
index 43d11b4..5bf0d32 100644
|
||||
--- a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
+++ b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
@@ -461,16 +461,23 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||||
{
|
||||
- if ([_handlersToWaitFor count]) {
|
||||
- RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
|
||||
- if (handler != nil) {
|
||||
- for (NSNumber *handlerTag in _handlersToWaitFor) {
|
||||
- if ([handler.tag isEqual:handlerTag]) {
|
||||
- return YES;
|
||||
- }
|
||||
- }
|
||||
+ RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
|
||||
+ if (handler == nil) {
|
||||
+ return NO;
|
||||
+ }
|
||||
+
|
||||
+ for (NSNumber *handlerTag in _handlersToWaitFor) {
|
||||
+ if ([handler.tag isEqual:handlerTag]) {
|
||||
+ return YES;
|
||||
}
|
||||
}
|
||||
+
|
||||
+ for (NSNumber *handlerTag in handler->_handlersThatShouldWait) {
|
||||
+ if ([_tag isEqual:handlerTag]) {
|
||||
+ return YES;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
diff --git a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
|
||||
index eae3989..432745a 100644
|
||||
--- a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
|
||||
+++ b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
|
||||
@@ -416,6 +416,10 @@ void NativeProxy::progressLayoutAnimation(
|
||||
tag, newPropsJNI, isSharedTransition);
|
||||
}
|
||||
|
||||
+void NativeProxy::endLayoutAnimation(int tag, bool shouldRemove) {
|
||||
+ layoutAnimations_->cthis()->endLayoutAnimation(tag, shouldRemove);
|
||||
+}
|
||||
+
|
||||
PlatformDepMethodsHolder NativeProxy::getPlatformDependentMethods() {
|
||||
#ifdef RCT_NEW_ARCH_ENABLED
|
||||
// nothing
|
||||
@@ -455,14 +459,7 @@ PlatformDepMethodsHolder NativeProxy::getPlatformDependentMethods() {
|
||||
auto progressLayoutAnimation =
|
||||
bindThis(&NativeProxy::progressLayoutAnimation);
|
||||
|
||||
- auto endLayoutAnimation = [weakThis = weak_from_this()](
|
||||
- int tag, bool removeView) {
|
||||
- auto strongThis = weakThis.lock();
|
||||
- if (!strongThis) {
|
||||
- return;
|
||||
- }
|
||||
- strongThis->layoutAnimations_->cthis()->endLayoutAnimation(tag, removeView);
|
||||
- };
|
||||
+ auto endLayoutAnimation = bindThis(&NativeProxy::endLayoutAnimation);
|
||||
|
||||
auto maybeFlushUiUpdatesQueueFunction =
|
||||
bindThis(&NativeProxy::maybeFlushUIUpdatesQueue);
|
||||
diff --git a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
|
||||
index 2ee2cc8..2edb5c9 100644
|
||||
--- a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
|
||||
+++ b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
|
||||
@@ -234,6 +234,8 @@ class NativeProxy : public jni::HybridClass<NativeProxy>,
|
||||
const jsi::Object &newProps,
|
||||
bool isSharedTransition);
|
||||
|
||||
+ void endLayoutAnimation(int tag, bool shouldRemove);
|
||||
+
|
||||
/***
|
||||
* Wraps a method of `NativeProxy` in a function object capturing `this`
|
||||
* @tparam TReturn return type of passed method
|
||||
@@ -0,0 +1,326 @@
|
||||
diff --git a/node_modules/react-native-screens/ios/RNSScreen.mm b/node_modules/react-native-screens/ios/RNSScreen.mm
|
||||
index b62a2e2..cb469db 100644
|
||||
--- a/node_modules/react-native-screens/ios/RNSScreen.mm
|
||||
+++ b/node_modules/react-native-screens/ios/RNSScreen.mm
|
||||
@@ -729,9 +729,26 @@ - (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingFor
|
||||
#endif
|
||||
}
|
||||
|
||||
-#if !RCT_NEW_ARCH_ENABLED
|
||||
+- (void)willMoveToWindow:(UIWindow *)newWindow
|
||||
+{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // In iOS 26, as soon as another screen appears in transition, it is interactable
|
||||
+ // To avoid glitches resulting from clicking buttons mid transition, we temporarily disable all interactions
|
||||
+ // Disabling interactions for parent navigation controller won't be enough in case of nested stack
|
||||
+ // Furthermore, a stack put inside a modal will exist in an entirely different hierarchy
|
||||
+ // To be sure, we block interactions on the whole window.
|
||||
+ // Note that newWindows is nil when moving from instead of moving to, and Obj-C handles nil correctly
|
||||
+ newWindow.userInteractionEnabled = false;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Disable interactions to disallow multiple modals dismissed at once; see willMoveToWindow
|
||||
+ presentationController.containerView.window.userInteractionEnabled = false;
|
||||
+ }
|
||||
+#if !RCT_NEW_ARCH_ENABLED
|
||||
// On Paper, we need to call both "cancel" and "reset" here because RN's gesture
|
||||
// recognizer does not handle the scenario when it gets cancelled by other top
|
||||
// level gesture recognizer. In this case by the modal dismiss gesture.
|
||||
@@ -744,8 +761,8 @@ - (void)presentationControllerWillDismiss:(UIPresentationController *)presentati
|
||||
// down.
|
||||
[_touchHandler cancel];
|
||||
[_touchHandler reset];
|
||||
-}
|
||||
#endif // !RCT_NEW_ARCH_ENABLED
|
||||
+}
|
||||
|
||||
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
@@ -757,6 +774,10 @@ - (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presenta
|
||||
|
||||
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Reenable interactions; see presentationControllerWillDismiss
|
||||
+ presentationController.containerView.window.userInteractionEnabled = true;
|
||||
+ }
|
||||
// NOTE(kkafar): We should consider depracating the use of gesture cancel here & align
|
||||
// with usePreventRemove API of react-navigation v7.
|
||||
[self notifyGestureCancel];
|
||||
@@ -767,6 +788,11 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)pr
|
||||
|
||||
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Reenable interactions; see presentationControllerWillDismiss
|
||||
+ // Dismissed screen doesn't hold a reference to window, but presentingViewController.view does
|
||||
+ presentationController.presentingViewController.view.window.userInteractionEnabled = true;
|
||||
+ }
|
||||
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
|
||||
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:) withObject:presentationController];
|
||||
}
|
||||
@@ -1518,6 +1544,10 @@ - (void)viewWillDisappear:(BOOL)animated
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Reenable interactions, see willMoveToWindow
|
||||
+ self.view.window.userInteractionEnabled = true;
|
||||
+ }
|
||||
[super viewDidAppear:animated];
|
||||
if (!_isSwiping || _shouldNotify) {
|
||||
// we are going forward or dismissing without swipe
|
||||
diff --git a/node_modules/react-native-screens/ios/RNSScreenStack.mm b/node_modules/react-native-screens/ios/RNSScreenStack.mm
|
||||
index 229dc58..10b365b 100644
|
||||
--- a/node_modules/react-native-screens/ios/RNSScreenStack.mm
|
||||
+++ b/node_modules/react-native-screens/ios/RNSScreenStack.mm
|
||||
@@ -62,26 +62,6 @@ @interface RNSScreenStackView () <
|
||||
|
||||
@implementation RNSNavigationController
|
||||
|
||||
-#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
-- (void)viewDidLoad
|
||||
-{
|
||||
- // iOS 26 introduces new gesture recognizer which replaces our RNSPanGestureRecognizer.
|
||||
- // The problem is that we are not able to handle it here for various reasons:
|
||||
- // - the new recognizer comes with its own delegate and our current approach is to wire
|
||||
- // all recognizers to RNSScreenStackView; to be 100% sure we don't break the logic,
|
||||
- // we would have to decorate its delegate and call it after our code, which would
|
||||
- // break other recognizers that the stack view is the delegate for
|
||||
- // - when RNSScreenStackView.setupGestureHandler method is called, the recognizer hasn't been
|
||||
- // loaded yet and there is no other place to configure in a not "hacky" way
|
||||
- // - the official docs warn us to not use it for anything other than "setting up failure requirements with it"
|
||||
- // - we expose fullScreenGestureEnabled prop to enable/disable the feature,
|
||||
- // so we need control over the delegate
|
||||
- if (@available(iOS 26.0, *)) {
|
||||
- self.interactiveContentPopGestureRecognizer.enabled = NO;
|
||||
- }
|
||||
-}
|
||||
-#endif // iOS 26
|
||||
-
|
||||
#if !TARGET_OS_TV
|
||||
- (UIViewController *)childViewControllerForStatusBarStyle
|
||||
{
|
||||
@@ -219,50 +199,6 @@ - (bool)onRepeatedTabSelectionOfTabScreenController:(RNSTabsScreenViewController
|
||||
return false;
|
||||
}
|
||||
|
||||
-#pragma mark - UINavigationBarDelegate
|
||||
-
|
||||
-#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
-- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
|
||||
-{
|
||||
- if (@available(iOS 26, *)) {
|
||||
- // To prevent popping multiple screens when back button is pressed repeatedly,
|
||||
- // We allow for pop operation to proceed only if no transition is in progress,
|
||||
- // which we check indirectly by checking if transitionCoordinator is set.
|
||||
- // If it's not, we are safe to proceed.
|
||||
- if (self.transitionCoordinator == nil) {
|
||||
- // We still need to disable interactions for back button so click effects are not applied,
|
||||
- // and there is unfortunately no better place for it currently
|
||||
- UIView *button = [navigationBar rnscreens_findBackButtonWrapperView];
|
||||
- if (button != nil) {
|
||||
- button.userInteractionEnabled = false;
|
||||
- }
|
||||
-
|
||||
- return true;
|
||||
- }
|
||||
-
|
||||
- return false;
|
||||
- }
|
||||
-
|
||||
- return true;
|
||||
-}
|
||||
-
|
||||
-- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item
|
||||
-{
|
||||
- if (@available(iOS 26, *)) {
|
||||
- // Reset interactions on back button -> see navigationBar:shouldPopItem
|
||||
- // IMPORTANT: This reset won't execute when preventNativeDismiss is on.
|
||||
- // However, on iOS 26, unlike in previous versions, the back button instance changes
|
||||
- // when handling preventNativeDismiss and userIteractionEnabled is reset.
|
||||
- // The instance also changes when regular screen pop happens, but in that case
|
||||
- // the value of userInteractionEnabled is carried on, and we reset it here.
|
||||
- UIView *button = [navigationBar rnscreens_findBackButtonWrapperView];
|
||||
- if (button != nil) {
|
||||
- button.userInteractionEnabled = true;
|
||||
- }
|
||||
- }
|
||||
-}
|
||||
-#endif // Check for iOS >= 26
|
||||
-
|
||||
#pragma mark - RNSFrameCorrectionProvider
|
||||
|
||||
#ifdef RNS_GAMMA_ENABLED
|
||||
@@ -327,7 +263,7 @@ @implementation RNSScreenStackView {
|
||||
UINavigationController *_controller;
|
||||
NSMutableArray<RNSScreenView *> *_reactSubviews;
|
||||
BOOL _invalidated;
|
||||
- BOOL _isFullWidthSwiping;
|
||||
+ BOOL _isFullWidthSwipingWithPanGesture; // used only for content swipe with RNSPanGestureRecognizer
|
||||
RNSPercentDrivenInteractiveTransition *_interactionController;
|
||||
__weak RNSScreenStackManager *_manager;
|
||||
BOOL _updateScheduled;
|
||||
@@ -522,6 +458,11 @@ - (void)reactAddControllerToClosestParent:(UIViewController *)controller
|
||||
[self addSubview:controller.view];
|
||||
#if !TARGET_OS_TV
|
||||
_controller.interactivePopGestureRecognizer.delegate = self;
|
||||
+ #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ _controller.interactiveContentPopGestureRecognizer.delegate = self;
|
||||
+ }
|
||||
+#endif // Check for iOS >= 26.0
|
||||
#endif
|
||||
[controller didMoveToParentViewController:parentView.reactViewController];
|
||||
// On iOS pre 12 we observed that `willShowViewController` delegate method does not always
|
||||
@@ -943,7 +884,7 @@ - (void)dismissOnReload
|
||||
// when preventing the native dismiss with back button, we have to return the animator.
|
||||
// Also, we need to return the animator when full width swiping even if the animation is not custom,
|
||||
// otherwise the screen will be just popped immediately due to no animation
|
||||
- ((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwiping ||
|
||||
+ ((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwipingWithPanGesture ||
|
||||
[RNSScreenStackAnimator isCustomAnimation:screen.stackAnimation] || _customAnimation)) {
|
||||
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
|
||||
}
|
||||
@@ -967,23 +908,39 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||||
}
|
||||
RNSScreenView *topScreen = _reactSubviews.lastObject;
|
||||
|
||||
+ BOOL customAnimationOnSwipePropSetAndSelectedAnimationIsCustom =
|
||||
+ topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation];
|
||||
+
|
||||
#if TARGET_OS_TV || TARGET_OS_VISION
|
||||
[self cancelTouchesInParent];
|
||||
return YES;
|
||||
#else
|
||||
- // RNSPanGestureRecognizer will receive events iff topScreen.fullScreenSwipeEnabled == YES;
|
||||
- // Events are filtered in gestureRecognizer:shouldReceivePressOrTouchEvent: method
|
||||
if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
|
||||
- if ([self isInGestureResponseDistance:gestureRecognizer topScreen:topScreen]) {
|
||||
- _isFullWidthSwiping = YES;
|
||||
- [self cancelTouchesInParent];
|
||||
- return YES;
|
||||
+ // On iOS < 26, we have a custom full screen swipe recognizer that functions similarily
|
||||
+ // to interactiveContentPopGestureRecognizer introduced in iOS 26.
|
||||
+ // On iOS >= 26, we want to use the native one, but we are unable to handle custom animations
|
||||
+ // with native interactiveContentPopGestureRecognizer, so we have to fallback to the old implementation.
|
||||
+ // In this case, the old one should behave as close as the new native one, having only the difference
|
||||
+ // in animation, and without any customization that is exclusive for it (e.g. gestureResponseDistance).
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ if (customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) {
|
||||
+ _isFullWidthSwipingWithPanGesture = YES;
|
||||
+ [self cancelTouchesInParent];
|
||||
+ return YES;
|
||||
+ }
|
||||
+ return NO;
|
||||
+ } else {
|
||||
+ if ([self isInGestureResponseDistance:gestureRecognizer topScreen:topScreen]) {
|
||||
+ _isFullWidthSwipingWithPanGesture = YES;
|
||||
+ [self cancelTouchesInParent];
|
||||
+ return YES;
|
||||
+ }
|
||||
+ return NO;
|
||||
}
|
||||
- return NO;
|
||||
}
|
||||
|
||||
// Now we're dealing with RNSScreenEdgeGestureRecognizer (or _UIParallaxTransitionPanGestureRecognizer)
|
||||
- if (topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation]) {
|
||||
+ if (customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) {
|
||||
if ([gestureRecognizer isKindOfClass:[RNSScreenEdgeGestureRecognizer class]]) {
|
||||
UIRectEdge edges = ((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges;
|
||||
BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
|
||||
@@ -1028,7 +985,9 @@ - (void)setupGestureHandlers
|
||||
rightEdgeSwipeGestureRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:rightEdgeSwipeGestureRecognizer];
|
||||
|
||||
- // gesture recognizer for full width swipe gesture
|
||||
+ // Starting from iOS 26, RNSPanGestureRecognizer has been mostly replaced by native
|
||||
+ // interactiveContentPopGestureRecognizer. It still needs to handle custom dismiss animations,
|
||||
+ // which we are not able to handle with the latter.
|
||||
RNSPanGestureRecognizer *panRecognizer = [[RNSPanGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleSwipe:)];
|
||||
panRecognizer.delegate = self;
|
||||
@@ -1091,7 +1050,7 @@ - (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
|
||||
[_interactionController cancelInteractiveTransition];
|
||||
}
|
||||
_interactionController = nil;
|
||||
- _isFullWidthSwiping = NO;
|
||||
+ _isFullWidthSwipingWithPanGesture = NO;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
@@ -1225,14 +1184,6 @@ - (BOOL)isScrollViewPanGestureRecognizer:(UIGestureRecognizer *)gestureRecognize
|
||||
// Be careful when adding another type of gesture recognizer.
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePressOrTouchEvent:(NSObject *)event
|
||||
{
|
||||
- if (@available(iOS 26, *)) {
|
||||
- // in iOS 26, you can swipe to pop screen before the previous one finished transitioning;
|
||||
- // this prevents from registering the second gesture
|
||||
- if ([self isTransitionInProgress]) {
|
||||
- return NO;
|
||||
- }
|
||||
- }
|
||||
-
|
||||
RNSScreenView *topScreen = _reactSubviews.lastObject;
|
||||
|
||||
for (RNSScreenView *s in _reactSubviews.reverseObjectEnumerator) {
|
||||
@@ -1249,10 +1200,30 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ BOOL customAnimationOnSwipePropSetAndSelectedAnimationIsCustom =
|
||||
+ topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation];
|
||||
+#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // On iOS 26, fullScreenSwipeEnabled takes no effect, and depending on whether custom animations are on,
|
||||
+ // we select either interactiveContentPopGestureRecognizer or RNSPanGestureRecognizer
|
||||
+ if (([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]] &&
|
||||
+ !customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) ||
|
||||
+ (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer &&
|
||||
+ customAnimationOnSwipePropSetAndSelectedAnimationIsCustom)) {
|
||||
+ return NO;
|
||||
+ }
|
||||
+ } else {
|
||||
+ // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled.
|
||||
+ if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
|
||||
+ return topScreen.fullScreenSwipeEnabled;
|
||||
+ }
|
||||
+ }
|
||||
+#else // check for iOS >= 26
|
||||
// We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled.
|
||||
if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
|
||||
return topScreen.fullScreenSwipeEnabled;
|
||||
}
|
||||
+#endif // check for iOS >= 26
|
||||
|
||||
// RNSScreenEdgeGestureRecognizer || _UIParallaxTransitionPanGestureRecognizer
|
||||
return YES;
|
||||
@@ -1268,15 +1239,6 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive
|
||||
return [self gestureRecognizer:gestureRecognizer shouldReceivePressOrTouchEvent:touch];
|
||||
}
|
||||
|
||||
-- (BOOL)isTransitionInProgress
|
||||
-{
|
||||
- if (_controller.transitionCoordinator != nil) {
|
||||
- return YES;
|
||||
- }
|
||||
-
|
||||
- return NO;
|
||||
-}
|
||||
-
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||||
{
|
||||
@@ -1289,7 +1251,6 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateBegan || isBackGesture) {
|
||||
return NO;
|
||||
}
|
||||
-
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
@@ -32,13 +32,12 @@ import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
ensureGeolocationResolved,
|
||||
beginResolveGeolocationConfig,
|
||||
ensureGeolocationConfigIsResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
|
||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
import {Provider as ModalStateProvider} from '#/state/modals'
|
||||
@@ -91,7 +90,7 @@ if (isAndroid) {
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
beginResolveGeolocation()
|
||||
beginResolveGeolocationConfig()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
@@ -203,9 +202,10 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
|
||||
setReady(true),
|
||||
)
|
||||
Promise.all([
|
||||
initPersistedState(),
|
||||
ensureGeolocationConfigIsResolved(),
|
||||
]).then(() => setReady(true))
|
||||
}, [])
|
||||
|
||||
if (!isReady) {
|
||||
@@ -224,24 +224,22 @@ function App() {
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<BottomSheetProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<InnerApp />
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</BottomSheetProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<BottomSheetProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<InnerApp />
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</BottomSheetProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
|
||||
@@ -21,12 +21,11 @@ import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
ensureGeolocationResolved,
|
||||
beginResolveGeolocationConfig,
|
||||
ensureGeolocationConfigIsResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
import {Provider as ModalStateProvider} from '#/state/modals'
|
||||
@@ -69,7 +68,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
beginResolveGeolocation()
|
||||
beginResolveGeolocationConfig()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
@@ -178,9 +177,10 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
|
||||
setReady(true),
|
||||
)
|
||||
Promise.all([
|
||||
initPersistedState(),
|
||||
ensureGeolocationConfigIsResolved(),
|
||||
]).then(() => setReady(true))
|
||||
}, [])
|
||||
|
||||
if (!isReady) {
|
||||
@@ -198,19 +198,17 @@ function App() {
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useRef} from 'react'
|
||||
import {type JSX, useCallback, useRef} from 'react'
|
||||
import {Linking} from 'react-native'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {i18n, type MessageDescriptor} from '@lingui/core'
|
||||
@@ -64,13 +64,12 @@ import {PostThreadScreen} from '#/view/screens/PostThread'
|
||||
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
|
||||
import {ProfileScreen} from '#/view/screens/Profile'
|
||||
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
|
||||
import {ProfileListScreen} from '#/view/screens/ProfileList'
|
||||
import {SavedFeeds} from '#/view/screens/SavedFeeds'
|
||||
import {Storybook} from '#/view/screens/Storybook'
|
||||
import {SupportScreen} from '#/view/screens/Support'
|
||||
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
|
||||
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
|
||||
import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
|
||||
import {BookmarksScreen} from '#/screens/Bookmarks'
|
||||
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {LogScreen} from '#/screens/Log'
|
||||
@@ -91,6 +90,8 @@ import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
|
||||
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch'
|
||||
import {ProfileListScreen} from '#/screens/ProfileList'
|
||||
import {SavedFeeds} from '#/screens/SavedFeeds'
|
||||
import {SearchScreen} from '#/screens/Search'
|
||||
import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
|
||||
import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
|
||||
@@ -600,6 +601,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Bookmarks"
|
||||
getComponent={() => BookmarksScreen}
|
||||
options={{
|
||||
title: title(msg`Saved Posts`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import Animated, {
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import Svg, {Path, SvgProps} from 'react-native-svg'
|
||||
import Svg, {Path, type SvgProps} from 'react-native-svg'
|
||||
import {Image} from 'expo-image'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe
|
||||
|
||||
const factor = 0.0625 // 1 - (15/16)
|
||||
const fontScaleMultipliers: Record<Device['fontScale'], number> = {
|
||||
'-2': 1 - factor * 3,
|
||||
'-1': 1 - factor * 2,
|
||||
'0': 1 - factor * 1, // default
|
||||
'1': 1,
|
||||
'2': 1 + factor * 1,
|
||||
'-2': 1 - factor * 1, // unused
|
||||
'-1': 1 - factor * 1,
|
||||
'0': 1, // default
|
||||
'1': 1 + factor * 1,
|
||||
'2': 1 + factor * 1, // unused
|
||||
}
|
||||
|
||||
export function computeFontScaleMultiplier(scale: Device['fontScale']) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import {type Theme, type ThemeName} from '@bsky.app/alf'
|
||||
|
||||
import {
|
||||
computeFontScaleMultiplier,
|
||||
@@ -7,16 +8,14 @@ import {
|
||||
setFontFamily as persistFontFamily,
|
||||
setFontScale as persistFontScale,
|
||||
} from '#/alf/fonts'
|
||||
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||
import {type Theme, type ThemeName} from '#/alf/types'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
import {themes} from '#/alf/themes'
|
||||
import {type Device} from '#/storage'
|
||||
|
||||
export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf'
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * from '#/alf/breakpoints'
|
||||
export * from '#/alf/fonts'
|
||||
export * as tokens from '#/alf/tokens'
|
||||
export * from '#/alf/types'
|
||||
export * from '#/alf/util/flatten'
|
||||
export * from '#/alf/util/platform'
|
||||
export * from '#/alf/util/themeSelector'
|
||||
@@ -25,7 +24,7 @@ export * from '#/alf/util/useGutters'
|
||||
export type Alf = {
|
||||
themeName: ThemeName
|
||||
theme: Theme
|
||||
themes: ReturnType<typeof createThemes>
|
||||
themes: typeof themes
|
||||
fonts: {
|
||||
scale: Exclude<Device['fontScale'], undefined>
|
||||
scaleMultiplier: number
|
||||
@@ -44,14 +43,8 @@ export type Alf = {
|
||||
*/
|
||||
export const Context = React.createContext<Alf>({
|
||||
themeName: 'light',
|
||||
theme: defaultTheme,
|
||||
themes: createThemes({
|
||||
hues: {
|
||||
primary: BLUE_HUE,
|
||||
negative: RED_HUE,
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
}),
|
||||
theme: themes.light,
|
||||
themes,
|
||||
fonts: {
|
||||
scale: getFontScale(),
|
||||
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
|
||||
@@ -76,10 +69,10 @@ export function ThemeProvider({
|
||||
const setFontScaleAndPersist = React.useCallback<
|
||||
Alf['fonts']['setFontScale']
|
||||
>(
|
||||
fontScale => {
|
||||
setFontScale(fontScale)
|
||||
persistFontScale(fontScale)
|
||||
setFontScaleMultiplier(computeFontScaleMultiplier(fontScale))
|
||||
fs => {
|
||||
setFontScale(fs)
|
||||
persistFontScale(fs)
|
||||
setFontScaleMultiplier(computeFontScaleMultiplier(fs))
|
||||
},
|
||||
[setFontScale],
|
||||
)
|
||||
@@ -89,21 +82,12 @@ export function ThemeProvider({
|
||||
const setFontFamilyAndPersist = React.useCallback<
|
||||
Alf['fonts']['setFontFamily']
|
||||
>(
|
||||
fontFamily => {
|
||||
setFontFamily(fontFamily)
|
||||
persistFontFamily(fontFamily)
|
||||
ff => {
|
||||
setFontFamily(ff)
|
||||
persistFontFamily(ff)
|
||||
},
|
||||
[setFontFamily],
|
||||
)
|
||||
const themes = React.useMemo(() => {
|
||||
return createThemes({
|
||||
hues: {
|
||||
primary: BLUE_HUE,
|
||||
negative: RED_HUE,
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo<Alf>(
|
||||
() => ({
|
||||
@@ -121,7 +105,6 @@ export function ThemeProvider({
|
||||
}),
|
||||
[
|
||||
themeName,
|
||||
themes,
|
||||
fontScale,
|
||||
setFontScaleAndPersist,
|
||||
fontFamily,
|
||||
|
||||
@@ -1,585 +1,44 @@
|
||||
import {atoms} from '#/alf/atoms'
|
||||
import {Palette, Theme} from '#/alf/types'
|
||||
import {
|
||||
BLUE_HUE,
|
||||
defaultScale,
|
||||
dimScale,
|
||||
GREEN_HUE,
|
||||
RED_HUE,
|
||||
} from '#/alf/util/colorGeneration'
|
||||
createThemes,
|
||||
DEFAULT_PALETTE,
|
||||
DEFAULT_SUBDUED_PALETTE,
|
||||
} from '@bsky.app/alf'
|
||||
|
||||
const themes = createThemes({
|
||||
hues: {
|
||||
primary: BLUE_HUE,
|
||||
negative: RED_HUE,
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
const DEFAULT_THEMES = createThemes({
|
||||
defaultPalette: DEFAULT_PALETTE,
|
||||
subduedPalette: DEFAULT_SUBDUED_PALETTE,
|
||||
})
|
||||
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const lightPalette = themes.lightPalette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const darkPalette = themes.darkPalette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const dimPalette = themes.dimPalette
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const light = themes.light
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dark = themes.dark
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dim = themes.dim
|
||||
|
||||
export const defaultTheme = themes.light
|
||||
|
||||
export function createThemes({
|
||||
hues,
|
||||
}: {
|
||||
hues: {
|
||||
primary: number
|
||||
negative: number
|
||||
positive: number
|
||||
}
|
||||
}): {
|
||||
lightPalette: Palette
|
||||
darkPalette: Palette
|
||||
dimPalette: Palette
|
||||
light: Theme
|
||||
dark: Theme
|
||||
dim: Theme
|
||||
} {
|
||||
const color = {
|
||||
like: '#ec4899',
|
||||
trueBlack: '#000000',
|
||||
|
||||
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
|
||||
gray_25: `hsl(${hues.primary}, 20%, ${defaultScale[13]}%)`,
|
||||
gray_50: `hsl(${hues.primary}, 20%, ${defaultScale[12]}%)`,
|
||||
gray_100: `hsl(${hues.primary}, 20%, ${defaultScale[11]}%)`,
|
||||
gray_200: `hsl(${hues.primary}, 20%, ${defaultScale[10]}%)`,
|
||||
gray_300: `hsl(${hues.primary}, 20%, ${defaultScale[9]}%)`,
|
||||
gray_400: `hsl(${hues.primary}, 20%, ${defaultScale[8]}%)`,
|
||||
gray_500: `hsl(${hues.primary}, 20%, ${defaultScale[7]}%)`,
|
||||
gray_600: `hsl(${hues.primary}, 24%, ${defaultScale[6]}%)`,
|
||||
gray_700: `hsl(${hues.primary}, 24%, ${defaultScale[5]}%)`,
|
||||
gray_800: `hsl(${hues.primary}, 28%, ${defaultScale[4]}%)`,
|
||||
gray_900: `hsl(${hues.primary}, 28%, ${defaultScale[3]}%)`,
|
||||
gray_950: `hsl(${hues.primary}, 28%, ${defaultScale[2]}%)`,
|
||||
gray_975: `hsl(${hues.primary}, 28%, ${defaultScale[1]}%)`,
|
||||
gray_1000: `hsl(${hues.primary}, 28%, ${defaultScale[0]}%)`,
|
||||
|
||||
primary_25: `hsl(${hues.primary}, 99%, 97%)`,
|
||||
primary_50: `hsl(${hues.primary}, 99%, 95%)`,
|
||||
primary_100: `hsl(${hues.primary}, 99%, 90%)`,
|
||||
primary_200: `hsl(${hues.primary}, 99%, 80%)`,
|
||||
primary_300: `hsl(${hues.primary}, 99%, 70%)`,
|
||||
primary_400: `hsl(${hues.primary}, 99%, 60%)`,
|
||||
primary_500: `hsl(${hues.primary}, 99%, 53%)`,
|
||||
primary_600: `hsl(${hues.primary}, 99%, 42%)`,
|
||||
primary_700: `hsl(${hues.primary}, 99%, 34%)`,
|
||||
primary_800: `hsl(${hues.primary}, 99%, 26%)`,
|
||||
primary_900: `hsl(${hues.primary}, 99%, 18%)`,
|
||||
primary_950: `hsl(${hues.primary}, 99%, 10%)`,
|
||||
primary_975: `hsl(${hues.primary}, 99%, 7%)`,
|
||||
|
||||
green_25: `hsl(${hues.positive}, 82%, 97%)`,
|
||||
green_50: `hsl(${hues.positive}, 82%, 95%)`,
|
||||
green_100: `hsl(${hues.positive}, 82%, 90%)`,
|
||||
green_200: `hsl(${hues.positive}, 82%, 80%)`,
|
||||
green_300: `hsl(${hues.positive}, 82%, 70%)`,
|
||||
green_400: `hsl(${hues.positive}, 82%, 60%)`,
|
||||
green_500: `hsl(${hues.positive}, 82%, 50%)`,
|
||||
green_600: `hsl(${hues.positive}, 82%, 42%)`,
|
||||
green_700: `hsl(${hues.positive}, 82%, 34%)`,
|
||||
green_800: `hsl(${hues.positive}, 82%, 26%)`,
|
||||
green_900: `hsl(${hues.positive}, 82%, 18%)`,
|
||||
green_950: `hsl(${hues.positive}, 82%, 10%)`,
|
||||
green_975: `hsl(${hues.positive}, 82%, 7%)`,
|
||||
|
||||
red_25: `hsl(${hues.negative}, 91%, 97%)`,
|
||||
red_50: `hsl(${hues.negative}, 91%, 95%)`,
|
||||
red_100: `hsl(${hues.negative}, 91%, 90%)`,
|
||||
red_200: `hsl(${hues.negative}, 91%, 80%)`,
|
||||
red_300: `hsl(${hues.negative}, 91%, 70%)`,
|
||||
red_400: `hsl(${hues.negative}, 91%, 60%)`,
|
||||
red_500: `hsl(${hues.negative}, 91%, 50%)`,
|
||||
red_600: `hsl(${hues.negative}, 91%, 42%)`,
|
||||
red_700: `hsl(${hues.negative}, 91%, 34%)`,
|
||||
red_800: `hsl(${hues.negative}, 91%, 26%)`,
|
||||
red_900: `hsl(${hues.negative}, 91%, 18%)`,
|
||||
red_950: `hsl(${hues.negative}, 91%, 10%)`,
|
||||
red_975: `hsl(${hues.negative}, 91%, 7%)`,
|
||||
} as const
|
||||
|
||||
const lightPalette = {
|
||||
white: color.gray_0,
|
||||
black: color.gray_1000,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_25,
|
||||
contrast_50: color.gray_50,
|
||||
contrast_100: color.gray_100,
|
||||
contrast_200: color.gray_200,
|
||||
contrast_300: color.gray_300,
|
||||
contrast_400: color.gray_400,
|
||||
contrast_500: color.gray_500,
|
||||
contrast_600: color.gray_600,
|
||||
contrast_700: color.gray_700,
|
||||
contrast_800: color.gray_800,
|
||||
contrast_900: color.gray_900,
|
||||
contrast_950: color.gray_950,
|
||||
contrast_975: color.gray_975,
|
||||
|
||||
primary_25: color.primary_25,
|
||||
primary_50: color.primary_50,
|
||||
primary_100: color.primary_100,
|
||||
primary_200: color.primary_200,
|
||||
primary_300: color.primary_300,
|
||||
primary_400: color.primary_400,
|
||||
primary_500: color.primary_500,
|
||||
primary_600: color.primary_600,
|
||||
primary_700: color.primary_700,
|
||||
primary_800: color.primary_800,
|
||||
primary_900: color.primary_900,
|
||||
primary_950: color.primary_950,
|
||||
primary_975: color.primary_975,
|
||||
|
||||
positive_25: color.green_25,
|
||||
positive_50: color.green_50,
|
||||
positive_100: color.green_100,
|
||||
positive_200: color.green_200,
|
||||
positive_300: color.green_300,
|
||||
positive_400: color.green_400,
|
||||
positive_500: color.green_500,
|
||||
positive_600: color.green_600,
|
||||
positive_700: color.green_700,
|
||||
positive_800: color.green_800,
|
||||
positive_900: color.green_900,
|
||||
positive_950: color.green_950,
|
||||
positive_975: color.green_975,
|
||||
|
||||
negative_25: color.red_25,
|
||||
negative_50: color.red_50,
|
||||
negative_100: color.red_100,
|
||||
negative_200: color.red_200,
|
||||
negative_300: color.red_300,
|
||||
negative_400: color.red_400,
|
||||
negative_500: color.red_500,
|
||||
negative_600: color.red_600,
|
||||
negative_700: color.red_700,
|
||||
negative_800: color.red_800,
|
||||
negative_900: color.red_900,
|
||||
negative_950: color.red_950,
|
||||
negative_975: color.red_975,
|
||||
} as const
|
||||
|
||||
const darkPalette: Palette = {
|
||||
white: color.gray_25,
|
||||
black: color.trueBlack,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_975,
|
||||
contrast_50: color.gray_950,
|
||||
contrast_100: color.gray_900,
|
||||
contrast_200: color.gray_800,
|
||||
contrast_300: color.gray_700,
|
||||
contrast_400: color.gray_600,
|
||||
contrast_500: color.gray_500,
|
||||
contrast_600: color.gray_400,
|
||||
contrast_700: color.gray_300,
|
||||
contrast_800: color.gray_200,
|
||||
contrast_900: color.gray_100,
|
||||
contrast_950: color.gray_50,
|
||||
contrast_975: color.gray_25,
|
||||
|
||||
primary_25: color.primary_975,
|
||||
primary_50: color.primary_950,
|
||||
primary_100: color.primary_900,
|
||||
primary_200: color.primary_800,
|
||||
primary_300: color.primary_700,
|
||||
primary_400: color.primary_600,
|
||||
primary_500: color.primary_500,
|
||||
primary_600: color.primary_400,
|
||||
primary_700: color.primary_300,
|
||||
primary_800: color.primary_200,
|
||||
primary_900: color.primary_100,
|
||||
primary_950: color.primary_50,
|
||||
primary_975: color.primary_25,
|
||||
|
||||
positive_25: color.green_975,
|
||||
positive_50: color.green_950,
|
||||
positive_100: color.green_900,
|
||||
positive_200: color.green_800,
|
||||
positive_300: color.green_700,
|
||||
positive_400: color.green_600,
|
||||
positive_500: color.green_500,
|
||||
positive_600: color.green_400,
|
||||
positive_700: color.green_300,
|
||||
positive_800: color.green_200,
|
||||
positive_900: color.green_100,
|
||||
positive_950: color.green_50,
|
||||
positive_975: color.green_25,
|
||||
|
||||
negative_25: color.red_975,
|
||||
negative_50: color.red_950,
|
||||
negative_100: color.red_900,
|
||||
negative_200: color.red_800,
|
||||
negative_300: color.red_700,
|
||||
negative_400: color.red_600,
|
||||
negative_500: color.red_500,
|
||||
negative_600: color.red_400,
|
||||
negative_700: color.red_300,
|
||||
negative_800: color.red_200,
|
||||
negative_900: color.red_100,
|
||||
negative_950: color.red_50,
|
||||
negative_975: color.red_25,
|
||||
} as const
|
||||
|
||||
const dimPalette: Palette = {
|
||||
...darkPalette,
|
||||
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
|
||||
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
|
||||
contrast_100: `hsl(${hues.primary}, 28%, ${dimScale[3]}%)`,
|
||||
contrast_200: `hsl(${hues.primary}, 28%, ${dimScale[4]}%)`,
|
||||
contrast_300: `hsl(${hues.primary}, 24%, ${dimScale[5]}%)`,
|
||||
contrast_400: `hsl(${hues.primary}, 24%, ${dimScale[6]}%)`,
|
||||
contrast_500: `hsl(${hues.primary}, 20%, ${dimScale[7]}%)`,
|
||||
contrast_600: `hsl(${hues.primary}, 20%, ${dimScale[8]}%)`,
|
||||
contrast_700: `hsl(${hues.primary}, 20%, ${dimScale[9]}%)`,
|
||||
contrast_800: `hsl(${hues.primary}, 20%, ${dimScale[10]}%)`,
|
||||
contrast_900: `hsl(${hues.primary}, 20%, ${dimScale[11]}%)`,
|
||||
contrast_950: `hsl(${hues.primary}, 20%, ${dimScale[12]}%)`,
|
||||
contrast_975: `hsl(${hues.primary}, 20%, ${dimScale[13]}%)`,
|
||||
|
||||
primary_25: `hsl(${hues.primary}, 50%, ${dimScale[1]}%)`,
|
||||
primary_50: `hsl(${hues.primary}, 60%, ${dimScale[2]}%)`,
|
||||
primary_100: `hsl(${hues.primary}, 70%, ${dimScale[3]}%)`,
|
||||
primary_200: `hsl(${hues.primary}, 82%, ${dimScale[4]}%)`,
|
||||
primary_300: `hsl(${hues.primary}, 90%, ${dimScale[5]}%)`,
|
||||
primary_400: `hsl(${hues.primary}, 95%, ${dimScale[6]}%)`,
|
||||
primary_500: `hsl(${hues.primary}, 99%, ${dimScale[7]}%)`,
|
||||
primary_600: `hsl(${hues.primary}, 99%, ${dimScale[8]}%)`,
|
||||
primary_700: `hsl(${hues.primary}, 99%, ${dimScale[9]}%)`,
|
||||
primary_800: `hsl(${hues.primary}, 99%, ${dimScale[10]}%)`,
|
||||
primary_900: `hsl(${hues.primary}, 99%, ${dimScale[11]}%)`,
|
||||
primary_950: `hsl(${hues.primary}, 99%, ${dimScale[12]}%)`,
|
||||
primary_975: `hsl(${hues.primary}, 99%, ${dimScale[13]}%)`,
|
||||
|
||||
positive_25: `hsl(${hues.positive}, 50%, ${dimScale[1]}%)`,
|
||||
positive_50: `hsl(${hues.positive}, 60%, ${dimScale[2]}%)`,
|
||||
positive_100: `hsl(${hues.positive}, 70%, ${dimScale[3]}%)`,
|
||||
positive_200: `hsl(${hues.positive}, 82%, ${dimScale[4]}%)`,
|
||||
positive_300: `hsl(${hues.positive}, 82%, ${dimScale[5]}%)`,
|
||||
positive_400: `hsl(${hues.positive}, 82%, ${dimScale[6]}%)`,
|
||||
positive_500: `hsl(${hues.positive}, 82%, ${dimScale[7]}%)`,
|
||||
positive_600: `hsl(${hues.positive}, 82%, ${dimScale[8]}%)`,
|
||||
positive_700: `hsl(${hues.positive}, 82%, ${dimScale[9]}%)`,
|
||||
positive_800: `hsl(${hues.positive}, 82%, ${dimScale[10]}%)`,
|
||||
positive_900: `hsl(${hues.positive}, 82%, ${dimScale[11]}%)`,
|
||||
positive_950: `hsl(${hues.positive}, 82%, ${dimScale[12]}%)`,
|
||||
positive_975: `hsl(${hues.positive}, 82%, ${dimScale[13]}%)`,
|
||||
|
||||
negative_25: `hsl(${hues.negative}, 70%, ${dimScale[1]}%)`,
|
||||
negative_50: `hsl(${hues.negative}, 80%, ${dimScale[2]}%)`,
|
||||
negative_100: `hsl(${hues.negative}, 84%, ${dimScale[3]}%)`,
|
||||
negative_200: `hsl(${hues.negative}, 88%, ${dimScale[4]}%)`,
|
||||
negative_300: `hsl(${hues.negative}, 91%, ${dimScale[5]}%)`,
|
||||
negative_400: `hsl(${hues.negative}, 91%, ${dimScale[6]}%)`,
|
||||
negative_500: `hsl(${hues.negative}, 91%, ${dimScale[7]}%)`,
|
||||
negative_600: `hsl(${hues.negative}, 91%, ${dimScale[8]}%)`,
|
||||
negative_700: `hsl(${hues.negative}, 91%, ${dimScale[9]}%)`,
|
||||
negative_800: `hsl(${hues.negative}, 91%, ${dimScale[10]}%)`,
|
||||
negative_900: `hsl(${hues.negative}, 91%, ${dimScale[11]}%)`,
|
||||
negative_950: `hsl(${hues.negative}, 91%, ${dimScale[12]}%)`,
|
||||
negative_975: `hsl(${hues.negative}, 91%, ${dimScale[13]}%)`,
|
||||
} as const
|
||||
|
||||
const light: Theme = {
|
||||
scheme: 'light',
|
||||
name: 'light',
|
||||
palette: lightPalette,
|
||||
atoms: {
|
||||
text: {
|
||||
color: lightPalette.black,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: lightPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: lightPalette.contrast_700,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: lightPalette.contrast_900,
|
||||
},
|
||||
text_inverted: {
|
||||
color: lightPalette.white,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: lightPalette.white,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: lightPalette.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: lightPalette.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: lightPalette.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: lightPalette.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: lightPalette.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: lightPalette.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: lightPalette.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: lightPalette.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: lightPalette.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: lightPalette.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: lightPalette.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: lightPalette.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: lightPalette.contrast_975,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: lightPalette.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: lightPalette.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: lightPalette.contrast_300,
|
||||
},
|
||||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowColor: lightPalette.black,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowColor: lightPalette.black,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowColor: lightPalette.black,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const dark: Theme = {
|
||||
scheme: 'dark',
|
||||
name: 'dark',
|
||||
palette: darkPalette,
|
||||
atoms: {
|
||||
text: {
|
||||
color: darkPalette.white,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: darkPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: darkPalette.contrast_600,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: darkPalette.contrast_900,
|
||||
},
|
||||
text_inverted: {
|
||||
color: darkPalette.black,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: darkPalette.black,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: darkPalette.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: darkPalette.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: darkPalette.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: darkPalette.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: darkPalette.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: darkPalette.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: darkPalette.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: darkPalette.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: darkPalette.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: darkPalette.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: darkPalette.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: darkPalette.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: darkPalette.contrast_975,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: darkPalette.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: darkPalette.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: darkPalette.contrast_300,
|
||||
},
|
||||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: color.trueBlack,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: color.trueBlack,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: color.trueBlack,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const dim: Theme = {
|
||||
...dark,
|
||||
scheme: 'dark',
|
||||
name: 'dim',
|
||||
palette: dimPalette,
|
||||
atoms: {
|
||||
...dark.atoms,
|
||||
text: {
|
||||
color: dimPalette.white,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: dimPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: dimPalette.contrast_600,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: dimPalette.contrast_900,
|
||||
},
|
||||
text_inverted: {
|
||||
color: dimPalette.black,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: dimPalette.black,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: dimPalette.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: dimPalette.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: dimPalette.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: dimPalette.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: dimPalette.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: dimPalette.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: dimPalette.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: dimPalette.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: dimPalette.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: dimPalette.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: dimPalette.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: dimPalette.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: dimPalette.contrast_975,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: dimPalette.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: dimPalette.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: dimPalette.contrast_300,
|
||||
},
|
||||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${hues.primary}, 28%, 6%)`,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${hues.primary}, 28%, 6%)`,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${hues.primary}, 28%, 6%)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
lightPalette,
|
||||
darkPalette,
|
||||
dimPalette,
|
||||
light,
|
||||
dark,
|
||||
dim,
|
||||
}
|
||||
export const themes = {
|
||||
lightPalette: DEFAULT_THEMES.light.palette,
|
||||
darkPalette: DEFAULT_THEMES.dark.palette,
|
||||
dimPalette: DEFAULT_THEMES.dim.palette,
|
||||
light: DEFAULT_THEMES.light,
|
||||
dark: DEFAULT_THEMES.dark,
|
||||
dim: DEFAULT_THEMES.dim,
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const lightPalette = DEFAULT_THEMES.light.palette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const darkPalette = DEFAULT_THEMES.dark.palette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const dimPalette = DEFAULT_THEMES.dim.palette
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const light = DEFAULT_THEMES.light
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dark = DEFAULT_THEMES.dark
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dim = DEFAULT_THEMES.dim
|
||||
|
||||
@@ -1,61 +1,10 @@
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {tokens} from '@bsky.app/alf'
|
||||
|
||||
export const TRACKING = isAndroid ? 0.1 : 0
|
||||
export * from '@bsky.app/alf/dist/tokens'
|
||||
|
||||
export const color = {
|
||||
temp_purple: 'rgb(105 0 255)',
|
||||
temp_purple_dark: 'rgb(83 0 202)',
|
||||
} as const
|
||||
|
||||
export const space = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 20,
|
||||
_2xl: 24,
|
||||
_3xl: 28,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const fontSize = {
|
||||
_2xs: 10,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
_2xl: 22,
|
||||
_3xl: 26,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const lineHeight = {
|
||||
none: 1,
|
||||
normal: 1.5,
|
||||
relaxed: 1.625,
|
||||
} as const
|
||||
|
||||
export const borderRadius = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
full: 999,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* These correspond to Inter font files we actually load.
|
||||
*/
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
bold: '600',
|
||||
heavy: '800',
|
||||
temp_purple: tokens.labelerColor.purple,
|
||||
temp_purple_dark: tokens.labelerColor.purple_dark,
|
||||
} as const
|
||||
|
||||
export const gradients = {
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||
|
||||
export type TextStyleProp = {
|
||||
style?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
export type ViewStyleProp = {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type Palette = {
|
||||
white: string
|
||||
black: string
|
||||
like: string
|
||||
|
||||
contrast_25: string
|
||||
contrast_50: string
|
||||
contrast_100: string
|
||||
contrast_200: string
|
||||
contrast_300: string
|
||||
contrast_400: string
|
||||
contrast_500: string
|
||||
contrast_600: string
|
||||
contrast_700: string
|
||||
contrast_800: string
|
||||
contrast_900: string
|
||||
contrast_950: string
|
||||
contrast_975: string
|
||||
|
||||
primary_25: string
|
||||
primary_50: string
|
||||
primary_100: string
|
||||
primary_200: string
|
||||
primary_300: string
|
||||
primary_400: string
|
||||
primary_500: string
|
||||
primary_600: string
|
||||
primary_700: string
|
||||
primary_800: string
|
||||
primary_900: string
|
||||
primary_950: string
|
||||
primary_975: string
|
||||
|
||||
positive_25: string
|
||||
positive_50: string
|
||||
positive_100: string
|
||||
positive_200: string
|
||||
positive_300: string
|
||||
positive_400: string
|
||||
positive_500: string
|
||||
positive_600: string
|
||||
positive_700: string
|
||||
positive_800: string
|
||||
positive_900: string
|
||||
positive_950: string
|
||||
positive_975: string
|
||||
|
||||
negative_25: string
|
||||
negative_50: string
|
||||
negative_100: string
|
||||
negative_200: string
|
||||
negative_300: string
|
||||
negative_400: string
|
||||
negative_500: string
|
||||
negative_600: string
|
||||
negative_700: string
|
||||
negative_800: string
|
||||
negative_900: string
|
||||
negative_950: string
|
||||
negative_975: string
|
||||
}
|
||||
export type ThemedAtoms = {
|
||||
text: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_low: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_medium: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_high: {
|
||||
color: string
|
||||
}
|
||||
text_inverted: {
|
||||
color: string
|
||||
}
|
||||
bg: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_25: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_50: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_100: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_200: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_300: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_400: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_500: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_600: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_700: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_800: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_900: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_950: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_975: {
|
||||
backgroundColor: string
|
||||
}
|
||||
border_contrast_low: {
|
||||
borderColor: string
|
||||
}
|
||||
border_contrast_medium: {
|
||||
borderColor: string
|
||||
}
|
||||
border_contrast_high: {
|
||||
borderColor: string
|
||||
}
|
||||
shadow_sm: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
shadow_md: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
shadow_lg: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
}
|
||||
export type Theme = {
|
||||
scheme: 'light' | 'dark' // for library support
|
||||
name: ThemeName
|
||||
palette: Palette
|
||||
atoms: ThemedAtoms
|
||||
}
|
||||
@@ -3,27 +3,11 @@ import {type TextProps as RNTextProps} from 'react-native'
|
||||
import {type StyleProp, type TextStyle} from 'react-native'
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
import createEmojiRegex from 'emoji-regex'
|
||||
import type React from 'react'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
|
||||
|
||||
/**
|
||||
* Util to calculate lineHeight from a text size atom and a leading atom
|
||||
*
|
||||
* Example:
|
||||
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
|
||||
*/
|
||||
export function leading<
|
||||
Size extends {fontSize?: number},
|
||||
Leading extends {lineHeight?: number},
|
||||
>(textSize: Size, leading: Leading) {
|
||||
const size = textSize?.fontSize || atoms.text_md.fontSize
|
||||
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
|
||||
return Math.round(size * lineHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
|
||||
* other relative leading atoms.
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {jest} from '@jest/globals'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {transparentifyColor} from '../colorGeneration'
|
||||
|
||||
jest.mock('#/logger', () => ({
|
||||
logger: {warn: jest.fn()},
|
||||
}))
|
||||
|
||||
describe('transparentifyColor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -41,8 +36,5 @@ describe('transparentifyColor', () => {
|
||||
const unsupported = 'blue'
|
||||
const result = transparentifyColor(unsupported, 0.5)
|
||||
expect(result).toBe(unsupported)
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Could not make '${unsupported}' transparent`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,49 +1,8 @@
|
||||
import {logger} from '#/logger'
|
||||
import {utils} from '@bsky.app/alf'
|
||||
|
||||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
|
||||
/**
|
||||
* Smooth progression of lightness "stops" for generating HSL colors.
|
||||
* @deprecated use `utils.alpha` from `@bsky.app/alf` instead
|
||||
*/
|
||||
export const COLOR_STOPS = [
|
||||
0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1,
|
||||
]
|
||||
|
||||
export function generateScale(start: number, end: number) {
|
||||
const range = end - start
|
||||
return COLOR_STOPS.map(stop => {
|
||||
return start + range * stop
|
||||
})
|
||||
}
|
||||
|
||||
export const defaultScale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export function transparentifyColor(color: string, alpha: number) {
|
||||
if (color.startsWith('hsl(')) {
|
||||
return 'hsla(' + color.slice('hsl('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('rgb(')) {
|
||||
return 'rgba(' + color.slice('rgb('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('#')) {
|
||||
if (color.length === 7) {
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
// Per MDN: If there is only one number, it is duplicated: e means ee
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color
|
||||
return color.slice(0, 7) + alphaHex.padStart(2, alphaHex)
|
||||
} else if (color.length === 4) {
|
||||
// convert to 6-digit hex before adding alpha
|
||||
const [r, g, b] = color.slice(1).split('')
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
return `#${r.repeat(2)}${g.repeat(2)}${b.repeat(2)}${alphaHex.padStart(
|
||||
2,
|
||||
alphaHex,
|
||||
)}`
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Could not make '${color}' transparent`)
|
||||
}
|
||||
return color
|
||||
}
|
||||
export const transparentifyColor = utils.alpha
|
||||
|
||||
@@ -1,62 +1 @@
|
||||
import {Platform} from 'react-native'
|
||||
|
||||
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
||||
|
||||
/**
|
||||
* Identity function on web. Returns nothing on other platforms.
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function web(value: any) {
|
||||
if (isWeb) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity function on iOS. Returns nothing on other platforms.
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function ios(value: any) {
|
||||
if (isIOS) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity function on Android. Returns nothing on other platforms..
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function android(value: any) {
|
||||
if (isAndroid) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity function on iOS and Android. Returns nothing on web.
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function native(value: any) {
|
||||
if (isNative) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export const platform = Platform.select
|
||||
export {android, ios, native, platform, web} from '@bsky.app/alf'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
import {type Theme} from '@bsky.app/alf'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {type Theme} from '../types'
|
||||
|
||||
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
|
||||
if (isAndroid) {
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
import {ThemeName} from '#/alf/types'
|
||||
import {utils} from '@bsky.app/alf'
|
||||
|
||||
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
|
||||
switch (name) {
|
||||
case 'light':
|
||||
return options.light
|
||||
case 'dark':
|
||||
return options.dark || options.dim
|
||||
case 'dim':
|
||||
return options.dim || options.dark
|
||||
default:
|
||||
throw new Error(`select(theme, options) received unknown theme ${name}`)
|
||||
}
|
||||
}
|
||||
export const select = utils.select
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import {ColorSchemeName, useColorScheme} from 'react-native'
|
||||
import {type ColorSchemeName, useColorScheme} from 'react-native'
|
||||
import {type ThemeName} from '@bsky.app/alf'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useThemePrefs} from '#/state/shell'
|
||||
import {dark, dim, light} from '#/alf/themes'
|
||||
import {ThemeName} from '#/alf/types'
|
||||
|
||||
export function useColorModeTheme(): ThemeName {
|
||||
const theme = useThemeName()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import {Breakpoint, useBreakpoints} from '#/alf/breakpoints'
|
||||
import {type Breakpoint, useBreakpoints} from '#/alf/breakpoints'
|
||||
import * as tokens from '#/alf/tokens'
|
||||
|
||||
type Gutter = 'compact' | 'base' | 'wide' | 0
|
||||
|
||||
@@ -77,7 +77,7 @@ export function AccountList({
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.font_semi_bold,
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.py_sm,
|
||||
@@ -151,7 +151,7 @@ function AccountItem({
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.font_bold, a.leading_tight]}
|
||||
style={[a.font_semi_bold, a.leading_tight]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeDisplayName(
|
||||
profile?.displayName || profile?.handle || account.handle,
|
||||
|
||||
@@ -6,16 +6,27 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useDeviceGeolocationApi} from '#/state/geolocation'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Full as Logo, Mark} from '#/components/icons/Logo'
|
||||
import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
|
||||
import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link'
|
||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
|
||||
|
||||
export function BlockedGeoOverlay() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const insets = useSafeAreaInsets()
|
||||
const geoDialog = Dialog.useDialogControl()
|
||||
const {setDeviceGeolocation} = useDeviceGeolocationApi()
|
||||
|
||||
useEffect(() => {
|
||||
// just counting overall hits here
|
||||
@@ -51,59 +62,132 @@ export function BlockedGeoOverlay() {
|
||||
]
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
a.px_2xl,
|
||||
{
|
||||
paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.mx_auto,
|
||||
web({
|
||||
maxWidth: 440,
|
||||
paddingTop: gtPhone ? '8vh' : undefined,
|
||||
}),
|
||||
<>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
a.px_2xl,
|
||||
{
|
||||
paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.pl_md,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
backgroundColor: t.palette.primary_25,
|
||||
},
|
||||
]}>
|
||||
<Mark fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
<View
|
||||
style={[
|
||||
a.mx_auto,
|
||||
web({
|
||||
maxWidth: 380,
|
||||
paddingTop: gtPhone ? '8vh' : undefined,
|
||||
}),
|
||||
]}>
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.pl_md,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
backgroundColor: t.palette.primary_25,
|
||||
},
|
||||
]}>
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
<Mark fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
style={[
|
||||
a.font_semi_bold,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
},
|
||||
]}>
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_lg, {paddingTop: 32}]}>
|
||||
{blocks.map((block, index) => (
|
||||
<Text key={index} style={[textStyles]}>
|
||||
{block}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!isWeb && (
|
||||
<>
|
||||
<View style={[a.pt_2xl]}>
|
||||
<Divider />
|
||||
</View>
|
||||
|
||||
<View style={[a.mt_xl, a.align_start]}>
|
||||
<Text style={[a.text_lg, a.font_bold, a.leading_snug, a.pb_xs]}>
|
||||
<Trans>Not in Mississippi?</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.pb_md,
|
||||
]}>
|
||||
<Trans>
|
||||
Confirm your location with GPS. Your location data is not
|
||||
tracked and does not leave your device.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Confirm your location`)}
|
||||
onPress={() => geoDialog.open()}
|
||||
size="small"
|
||||
color="primary_subtle">
|
||||
<ButtonIcon icon={LocationIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Confirm your location</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<DeviceLocationRequestDialog
|
||||
control={geoDialog}
|
||||
onLocationAcquired={props => {
|
||||
if (props.geolocationStatus.isAgeBlockedGeo) {
|
||||
props.disableDialogAction()
|
||||
props.setDialogError(
|
||||
_(
|
||||
msg`We're sorry, but based on your device's location, you are currently located in a region where we cannot provide access at this time.`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
props.closeDialog(() => {
|
||||
// set this after close!
|
||||
setDeviceGeolocation({
|
||||
countryCode: props.geolocationStatus.countryCode,
|
||||
regionCode: props.geolocationStatus.regionCode,
|
||||
})
|
||||
Toast.show(_(msg`Thanks! You're all set.`), {
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={[{paddingTop: 48}]}>
|
||||
<Logo width={120} textFill={t.atoms.text.color} />
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={[a.gap_lg, {paddingTop: 32, paddingBottom: 48}]}>
|
||||
{blocks.map((block, index) => (
|
||||
<Text key={index} style={[textStyles]}>
|
||||
{block}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Logo width={120} textFill={t.atoms.text.color} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
{/*
|
||||
* While this blocking overlay is up, other dialogs in the shell
|
||||
* are not mounted, so it _should_ be safe to use these here
|
||||
* without fear of other modals showing up.
|
||||
*/}
|
||||
<BottomSheetOutlet />
|
||||
<PortalOutlet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ export type ButtonState = {
|
||||
export type ButtonContext = VariantProps & ButtonState
|
||||
|
||||
type NonTextElements =
|
||||
| React.ReactElement
|
||||
| Iterable<React.ReactElement | null | undefined | boolean>
|
||||
| React.ReactElement<any>
|
||||
| Iterable<React.ReactElement<any> | null | undefined | boolean>
|
||||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
@@ -240,7 +240,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg_contrast_25)
|
||||
baseStyles.push(t.atoms.bg_contrast_50)
|
||||
hoverStyles.push(t.atoms.bg_contrast_100)
|
||||
} else {
|
||||
baseStyles.push(t.atoms.bg_contrast_50)
|
||||
@@ -289,11 +289,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_25,
|
||||
dim: t.palette.primary_50,
|
||||
dark: t.palette.primary_50,
|
||||
}),
|
||||
backgroundColor: t.palette.primary_50,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative_subtle') {
|
||||
@@ -314,11 +310,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_25,
|
||||
dim: t.palette.negative_50,
|
||||
dark: t.palette.negative_50,
|
||||
}),
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -372,7 +364,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -396,7 +388,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@ export function Root({children}: {children: React.ReactNode}) {
|
||||
const hoverablesSV = useSharedValue<
|
||||
Record<string, {id: string; rect: Measurement}>
|
||||
>({})
|
||||
const syncHoverablesThrottleRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const syncHoverablesThrottleRef =
|
||||
useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
const [hoveredMenuItem, setHoveredMenuItem] = useState<string | null>(null)
|
||||
|
||||
const onHoverableTouchUp = useCallback((id: string) => {
|
||||
@@ -795,7 +796,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_high,
|
||||
{paddingTop: 3},
|
||||
style,
|
||||
@@ -854,7 +855,11 @@ export function LabelText({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
style={[a.font_bold, t.atoms.text_contrast_medium, {marginBottom: -8}]}>
|
||||
style={[
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
{marginBottom: -8},
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {type SharedValue} from 'react-native-reanimated'
|
||||
import type React from 'react'
|
||||
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
import {
|
||||
|
||||
@@ -267,7 +267,10 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
scrollEventThrottle={50}
|
||||
onScroll={isAndroid ? onScroll : undefined}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
stickyHeaderIndices={header ? [0] : undefined}>
|
||||
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
||||
// on Android. fine to disable for now, because we don't have any
|
||||
// dialogs that use this that actually scroll -sfn
|
||||
stickyHeaderIndices={ios(header ? [0] : undefined)}>
|
||||
{header}
|
||||
{children}
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function HeaderText({
|
||||
style?: StyleProp<TextStyle>
|
||||
}) {
|
||||
return (
|
||||
<Text style={[a.text_lg, a.text_center, a.font_heavy, style]}>
|
||||
<Text style={[a.text_lg, a.text_center, a.font_bold, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {type ViewStyleProp} from '#/alf'
|
||||
import {type BottomSheetViewProps} from '../../../modules/bottom-sheet'
|
||||
@@ -34,7 +33,7 @@ export type DialogControlRefProps = {
|
||||
*/
|
||||
export type DialogControlProps = DialogControlRefProps & {
|
||||
id: string
|
||||
ref: React.RefObject<DialogControlRefProps>
|
||||
ref: React.RefObject<DialogControlRefProps | null>
|
||||
isOpen?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import {DialogControlProps} from '#/components/Dialog/types'
|
||||
import {type DialogControlProps} from '#/components/Dialog/types'
|
||||
|
||||
export function useAutoOpen(control: DialogControlProps, showTimeout?: number) {
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -40,7 +40,7 @@ export function Error({
|
||||
]}
|
||||
sideBorders={sideBorders}>
|
||||
<View style={[a.w_full, a.align_center, a.gap_lg]}>
|
||||
<Text style={[a.font_bold, a.text_3xl]}>{title}</Text>
|
||||
<Text style={[a.font_semi_bold, a.text_3xl]}>{title}</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
|
||||
@@ -128,7 +128,7 @@ export function TitleAndByline({
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
@@ -214,7 +214,7 @@ export function DescriptionPlaceholder() {
|
||||
export function Likes({count}: {count: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_bold]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_semi_bold]}>
|
||||
<Trans>
|
||||
Liked by <Plural value={count || 0} one="# user" other="# users" />
|
||||
</Trans>
|
||||
|
||||
@@ -380,7 +380,7 @@ export function ProfileGrid({
|
||||
a.justify_between,
|
||||
]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
<Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
|
||||
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
|
||||
{isFeedContext ? (
|
||||
<Trans>Suggested for you</Trans>
|
||||
) : (
|
||||
@@ -516,7 +516,7 @@ export function SuggestedFeeds() {
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Some other feeds you might like</Trans>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function Fill({
|
||||
children,
|
||||
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -26,7 +24,7 @@ import {useA11y} from '#/state/a11y'
|
||||
* screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
|
||||
* no better option is available.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
export function FocusScope({children}: {children: React.ReactNode}) {
|
||||
const {screenReaderEnabled} = useA11y()
|
||||
|
||||
return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
|
||||
@@ -41,7 +39,7 @@ export function FocusScope({children}: {children: ReactNode}) {
|
||||
* they have reached the start or end of the content and tell them how to
|
||||
* remain within the active content section.
|
||||
*/
|
||||
function FocusTrap({children}: {children: ReactNode}) {
|
||||
function FocusTrap({children}: {children: React.ReactNode}) {
|
||||
const {_} = useLingui()
|
||||
const child = useRef<View>(null)
|
||||
|
||||
@@ -53,7 +51,7 @@ function FocusTrap({children}: {children: ReactNode}) {
|
||||
const decoratedChildren = useMemo(() => {
|
||||
return Children.toArray(children).map((node, i) => {
|
||||
if (i === 0 && isValidElement(node)) {
|
||||
const n = node as ReactElement<any>
|
||||
const n = node as React.ReactElement<any>
|
||||
if (n.props.ref !== undefined) {
|
||||
throw new Error(
|
||||
'FocusScope needs to override the ref on its first child.',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {type ReactNode} from 'react'
|
||||
import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
|
||||
|
||||
/*
|
||||
@@ -6,7 +5,7 @@ import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
|
||||
* use this in Dialogs and such already. It's here as a convenient counterpart
|
||||
* to the hacky native solution.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
export function FocusScope({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<RadixFocusScope.FocusScope loop asChild trapped>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {atoms as a, tokens, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, type tokens, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function GradientFill({
|
||||
gradient,
|
||||
|
||||
@@ -3,12 +3,12 @@ import {View} from 'react-native'
|
||||
import {
|
||||
atoms as a,
|
||||
flatten,
|
||||
TextStyleProp,
|
||||
type TextStyleProp,
|
||||
useTheme,
|
||||
ViewStyleProp,
|
||||
type ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {Props} from '#/components/icons/common'
|
||||
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||
import {type Props} from '#/components/icons/common'
|
||||
import {type Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||
|
||||
export function IconCircle({
|
||||
icon: Icon,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {type AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useLabelerInfoQuery} from '#/state/queries/labeler'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||
import {Link as InternalLink, type LinkProps} from '#/components/Link'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
|
||||
@@ -45,7 +45,7 @@ export function Avatar({avatar}: {avatar?: string}) {
|
||||
|
||||
export function Title({value}: {value: string}) {
|
||||
return (
|
||||
<Text emoji style={[a.text_md, a.font_bold, a.leading_tight]}>
|
||||
<Text emoji style={[a.text_md, a.font_semi_bold, a.leading_tight]}>
|
||||
{value}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -181,7 +181,7 @@ export function TitleText({
|
||||
<Text
|
||||
style={[
|
||||
a.text_lg,
|
||||
a.font_heavy,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
isIOS && align === 'platform' && a.text_center,
|
||||
gtMobile && a.text_xl,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
import {type AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, ViewStyle} from 'react-native'
|
||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import type React from 'react'
|
||||
|
||||
import {gradients} from '#/alf/tokens'
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export function TitleAndByline({
|
||||
allowOverride={creator && currentAccount?.did === creator.did}>
|
||||
<Hider.Mask>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.leading_snug, a.italic]}
|
||||
style={[a.text_md, a.font_semi_bold, a.leading_snug, a.italic]}
|
||||
numberOfLines={1}>
|
||||
<Trans>Hidden list</Trans>
|
||||
</Text>
|
||||
@@ -135,7 +135,7 @@ export function TitleAndByline({
|
||||
<Hider.Content>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
@@ -2,7 +2,6 @@ import {memo} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
|
||||