Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20a5ac8cd2 | |||
| e8f830aad7 | |||
| 94d9357b69 | |||
| 391a1d4be3 | |||
| e44a022226 | |||
| 4b19e3794e | |||
| e7a4686bc4 | |||
| b71575a3f0 | |||
| 2b10180d74 | |||
| 9a73611f73 | |||
| 2b6cff298a | |||
| afa6d67c23 | |||
| d53c0e548f | |||
| ca5d4bff0c | |||
| a8b78588fb | |||
| 0098d617ee | |||
| 661d20d176 | |||
| c85ce941c3 | |||
| 2ad93a5895 | |||
| ffac99ff80 | |||
| 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-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 |
@@ -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,27 @@ 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})
|
||||
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})
|
||||
await events.once(this.metricsServer, 'listening')
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
this.ctx.abortController.abort()
|
||||
await this.terminator?.terminate()
|
||||
await this.metricsTerminator?.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,44 @@
|
||||
<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 }}" />
|
||||
{%- 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 +85,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,48 @@
|
||||
{% 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 %}
|
||||
{% 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,7 +71,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.16.2",
|
||||
"@atproto/api": "^0.16.7",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -92,15 +92,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 +121,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 +175,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 +216,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 +239,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 +261,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 +271,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
|
||||
@@ -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,13 @@ 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 {StorybookLists} from '#/view/screens/StorybookLists'
|
||||
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 +91,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'
|
||||
@@ -304,6 +306,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
getComponent={() => Storybook}
|
||||
options={{title: title(msg`Storybook`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="StorybookLists"
|
||||
getComponent={() => StorybookLists}
|
||||
options={{title: title(msg`Storybook Lists`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="DebugMod"
|
||||
getComponent={() => DebugModScreen}
|
||||
@@ -600,6 +607,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'
|
||||
|
||||
|
||||
@@ -1063,6 +1063,7 @@ export const atoms = {
|
||||
}),
|
||||
fade_out: web({
|
||||
animation: 'fadeOut ease-out 0.15s',
|
||||
animationFillMode: 'forwards',
|
||||
}),
|
||||
zoom_in: web({
|
||||
animation: 'zoomIn ease-out 0.1s',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {atoms} from '#/alf/atoms'
|
||||
import {Palette, Theme} from '#/alf/types'
|
||||
import {type Palette, type Theme} from '#/alf/types'
|
||||
import {
|
||||
BLUE_HUE,
|
||||
defaultScale,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||
import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
|
||||
|
||||
export type TextStyleProp = {
|
||||
style?: StyleProp<TextStyle>
|
||||
|
||||
@@ -3,7 +3,6 @@ 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'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ThemeName} from '#/alf/types'
|
||||
import {type ThemeName} from '#/alf/types'
|
||||
|
||||
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
|
||||
switch (name) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import {ColorSchemeName, useColorScheme} from 'react-native'
|
||||
import {type ColorSchemeName, useColorScheme} from 'react-native'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useThemePrefs} from '#/state/shell'
|
||||
import {dark, dim, light} from '#/alf/themes'
|
||||
import {ThemeName} from '#/alf/types'
|
||||
import {type 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
|
||||
|
||||
@@ -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,133 @@ 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_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_heavy, 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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -23,6 +23,11 @@ import {
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
|
||||
import {ScrollbarOffsetContext} from '#/components/Layout/context'
|
||||
import {
|
||||
List as BaseList,
|
||||
type ListItem,
|
||||
type ListProps,
|
||||
} from '#/components/List'
|
||||
|
||||
export * from '#/components/Layout/const'
|
||||
export * as Header from '#/components/Layout/Header'
|
||||
@@ -223,3 +228,15 @@ const WebCenterBorders = memo(function LayoutWebCenterBorders() {
|
||||
/>
|
||||
) : null
|
||||
})
|
||||
|
||||
export function List<Item extends ListItem>(props: ListProps<Item>) {
|
||||
return (
|
||||
<BaseList<Item>
|
||||
{...props}
|
||||
renderItem={item => {
|
||||
return <Center>{props.renderItem!(item)}</Center>
|
||||
}}
|
||||
style={[a.h_full_vh, props.style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import {createContext, useCallback, useContext, useMemo} from 'react'
|
||||
import {type NativeScrollEvent} from 'react-native'
|
||||
import {type ScrollHandlers} from 'react-native-reanimated'
|
||||
|
||||
export type NormalizedScrollHandlers = {
|
||||
/**
|
||||
* Web + Native — needs to be a `worklet` on native, but can be either on web
|
||||
*/
|
||||
onScroll?: ScrollHandlers<any>['onScroll']
|
||||
/**
|
||||
* Native only
|
||||
*/
|
||||
onScrollBeginDrag?: ScrollHandlers<any>['onBeginDrag']
|
||||
/**
|
||||
* Native only
|
||||
*/
|
||||
onScrollEndDrag?: ScrollHandlers<any>['onEndDrag']
|
||||
/**
|
||||
* Native only
|
||||
*/
|
||||
onMomentumScrollBegin?: ScrollHandlers<any>['onMomentumBegin']
|
||||
/**
|
||||
* Native only
|
||||
*/
|
||||
onMomentumScrollEnd?: ScrollHandlers<any>['onMomentumEnd']
|
||||
}
|
||||
|
||||
const ListScrollContext = createContext<NormalizedScrollHandlers>({
|
||||
onScroll: undefined,
|
||||
onScrollBeginDrag: undefined,
|
||||
onScrollEndDrag: undefined,
|
||||
onMomentumScrollBegin: undefined,
|
||||
onMomentumScrollEnd: undefined,
|
||||
})
|
||||
ListScrollContext.displayName = 'ListScrollContext'
|
||||
|
||||
export function ListScrollProvider({
|
||||
children,
|
||||
onScrollBeginDrag,
|
||||
onScrollEndDrag,
|
||||
onScroll,
|
||||
onMomentumScrollBegin,
|
||||
onMomentumScrollEnd,
|
||||
}: {children: React.ReactNode} & NormalizedScrollHandlers) {
|
||||
const handlers = useMemo(
|
||||
() => ({
|
||||
onScroll,
|
||||
onScrollBeginDrag: onScrollBeginDrag,
|
||||
onScrollEndDrag: onScrollEndDrag,
|
||||
onMomentumScrollBegin: onMomentumScrollBegin,
|
||||
onMomentumScrollEnd: onMomentumScrollEnd,
|
||||
}),
|
||||
[
|
||||
onScrollBeginDrag,
|
||||
onScrollEndDrag,
|
||||
onScroll,
|
||||
onMomentumScrollBegin,
|
||||
onMomentumScrollEnd,
|
||||
],
|
||||
)
|
||||
return (
|
||||
<ListScrollContext.Provider value={handlers}>
|
||||
{children}
|
||||
</ListScrollContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useListScrollContext(): NormalizedScrollHandlers {
|
||||
return useContext(ListScrollContext)
|
||||
}
|
||||
|
||||
export const useListScrollHandler = useCallback<(e: NativeScrollEvent) => void>
|
||||
@@ -0,0 +1,235 @@
|
||||
import {forwardRef, useMemo} from 'react'
|
||||
import {
|
||||
type FlatList,
|
||||
type FlatListProps,
|
||||
RefreshControl,
|
||||
type ViewToken,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
type FlatListPropsWithLayout,
|
||||
runOnJS,
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
||||
|
||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {isIOS, isNative} from '#/platform/detection'
|
||||
import {useLightbox} from '#/state/lightbox'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useListScrollContext} from '#/components/List/ListScrollProvider'
|
||||
|
||||
export {
|
||||
ListScrollProvider,
|
||||
useListScrollHandler,
|
||||
} from '#/components/List/ListScrollProvider'
|
||||
|
||||
export type ListRef<Item extends {key: string}> =
|
||||
React.MutableRefObject<FlatList<Item> | null>
|
||||
|
||||
export type ListItem = {key: string}
|
||||
|
||||
export type ListProps<Item extends ListItem> = Omit<
|
||||
FlatListProps<Item>,
|
||||
| 'onScroll'
|
||||
| 'onScrollBeginDrag'
|
||||
| 'onScrollEndDrag'
|
||||
| 'onMomentumScrollBegin'
|
||||
| 'onMomentumScrollEnd'
|
||||
| 'refreshControl'
|
||||
| 'contentOffset'
|
||||
> & {
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onScroll?: FlatListProps<Item>['onScroll']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onScrollBeginDrag?: FlatListProps<Item>['onScrollBeginDrag']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onScrollEndDrag?: FlatListProps<Item>['onScrollEndDrag']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onMomentumScrollBegin?: FlatListProps<Item>['onMomentumScrollBegin']
|
||||
/**
|
||||
* @deprecated use `ListScrollProvider` handler instead
|
||||
*/
|
||||
onMomentumScrollEnd?: FlatListProps<Item>['onMomentumScrollEnd']
|
||||
/**
|
||||
* @deprecated pass `refreshing` and `onRefresh` instead to enable
|
||||
*/
|
||||
refreshControl?: FlatListProps<Item>['refreshControl']
|
||||
/**
|
||||
* @deprecated use `headerOffset` instead
|
||||
*/
|
||||
contentOffset?: FlatListProps<Item>['contentOffset']
|
||||
/**
|
||||
* Wrapper around `onViewableItemsChanged` that calls back with individual
|
||||
* items IF they `item.isViewable` is true.
|
||||
*/
|
||||
onItemSeen?: (item: Item) => void
|
||||
/**
|
||||
* Sugar for adding padding to the top of the list to accommodate fixed
|
||||
* headers. Also applies insets to the scroll indicators.
|
||||
*/
|
||||
headerOffset?: number
|
||||
/**
|
||||
* Sugar for adding padding to the bottom of the list to accommodate fixed
|
||||
* footers. Also applies insets to the scroll indicators.
|
||||
*/
|
||||
footerOffset?: number
|
||||
/**
|
||||
* Configures the point at which `onScrolledDownChange` is called.
|
||||
*/
|
||||
didScrollDownThreshold?: number
|
||||
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||
}
|
||||
|
||||
export const List = forwardRef(function List<Item extends ListItem>(
|
||||
{...props}: ListProps<Item>,
|
||||
ref: React.Ref<FlatList<Item>>,
|
||||
) {
|
||||
const t = useTheme()
|
||||
const {activeLightbox} = useLightbox()
|
||||
const debounce400 = useDedupe(400)
|
||||
const isScrolledDown = useSharedValue(false)
|
||||
const scrollHandlers = useListScrollContext()
|
||||
const onScroll = useAnimatedScrollHandler({
|
||||
onScroll(e, ctx) {
|
||||
scrollHandlers.onScroll?.(e, ctx)
|
||||
|
||||
const didScrollDown =
|
||||
e.contentOffset.y > (props.didScrollDownThreshold ?? 200)
|
||||
if (isScrolledDown.get() !== didScrollDown) {
|
||||
isScrolledDown.set(didScrollDown)
|
||||
if (props.onScrolledDownChange) {
|
||||
runOnJS(props.onScrolledDownChange)(didScrollDown)
|
||||
}
|
||||
}
|
||||
|
||||
if (isIOS) runOnJS(debounce400)(updateActiveVideoViewAsync)
|
||||
},
|
||||
onBeginDrag(e, ctx) {
|
||||
scrollHandlers.onScrollBeginDrag?.(e, ctx)
|
||||
},
|
||||
onEndDrag(e, ctx) {
|
||||
scrollHandlers.onScrollEndDrag?.(e, ctx)
|
||||
if (isNative) runOnJS(updateActiveVideoViewAsync)()
|
||||
},
|
||||
/*
|
||||
* Note: adding onMomentumBegin here makes simulator scroll lag on Android.
|
||||
* So either don't add it, or figure out why. - sfn
|
||||
* TODO
|
||||
*/
|
||||
onMomentumBegin(e, ctx) {
|
||||
scrollHandlers.onMomentumScrollBegin?.(e, ctx)
|
||||
},
|
||||
onMomentumEnd(e, ctx) {
|
||||
scrollHandlers.onMomentumScrollEnd?.(e, ctx)
|
||||
if (isNative) runOnJS(updateActiveVideoViewAsync)()
|
||||
},
|
||||
})
|
||||
|
||||
const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
|
||||
const onItemSeen = props.onItemSeen
|
||||
if (!onItemSeen) return [undefined, undefined]
|
||||
return [
|
||||
(info: {
|
||||
viewableItems: Array<ViewToken<Item>>
|
||||
changed: Array<ViewToken<Item>>
|
||||
}) => {
|
||||
for (const item of info.changed) {
|
||||
if (item.isViewable) {
|
||||
onItemSeen(item.item)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
itemVisiblePercentThreshold: 40,
|
||||
minimumViewTime: 0.5e3,
|
||||
},
|
||||
]
|
||||
}, [props.onItemSeen])
|
||||
|
||||
let refreshControl
|
||||
if (props.refreshing !== undefined || props.onRefresh !== undefined) {
|
||||
refreshControl = (
|
||||
<RefreshControl
|
||||
key={t.atoms.text.color}
|
||||
refreshing={props.refreshing ?? false}
|
||||
onRefresh={props.onRefresh ?? undefined}
|
||||
tintColor={t.atoms.text.color}
|
||||
titleColor={t.atoms.text.color}
|
||||
progressViewOffset={props.progressViewOffset ?? props.headerOffset}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Web only, provides a handle on the resulting `List` DOM element, which we
|
||||
* then use to apply sticky styles to individual list item wrappers.
|
||||
*/
|
||||
const dataSet = props.stickyHeaderIndices?.reduce((ds, i) => {
|
||||
return {...ds, [`sticky-header-index-${i}`]: 1}
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.FlatList
|
||||
// @ts-ignore
|
||||
dataSet={dataSet}
|
||||
ref={ref}
|
||||
keyExtractor={props.keyExtractor || (i => i.key)}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
/**
|
||||
* iOS automatically adds in the safe area to the scroll indicator
|
||||
* insets, even though the overwhelming majority of our ScrollViews do
|
||||
* not stretch from edge to edge.
|
||||
* @see https://github.com/bluesky-social/social-app/pull/7131
|
||||
*/
|
||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||
/**
|
||||
* For better UX, we default to true, but it can be disabled if needed.
|
||||
* @see https://github.com/bluesky-social/social-app/pull/8529
|
||||
*/
|
||||
showsVerticalScrollIndicator
|
||||
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
|
||||
scrollIndicatorInsets={{
|
||||
top: props.headerOffset ?? 0,
|
||||
bottom: props.footerOffset ?? 0,
|
||||
/**
|
||||
* May fix a bug where the scroll indicator is in the middle of the screen
|
||||
* @see https://github.com/facebook/react-native/issues/26610
|
||||
*/
|
||||
right: 1,
|
||||
}}
|
||||
/**
|
||||
* Native only. On web, we use padding on `style` instead.
|
||||
*/
|
||||
contentOffset={
|
||||
props.headerOffset ? {x: 0, y: props.headerOffset * -1} : undefined
|
||||
}
|
||||
scrollsToTop={!activeLightbox}
|
||||
refreshControl={refreshControl}
|
||||
{...(props as FlatListPropsWithLayout<Item>)}
|
||||
style={[
|
||||
a.h_full,
|
||||
{
|
||||
paddingTop: props.headerOffset,
|
||||
paddingBottom: props.footerOffset,
|
||||
transform: 'unset',
|
||||
},
|
||||
props.style,
|
||||
]}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}) as <Item extends {key: string}>(
|
||||
props: ListProps<Item> & {ref?: React.Ref<FlatList<Item>>},
|
||||
) => React.ReactElement
|
||||
@@ -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'
|
||||
|
||||
@@ -8,7 +8,7 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
import {type Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
||||
|
||||
export function Loader(props: Props) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
import {type Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
|
||||
|
||||
export function Loader(props: Props) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import {Fill} from '#/components/Fill'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
AccessibilityProps,
|
||||
AccessibilityRole,
|
||||
GestureResponderEvent,
|
||||
PressableProps,
|
||||
type AccessibilityProps,
|
||||
type AccessibilityRole,
|
||||
type GestureResponderEvent,
|
||||
type PressableProps,
|
||||
} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {TextStyleProp, ViewStyleProp} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {type TextStyleProp, type ViewStyleProp} from '#/alf'
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
|
||||
export type ContextType = {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {differenceInSeconds} from 'date-fns'
|
||||
@@ -27,30 +27,12 @@ export function NewskieDialog({
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {currentAccount} = useSession()
|
||||
const timeAgo = useGetTimeAgo()
|
||||
const control = useDialogControl()
|
||||
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
const createdAt = profile.createdAt as string | undefined
|
||||
|
||||
const profileName = React.useMemo(() => {
|
||||
const name = profile.displayName || profile.handle
|
||||
|
||||
if (isMe) {
|
||||
return _(msg`You`)
|
||||
}
|
||||
|
||||
if (!moderationOpts) return name
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
|
||||
return sanitizeDisplayName(name, moderation.ui('displayName'))
|
||||
}, [_, isMe, moderationOpts, profile])
|
||||
|
||||
const [now] = React.useState(() => Date.now())
|
||||
const daysOld = React.useMemo(() => {
|
||||
const [now] = useState(() => Date.now())
|
||||
const daysOld = useMemo(() => {
|
||||
if (!createdAt) return Infinity
|
||||
return differenceInSeconds(now, new Date(createdAt)) / 86400
|
||||
}, [createdAt, now])
|
||||
@@ -77,88 +59,124 @@ export function NewskieDialog({
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`New user info dialog`)}
|
||||
style={web({width: 'auto', maxWidth: 400, minWidth: 200})}>
|
||||
<View style={[a.gap_md]}>
|
||||
<View style={[a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: 60,
|
||||
width: 64,
|
||||
},
|
||||
]}>
|
||||
<Newskie
|
||||
width={64}
|
||||
height={64}
|
||||
fill="#FFC404"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[a.font_bold, a.text_xl]}>
|
||||
{isMe ? (
|
||||
<Trans>Welcome, friend!</Trans>
|
||||
) : (
|
||||
<Trans>Say hello!</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[a.text_md, a.text_center, a.leading_snug]}>
|
||||
{profile.joinedViaStarterPack ? (
|
||||
<Trans>
|
||||
{profileName} joined Bluesky using a starter pack{' '}
|
||||
{timeAgo(createdAt, now, {format: 'long'})} ago
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
{profileName} joined Bluesky{' '}
|
||||
{timeAgo(createdAt, now, {format: 'long'})} ago
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
{profile.joinedViaStarterPack ? (
|
||||
<StarterPackCard.Link
|
||||
starterPack={profile.joinedViaStarterPack}
|
||||
onPress={() => {
|
||||
control.close()
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.mt_sm,
|
||||
a.p_lg,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<StarterPackCard.Card
|
||||
starterPack={profile.joinedViaStarterPack}
|
||||
/>
|
||||
</View>
|
||||
</StarterPackCard.Link>
|
||||
) : null}
|
||||
|
||||
{isNative && (
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={[a.mt_sm]}
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
<DialogInner profile={profile} createdAt={createdAt} now={now} />
|
||||
</Dialog.Outer>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogInner({
|
||||
profile,
|
||||
createdAt,
|
||||
now,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
createdAt: string
|
||||
now: number
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {currentAccount} = useSession()
|
||||
const timeAgo = useGetTimeAgo()
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
|
||||
const profileName = useMemo(() => {
|
||||
if (!moderationOpts) return profile.displayName || profile.handle
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
return sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
}, [moderationOpts, profile])
|
||||
|
||||
const getJoinMessage = () => {
|
||||
const timeAgoString = timeAgo(createdAt, now, {format: 'long'})
|
||||
|
||||
if (isMe) {
|
||||
if (profile.joinedViaStarterPack) {
|
||||
return _(
|
||||
msg`You joined Bluesky using a starter pack ${timeAgoString} ago`,
|
||||
)
|
||||
} else {
|
||||
return _(msg`You joined Bluesky ${timeAgoString} ago`)
|
||||
}
|
||||
} else {
|
||||
if (profile.joinedViaStarterPack) {
|
||||
return _(
|
||||
msg`${profileName} joined Bluesky using a starter pack ${timeAgoString} ago`,
|
||||
)
|
||||
} else {
|
||||
return _(msg`${profileName} joined Bluesky ${timeAgoString} ago`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`New user info dialog`)}
|
||||
style={web({maxWidth: 400})}>
|
||||
<View style={[a.gap_md]}>
|
||||
<View style={[a.align_center]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: 60,
|
||||
width: 64,
|
||||
},
|
||||
]}>
|
||||
<Newskie
|
||||
width={64}
|
||||
height={64}
|
||||
fill="#FFC404"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[a.font_bold, a.text_xl]}>
|
||||
{isMe ? <Trans>Welcome, friend!</Trans> : <Trans>Say hello!</Trans>}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[a.text_md, a.text_center, a.leading_snug]}>
|
||||
{getJoinMessage()}
|
||||
</Text>
|
||||
{profile.joinedViaStarterPack ? (
|
||||
<StarterPackCard.Link
|
||||
starterPack={profile.joinedViaStarterPack}
|
||||
onPress={() => control.close()}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.mt_sm,
|
||||
a.p_lg,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<StarterPackCard.Card
|
||||
starterPack={profile.joinedViaStarterPack}
|
||||
/>
|
||||
</View>
|
||||
</StarterPackCard.Link>
|
||||
) : null}
|
||||
|
||||
{isNative && (
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={[a.mt_sm]}
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {BSKY_LABELER_DID, ModerationCause} from '@atproto/api'
|
||||
import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||
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 {Button} from '#/components/Button'
|
||||
import {
|
||||
ModerationDetailsDialog,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
type Component = React.ReactElement
|
||||
type Component = React.ReactElement<any>
|
||||
|
||||
type ContextType = {
|
||||
outlet: Component | null
|
||||
|
||||
@@ -139,7 +139,7 @@ function useHLS({
|
||||
playlist: string
|
||||
setHasSubtitleTrack: (v: boolean) => void
|
||||
setError: (v: Error | null) => void
|
||||
videoRef: React.RefObject<HTMLVideoElement>
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>
|
||||
setHlsLoading: (v: boolean) => void
|
||||
}) {
|
||||
const [Hls, setHls] = useState<typeof HlsTypes.default | undefined>(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateIcon} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {Text as TypoText} from '#/components/Typography'
|
||||
|
||||
export function Container({children}: {children: React.ReactNode}) {
|
||||
@@ -17,12 +18,12 @@ export function Container({children}: {children: React.ReactNode}) {
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.px_lg,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
a.rounded_sm,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
a.gap_lg,
|
||||
]}>
|
||||
{children}
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -51,8 +52,8 @@ export function RetryButton({onPress}: {onPress: () => void}) {
|
||||
onPress={onPress}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
variant="solid"
|
||||
label={_(msg`Retry`)}>
|
||||
<ButtonIcon icon={ArrowRotateIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {type SvgProps} from 'react-native-svg'
|
||||
import type React from 'react'
|
||||
|
||||
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
|
||||
@@ -2,7 +2,6 @@ import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {isFirefox, isTouchDevice} from '#/lib/browser'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
|
||||
@@ -46,14 +46,14 @@ export function Controls({
|
||||
hlsLoading,
|
||||
hasSubtitleTrack,
|
||||
}: {
|
||||
videoRef: React.RefObject<HTMLVideoElement>
|
||||
hlsRef: React.RefObject<Hls | undefined>
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>
|
||||
hlsRef: React.RefObject<Hls | undefined | null>
|
||||
active: boolean
|
||||
setActive: () => void
|
||||
focused: boolean
|
||||
setFocused: (focused: boolean) => void
|
||||
onScreen: boolean
|
||||
fullscreenRef: React.RefObject<HTMLDivElement>
|
||||
fullscreenRef: React.RefObject<HTMLDivElement | null>
|
||||
hlsLoading: boolean
|
||||
hasSubtitleTrack: boolean
|
||||
}) {
|
||||
@@ -232,7 +232,7 @@ export function Controls({
|
||||
}, [onSeek, videoRef])
|
||||
|
||||
const [showCursor, setShowCursor] = useState(true)
|
||||
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
const onPointerMoveEmptySpace = useCallback(() => {
|
||||
setShowCursor(true)
|
||||
if (cursorTimeoutRef.current) {
|
||||
@@ -264,7 +264,7 @@ export function Controls({
|
||||
[hovered],
|
||||
)
|
||||
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
|
||||
const onHoverWithTimeout = useCallback(() => {
|
||||
onHover()
|
||||
|
||||
@@ -3,7 +3,6 @@ import {View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
@@ -4,7 +4,7 @@ import {isSafari} from '#/lib/browser'
|
||||
import {logger} from '#/logger'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
|
||||
export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||
export function useVideoElement(ref: RefObject<HTMLVideoElement | null>) {
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [muted, setMuted] = useState(true)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
|
||||
@@ -71,7 +71,10 @@ export function VideoEmbed({embed, crop}: Props) {
|
||||
) : (
|
||||
<ConstrainedImage
|
||||
fullBleed={crop === 'square'}
|
||||
aspectRatio={constrained || 1}>
|
||||
aspectRatio={constrained || 1}
|
||||
// slightly smaller max height than images
|
||||
// images use 16 / 9, for reference
|
||||
minMobileAspectRatio={14 / 9}>
|
||||
{contents}
|
||||
</ConstrainedImage>
|
||||
)}
|
||||
|
||||