Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b410ba867f |
@@ -1,5 +1,5 @@
|
||||
name: "Bug Report"
|
||||
description: "Create a report for an issue you have experienced in the app."
|
||||
description: "Create a report for an issue you have experience in the app."
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -19,14 +19,13 @@ body:
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: upload
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
|
||||
validations:
|
||||
required: false
|
||||
accept: ".png,.jpg,.jpeg,.gif,.webp,.mp4,.mov,.webm"
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform(s) does this occur on?
|
||||
|
||||
@@ -26,14 +26,13 @@ body:
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: upload
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
|
||||
validations:
|
||||
required: false
|
||||
accept: ".png,.jpg,.jpeg,.gif,.webp,.mp4,.mov,.webm"
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform(s) does this occur on?
|
||||
|
||||
@@ -15,7 +15,7 @@ body:
|
||||
implement it in a timely manner.
|
||||
validations:
|
||||
required: true
|
||||
- type: upload
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
@@ -24,7 +24,6 @@ body:
|
||||
in or is missing from.
|
||||
validations:
|
||||
required: false
|
||||
accept: ".png,.jpg,.jpeg,.gif,.webp,.mp4,.mov,.webm"
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe Alternatives
|
||||
|
||||
Vendored
-135
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Codemod to replace namespaced React calls with named imports
|
||||
*
|
||||
* Before:
|
||||
* import React from 'react'
|
||||
* React.useEffect(() => {}, [])
|
||||
*
|
||||
* After:
|
||||
* import { useEffect } from 'react'
|
||||
* useEffect(() => {}, [])
|
||||
*
|
||||
* Usage: jscodeshift -t .jscodeshift/react-import.js <file-path>
|
||||
* Example: jscodeshift -t .jscodeshift/react-import.js src/App.native.tsx
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const parser = 'tsx'
|
||||
|
||||
export default function transformer(file, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(file.source)
|
||||
|
||||
// Find the React import
|
||||
let reactImportPath = null
|
||||
const reactMembers = new Set()
|
||||
|
||||
root.find(j.ImportDeclaration).forEach(path => {
|
||||
const node = path.value
|
||||
if (node.source.value === 'react') {
|
||||
node.specifiers.forEach(spec => {
|
||||
// Check if this is a default import of React
|
||||
if (
|
||||
spec.type === 'ImportDefaultSpecifier' &&
|
||||
spec.local.name === 'React'
|
||||
) {
|
||||
reactImportPath = path
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!reactImportPath) {
|
||||
// No React import found, nothing to do
|
||||
return file.source
|
||||
}
|
||||
|
||||
// Find all React.* member expressions
|
||||
root
|
||||
.find(j.MemberExpression)
|
||||
.filter(path => {
|
||||
const node = path.value
|
||||
return (
|
||||
node.object.type === 'Identifier' &&
|
||||
node.object.name === 'React' &&
|
||||
node.property.type === 'Identifier'
|
||||
)
|
||||
})
|
||||
.forEach(path => {
|
||||
const propertyName = path.value.property.name
|
||||
reactMembers.add(propertyName)
|
||||
})
|
||||
|
||||
// Find all React.* JSX member expressions (e.g., <React.Fragment>)
|
||||
root
|
||||
.find(j.JSXMemberExpression)
|
||||
.filter(path => {
|
||||
const node = path.value
|
||||
return node.object.name === 'React' && node.property.name
|
||||
})
|
||||
.forEach(path => {
|
||||
const propertyName = path.value.property.name
|
||||
reactMembers.add(propertyName)
|
||||
})
|
||||
|
||||
// If no React members are used, remove the import
|
||||
if (reactMembers.size === 0) {
|
||||
reactImportPath.prune()
|
||||
return root.toSource()
|
||||
}
|
||||
|
||||
// Sort the members for consistent output
|
||||
const sortedMembers = Array.from(reactMembers).sort()
|
||||
|
||||
// Create new import specifiers
|
||||
const newSpecifiers = sortedMembers.map(name =>
|
||||
j.importSpecifier(j.identifier(name), j.identifier(name)),
|
||||
)
|
||||
|
||||
// Get the existing import specifiers
|
||||
const sortedImports = Array.from(reactImportPath.value.specifiers).sort()
|
||||
const existingSpecifiers = sortedImports.filter(
|
||||
specifier => specifier.type !== 'ImportDefaultSpecifier',
|
||||
)
|
||||
|
||||
const allSpecifiers = [
|
||||
...new Map(
|
||||
[...existingSpecifiers, ...newSpecifiers].map(item => [
|
||||
item.imported.name,
|
||||
item,
|
||||
]),
|
||||
).values(),
|
||||
]
|
||||
|
||||
// Update the import declaration
|
||||
reactImportPath.value.specifiers = allSpecifiers
|
||||
|
||||
// Replace all React.* member expressions with just the identifier
|
||||
root
|
||||
.find(j.MemberExpression)
|
||||
.filter(path => {
|
||||
const node = path.value
|
||||
return (
|
||||
node.object.type === 'Identifier' &&
|
||||
node.object.name === 'React' &&
|
||||
node.property.type === 'Identifier'
|
||||
)
|
||||
})
|
||||
.replaceWith(path => {
|
||||
return j.identifier(path.value.property.name)
|
||||
})
|
||||
|
||||
// Replace all React.* JSX member expressions with just the identifier
|
||||
root
|
||||
.find(j.JSXMemberExpression)
|
||||
.filter(path => {
|
||||
const node = path.value
|
||||
return node.object.name === 'React' && node.property.name
|
||||
})
|
||||
.replaceWith(path => {
|
||||
return j.jsxIdentifier(path.value.property.name)
|
||||
})
|
||||
|
||||
return root.toSource()
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Codemod to replace namespaced React calls with named imports
|
||||
*
|
||||
* Before:
|
||||
* import * as Toast from '#/view/com/util/Toast'
|
||||
* Toast.show(message, 'xmark')
|
||||
*
|
||||
* After:
|
||||
* import * as Toast from '#/components/Toast'
|
||||
* Toast.show(message, {type: 'error'})
|
||||
*
|
||||
* Usage: jscodeshift -t .jscodeshift/toast-v2.js <file-path>
|
||||
* Example: jscodeshift -t .jscodeshift/toast-v2.js src/App.native.tsx
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const parser = 'tsx'
|
||||
|
||||
const OLD_IMPORT = '#/view/com/util/Toast'
|
||||
const NEW_IMPORT = '#/components/Toast'
|
||||
|
||||
const convertLegacyToastType = type => {
|
||||
switch (type) {
|
||||
// these ones are fine
|
||||
case 'default':
|
||||
case 'success':
|
||||
case 'error':
|
||||
case 'warning':
|
||||
case 'info':
|
||||
return type
|
||||
// legacy ones need conversion
|
||||
case 'xmark':
|
||||
return 'error'
|
||||
case 'exclamation-circle':
|
||||
return 'warning'
|
||||
case 'check':
|
||||
return 'success'
|
||||
case 'clipboard-check':
|
||||
return 'success'
|
||||
case 'circle-exclamation':
|
||||
case 'exclamation-circle':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
export default function transformer(file, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(file.source)
|
||||
|
||||
// Find Toast import declarations using the old path
|
||||
const toastImports = root
|
||||
.find(j.ImportDeclaration)
|
||||
.filter(path => path.value.source.value === OLD_IMPORT)
|
||||
|
||||
if (toastImports.length === 0) {
|
||||
return file.source
|
||||
}
|
||||
|
||||
// Update import path
|
||||
toastImports.forEach(path => {
|
||||
path.value.source.value = NEW_IMPORT
|
||||
})
|
||||
|
||||
// Collect all local names the Toast namespace is bound to
|
||||
const toastLocalNames = new Set()
|
||||
toastImports.forEach(path => {
|
||||
path.value.specifiers.forEach(spec => {
|
||||
if (spec.type === 'ImportNamespaceSpecifier') {
|
||||
toastLocalNames.add(spec.local.name)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Transform Toast.show(message, type) calls
|
||||
root.find(j.CallExpression).forEach(path => {
|
||||
const {callee, arguments: args} = path.value
|
||||
|
||||
// Match <ToastName>.show(...)
|
||||
if (
|
||||
callee.type !== 'MemberExpression' ||
|
||||
callee.object.type !== 'Identifier' ||
|
||||
!toastLocalNames.has(callee.object.name) ||
|
||||
callee.property.name !== 'show'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only transform 2-arg calls where the second arg is a string literal
|
||||
if (args.length !== 2) return
|
||||
const typeArg = args[1]
|
||||
if (typeArg.type !== 'StringLiteral' && typeArg.type !== 'Literal') return
|
||||
|
||||
const legacyType = typeArg.value
|
||||
const newType = convertLegacyToastType(legacyType)
|
||||
|
||||
// Replace the second argument with an options object: {type: 'newType'}
|
||||
args[1] = j.objectExpression([
|
||||
j.property('init', j.identifier('type'), j.stringLiteral(newType)),
|
||||
])
|
||||
})
|
||||
|
||||
return root.toSource()
|
||||
}
|
||||
@@ -91,8 +91,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn: "Add user to list"
|
||||
- swipe:
|
||||
direction: DOWN
|
||||
- assertVisible:
|
||||
id: "profileCard-bob.test-link"
|
||||
- assertVisible: "View Bob's profile"
|
||||
|
||||
- tapOn: "Posts"
|
||||
- assertVisible:
|
||||
@@ -124,8 +123,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
- tapOn: "People"
|
||||
- assertVisible:
|
||||
id: "profileCard-bob.test-link"
|
||||
- assertVisible: "View Bob's profile"
|
||||
- tapOn:
|
||||
point: "90%,43%"
|
||||
- tapOn:
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"test": "npm run test:unit && npm run test:e2e",
|
||||
"test:e2e": "./tests/infra/with-test-db.sh node --loader ts-node/esm --test ./tests/index.ts",
|
||||
"test:unit": "node --loader ts-node/esm --test ./src/*.test.ts",
|
||||
"test": "./tests/infra/with-test-db.sh node --loader ts-node/esm --test ./tests/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -15,7 +15,6 @@ export type ServiceConfig = {
|
||||
safelinkPdsUrl?: string
|
||||
safelinkAgentIdentifier?: string
|
||||
safelinkAgentPass?: string
|
||||
metricsApiHost?: string
|
||||
}
|
||||
|
||||
export type DbConfig = {
|
||||
@@ -46,7 +45,6 @@ export type Environment = {
|
||||
safelinkPdsUrl?: string
|
||||
safelinkAgentIdentifier?: string
|
||||
safelinkAgentPass?: string
|
||||
metricsApiHost?: string
|
||||
}
|
||||
|
||||
export const readEnv = (): Environment => {
|
||||
@@ -67,7 +65,6 @@ export const readEnv = (): Environment => {
|
||||
safelinkPdsUrl: envStr('LINK_SAFELINK_PDS_URL'),
|
||||
safelinkAgentIdentifier: envStr('LINK_SAFELINK_AGENT_IDENTIFIER'),
|
||||
safelinkAgentPass: envStr('LINK_SAFELINK_AGENT_PASS'),
|
||||
metricsApiHost: envStr('LINK_METRICS_API_HOST'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +79,6 @@ export const envToCfg = (env: Environment): Config => {
|
||||
safelinkPdsUrl: env.safelinkPdsUrl,
|
||||
safelinkAgentIdentifier: env.safelinkAgentIdentifier,
|
||||
safelinkAgentPass: env.safelinkAgentPass,
|
||||
metricsApiHost: env.metricsApiHost,
|
||||
}
|
||||
if (!env.dbPostgresUrl) {
|
||||
throw new Error('Must configure postgres url (LINK_DB_POSTGRES_URL)')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {SafelinkClient} from './cache/safelinkClient.js'
|
||||
import {type Config} from './config.js'
|
||||
import Database from './db/index.js'
|
||||
import {MetricsClient} from './metrics.js'
|
||||
|
||||
export type AppContextOptions = {
|
||||
cfg: Config
|
||||
@@ -13,7 +12,6 @@ export class AppContext {
|
||||
db: Database
|
||||
safelinkClient: SafelinkClient
|
||||
abortController = new AbortController()
|
||||
metrics: MetricsClient
|
||||
|
||||
constructor(private opts: AppContextOptions) {
|
||||
this.cfg = this.opts.cfg
|
||||
@@ -22,9 +20,6 @@ export class AppContext {
|
||||
cfg: this.opts.cfg.service,
|
||||
db: this.opts.db,
|
||||
})
|
||||
this.metrics = new MetricsClient({
|
||||
trackingEndpoint: this.opts.cfg.service.metricsApiHost,
|
||||
})
|
||||
}
|
||||
|
||||
static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) {
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
|
||||
export function linkRedirectContents(link: string): string {
|
||||
// Encode characters that could break out of the single-quoted URL in meta refresh.
|
||||
// HTML entity escaping (') is insufficient because the browser decodes entities
|
||||
// before the meta refresh parser processes the URL, allowing apostrophes to
|
||||
// prematurely terminate the URL string.
|
||||
//
|
||||
// Example: "They're" with HTML escaping becomes "They're" in HTML, but after
|
||||
// the browser decodes the content attribute, the meta refresh parser sees "They're"
|
||||
// and interprets the apostrophe as the closing quote, truncating the URL to "They".
|
||||
const safeLink = link.replace(/'/g, '%27')
|
||||
|
||||
return `
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; URL='${escapeHTML(safeLink)}'" />
|
||||
<meta http-equiv="refresh" content="0; URL='${escapeHTML(link)}'" />
|
||||
<meta
|
||||
http-equiv="Cache-Control"
|
||||
content="no-store, no-cache, must-revalidate, max-age=0" />
|
||||
|
||||
@@ -36,7 +36,6 @@ export class LinkService {
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.ctx.metrics.start()
|
||||
this.server = this.app.listen(this.ctx.cfg.service.port)
|
||||
this.server.keepAliveTimeout = 90000
|
||||
this.terminator = createHttpTerminator({server: this.server})
|
||||
@@ -47,6 +46,5 @@ export class LinkService {
|
||||
this.ctx.abortController.abort()
|
||||
await this.terminator?.terminate()
|
||||
await this.ctx.db.close()
|
||||
this.ctx.metrics.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
import {afterEach, beforeEach, describe, it, mock} from 'node:test'
|
||||
|
||||
import {httpLogger} from './logger.js'
|
||||
import {MetricsClient} from './metrics.js'
|
||||
|
||||
type TestEvents = {
|
||||
click: {button: string}
|
||||
view: {screen: string}
|
||||
}
|
||||
|
||||
describe('MetricsClient', () => {
|
||||
let fetchMock: ReturnType<typeof mock.fn>
|
||||
let fetchRequests: {body: any}[]
|
||||
let client: MetricsClient<TestEvents>
|
||||
let loggerErrorMock: ReturnType<typeof mock.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
mock.timers.enable({apis: ['setInterval', 'setTimeout']})
|
||||
fetchRequests = []
|
||||
fetchMock = mock.fn(async (_url: any, options: any) => {
|
||||
const body = JSON.parse(options.body)
|
||||
fetchRequests.push({body})
|
||||
return {ok: true, status: 200, text: async () => ''}
|
||||
})
|
||||
;(globalThis as any).fetch = fetchMock
|
||||
loggerErrorMock = mock.fn()
|
||||
httpLogger.error = loggerErrorMock as any
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
client?.stop()
|
||||
mock.timers.reset()
|
||||
mock.restoreAll()
|
||||
})
|
||||
|
||||
it('flushes events on interval', async () => {
|
||||
client = new MetricsClient<TestEvents>({
|
||||
trackingEndpoint: 'https://test.metrics.api',
|
||||
})
|
||||
client.track('click', {button: 'submit'})
|
||||
client.track('view', {screen: 'home'})
|
||||
|
||||
assert.strictEqual(fetchRequests.length, 0)
|
||||
|
||||
mock.timers.tick(10_000)
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchRequests.length, 1)
|
||||
assert.strictEqual(fetchRequests[0].body.events.length, 2)
|
||||
assert.strictEqual(fetchRequests[0].body.events[0].event, 'click')
|
||||
assert.strictEqual(fetchRequests[0].body.events[1].event, 'view')
|
||||
})
|
||||
|
||||
it('flushes when maxBatchSize is exceeded', async () => {
|
||||
client = new MetricsClient<TestEvents>({
|
||||
trackingEndpoint: 'https://test.metrics.api',
|
||||
})
|
||||
client.maxBatchSize = 5
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
client.track('click', {button: `btn-${i}`})
|
||||
}
|
||||
|
||||
assert.strictEqual(fetchRequests.length, 0)
|
||||
|
||||
client.track('click', {button: 'btn-trigger'})
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchRequests.length, 1)
|
||||
assert.strictEqual(fetchRequests[0].body.events.length, 6)
|
||||
})
|
||||
|
||||
it('logs error on failed request', async () => {
|
||||
fetchMock.mock.mockImplementation(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => 'Internal Server Error',
|
||||
}
|
||||
})
|
||||
|
||||
client = new MetricsClient<TestEvents>({
|
||||
trackingEndpoint: 'https://test.metrics.api',
|
||||
})
|
||||
client.track('click', {button: 'submit'})
|
||||
|
||||
mock.timers.tick(10_000)
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchMock.mock.callCount(), 1)
|
||||
assert.strictEqual(loggerErrorMock.mock.callCount(), 1)
|
||||
const call = loggerErrorMock.mock.calls[0]
|
||||
const arg = call.arguments[0] as {err: Error}
|
||||
assert.ok(arg.err instanceof Error)
|
||||
assert.strictEqual(call.arguments[1], 'Failed to send metrics')
|
||||
})
|
||||
|
||||
it('handles fetch text() error gracefully', async () => {
|
||||
fetchMock.mock.mockImplementation(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => {
|
||||
throw new Error('Failed to read response')
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
client = new MetricsClient<TestEvents>({
|
||||
trackingEndpoint: 'https://test.metrics.api',
|
||||
})
|
||||
client.track('click', {button: 'submit'})
|
||||
|
||||
mock.timers.tick(10_000)
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchMock.mock.callCount(), 1)
|
||||
assert.strictEqual(loggerErrorMock.mock.callCount(), 1)
|
||||
const call = loggerErrorMock.mock.calls[0]
|
||||
const arg = call.arguments[0] as {err: Error}
|
||||
assert.ok(arg.err instanceof Error)
|
||||
assert.match(arg.err.message, /Unknown error/)
|
||||
assert.strictEqual(call.arguments[1], 'Failed to send metrics')
|
||||
})
|
||||
|
||||
it('flushes when stop() is called', async () => {
|
||||
client = new MetricsClient<TestEvents>({
|
||||
trackingEndpoint: 'https://test.metrics.api',
|
||||
})
|
||||
client.track('click', {button: 'submit'})
|
||||
|
||||
assert.strictEqual(fetchRequests.length, 0)
|
||||
|
||||
client.stop()
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchRequests.length, 1)
|
||||
assert.strictEqual(fetchRequests[0].body.events.length, 1)
|
||||
assert.strictEqual(fetchRequests[0].body.events[0].event, 'click')
|
||||
})
|
||||
|
||||
it('does not send if trackingEndpoint is not configured', async () => {
|
||||
client = new MetricsClient<TestEvents>({})
|
||||
client.track('click', {button: 'submit'})
|
||||
|
||||
mock.timers.tick(10_000)
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchMock.mock.callCount(), 0)
|
||||
})
|
||||
|
||||
it('start() is idempotent', async () => {
|
||||
client = new MetricsClient<TestEvents>({
|
||||
trackingEndpoint: 'https://test.metrics.api',
|
||||
})
|
||||
|
||||
client.track('click', {button: 'submit'})
|
||||
client.start()
|
||||
client.start()
|
||||
|
||||
mock.timers.tick(10_000)
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchRequests.length, 1)
|
||||
})
|
||||
|
||||
it('does not flush if queue is empty', async () => {
|
||||
client = new MetricsClient<TestEvents>({
|
||||
trackingEndpoint: 'https://test.metrics.api',
|
||||
})
|
||||
client.start()
|
||||
|
||||
mock.timers.tick(10_000)
|
||||
await flush()
|
||||
|
||||
assert.strictEqual(fetchMock.mock.callCount(), 0)
|
||||
})
|
||||
})
|
||||
|
||||
function flush() {
|
||||
return new Promise(r => setImmediate(r))
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import {httpLogger} from './logger.js'
|
||||
|
||||
/**
|
||||
* New metrics events should be added here
|
||||
*/
|
||||
type Events = {
|
||||
redirect: {
|
||||
link: string
|
||||
whitelisted: 'unknown' | 'yes'
|
||||
blocked: boolean
|
||||
warned: boolean
|
||||
utm_source?: string
|
||||
utm_medium?: string
|
||||
utm_campaign?: string
|
||||
utm_content?: string
|
||||
utm_term?: string
|
||||
}
|
||||
invalid_redirect: {
|
||||
link: string
|
||||
}
|
||||
}
|
||||
|
||||
type Event<M extends Record<string, any>> = {
|
||||
time: number
|
||||
event: keyof M
|
||||
payload: M[keyof M]
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export type Config = {
|
||||
trackingEndpoint?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This MetricsClient is duplicated from both `social-app` and `atproto`
|
||||
* codebases.
|
||||
*/
|
||||
export class MetricsClient<M extends Record<string, any> = Events> {
|
||||
maxBatchSize = 100
|
||||
|
||||
private disabled: boolean = false
|
||||
private started: boolean = false
|
||||
private queue: Event<M>[] = []
|
||||
private flushInterval: NodeJS.Timeout | null = null
|
||||
constructor(private config: Config) {
|
||||
this.disabled = !config.trackingEndpoint
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.disabled) return
|
||||
if (this.started) return
|
||||
this.started = true
|
||||
this.flushInterval = setInterval(() => {
|
||||
this.flush()
|
||||
}, 10_000)
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.flushInterval) {
|
||||
clearInterval(this.flushInterval)
|
||||
this.flushInterval = null
|
||||
}
|
||||
this.flush()
|
||||
}
|
||||
|
||||
track<E extends keyof M>(event: E, payload: M[E]) {
|
||||
if (this.disabled) return
|
||||
|
||||
this.start()
|
||||
|
||||
/**
|
||||
* deviceId is required for sharding events in Middleman. To avoid a hot
|
||||
* shard, we generate a random anonymous IDs for this client.
|
||||
*
|
||||
* @see https://github.com/bluesky-social/tango/blob/d5819cde419d13e0d2cf837f4b30d48529d64060/middleman/handlers_tracking.go#L195
|
||||
*/
|
||||
const anonId = `anon-${crypto.randomUUID()}`
|
||||
|
||||
/**
|
||||
* Event structure is like this to ensure compat with Middleman, which
|
||||
* receives events like this from other codebases, including `social-app`.
|
||||
*/
|
||||
const e = {
|
||||
source: 'blink',
|
||||
time: Date.now(),
|
||||
event,
|
||||
payload,
|
||||
metadata: {
|
||||
base: {
|
||||
deviceId: anonId,
|
||||
sessionId: anonId,
|
||||
},
|
||||
session: {
|
||||
did: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
this.queue.push(e)
|
||||
|
||||
if (this.queue.length > this.maxBatchSize) {
|
||||
this.flush()
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (this.disabled) return
|
||||
if (!this.queue.length) return
|
||||
const events = this.queue.splice(0, this.queue.length)
|
||||
this.sendBatch(events)
|
||||
}
|
||||
|
||||
private async sendBatch(events: Event<M>[]) {
|
||||
if (this.disabled || !this.config.trackingEndpoint) return
|
||||
|
||||
try {
|
||||
const res = await fetch(this.config.trackingEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({events}),
|
||||
keepalive: true,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => 'Unknown error')
|
||||
httpLogger.error(
|
||||
{err: new Error(`${res.status} Failed to fetch - ${errorText}`)},
|
||||
'Failed to send metrics',
|
||||
)
|
||||
} else {
|
||||
// Drain response body to allow connection reuse.
|
||||
await res.text().catch(() => {})
|
||||
}
|
||||
} catch (err) {
|
||||
httpLogger.error({err}, 'Failed to send metrics')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ export default function (ctx: AppContext, app: Express) {
|
||||
url.pathname === '/redirect') || // is a redirect loop
|
||||
INTERNAL_IP_REGEX.test(url.hostname) // isn't directing to an internal location
|
||||
) {
|
||||
ctx.metrics.track('invalid_redirect', {link})
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
|
||||
return res.status(302).end()
|
||||
@@ -49,9 +48,6 @@ export default function (ctx: AppContext, app: Express) {
|
||||
res.type('html')
|
||||
|
||||
let html: string | undefined
|
||||
let whitelisted: 'unknown' | 'yes' = 'unknown'
|
||||
let blocked: boolean = false
|
||||
let warned: boolean = false
|
||||
|
||||
if (ctx.cfg.service.safelinkEnabled) {
|
||||
const rule = await ctx.safelinkClient.tryFindRule(link)
|
||||
@@ -59,7 +55,6 @@ export default function (ctx: AppContext, app: Express) {
|
||||
switch (rule.action) {
|
||||
case 'whitelist':
|
||||
redirectLogger.info({rule}, 'Whitelist rule matched')
|
||||
whitelisted = 'yes'
|
||||
break
|
||||
case 'block':
|
||||
html = linkWarningLayout(
|
||||
@@ -71,7 +66,6 @@ export default function (ctx: AppContext, app: Express) {
|
||||
)
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
redirectLogger.info({rule}, 'Block rule matched')
|
||||
blocked = true
|
||||
break
|
||||
case 'warn':
|
||||
html = linkWarningLayout(
|
||||
@@ -83,7 +77,6 @@ export default function (ctx: AppContext, app: Express) {
|
||||
)
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
redirectLogger.info({rule}, 'Warn rule matched')
|
||||
warned = true
|
||||
break
|
||||
default:
|
||||
redirectLogger.warn({rule}, 'Unknown rule matched')
|
||||
@@ -96,18 +89,6 @@ export default function (ctx: AppContext, app: Express) {
|
||||
html = linkRedirectContents(url.href)
|
||||
}
|
||||
|
||||
ctx.metrics.track('redirect', {
|
||||
link,
|
||||
whitelisted,
|
||||
blocked,
|
||||
warned,
|
||||
utm_source: req.query.utm_source?.toString(),
|
||||
utm_medium: req.query.utm_medium?.toString(),
|
||||
utm_campaign: req.query.utm_campaign?.toString(),
|
||||
utm_content: req.query.utm_content?.toString(),
|
||||
utm_term: req.query.utm_term?.toString(),
|
||||
})
|
||||
|
||||
return res.end(html)
|
||||
}),
|
||||
)
|
||||
|
||||
+9
-29
@@ -2,9 +2,11 @@ import assert from 'node:assert'
|
||||
import {type AddressInfo} from 'node:net'
|
||||
import {after, before, describe, it} from 'node:test'
|
||||
|
||||
import {ToolsOzoneSafelinkDefs} from '@atproto/api'
|
||||
|
||||
import {Database, envToCfg, LinkService, readEnv} from '../src/index.js'
|
||||
|
||||
describe.skip('link service', async () => {
|
||||
describe('link service', async () => {
|
||||
let linkService: LinkService
|
||||
let baseUrl: string
|
||||
before(async () => {
|
||||
@@ -16,9 +18,9 @@ describe.skip('link service', async () => {
|
||||
dbPostgresSchema: 'link_test',
|
||||
dbPostgresUrl: process.env.DB_POSTGRES_URL,
|
||||
safelinkEnabled: true,
|
||||
safelinkPdsUrl: 'http://localhost:2583',
|
||||
safelinkAgentIdentifier: 'mod-authority.test',
|
||||
safelinkAgentPass: 'hunter2',
|
||||
ozoneUrl: 'http://localhost:2583',
|
||||
ozoneAgentHandle: 'mod-authority.test',
|
||||
ozoneAgentPass: 'hunter2',
|
||||
})
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.url,
|
||||
@@ -31,7 +33,6 @@ describe.skip('link service', async () => {
|
||||
const {port} = linkService.server?.address() as AddressInfo
|
||||
baseUrl = `http://localhost:${port}`
|
||||
|
||||
/*
|
||||
// Ensure blocklist, whitelist, and safelink rules are set up
|
||||
const now = new Date().toISOString()
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
@@ -109,7 +110,6 @@ describe.skip('link service', async () => {
|
||||
comment:
|
||||
'Could be quite the mistake to get into this addicting game, but we will warn instead of block',
|
||||
})
|
||||
*/
|
||||
})
|
||||
after(async () => {
|
||||
await linkService?.destroy()
|
||||
@@ -213,7 +213,6 @@ describe.skip('link service', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
/*
|
||||
it('Rule adjustment, safe redirect, 200 response for Instagram Account of teamsesh Bones', async () => {
|
||||
// Retrieve the latest event after all updates
|
||||
const result = linkService.ctx.cfg.eventCache.smartGet(
|
||||
@@ -233,7 +232,6 @@ describe.skip('link service', async () => {
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
})
|
||||
*/
|
||||
|
||||
async function getRedirect(link: string): Promise<[number, string]> {
|
||||
const url = new URL(link)
|
||||
@@ -293,10 +291,9 @@ describe('link service no safelink', async () => {
|
||||
dbPostgresSchema: 'link_test',
|
||||
dbPostgresUrl: process.env.DB_POSTGRES_URL,
|
||||
safelinkEnabled: false,
|
||||
safelinkPdsUrl: 'http://localhost:2583',
|
||||
safelinkAgentIdentifier: 'mod-authority.test',
|
||||
safelinkAgentPass: 'hunter2',
|
||||
metricsApiHost: 'http://localhost:2584',
|
||||
ozoneUrl: 'http://localhost:2583',
|
||||
ozoneAgentHandle: 'mod-authority.test',
|
||||
ozoneAgentPass: 'hunter2',
|
||||
})
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.url,
|
||||
@@ -360,21 +357,4 @@ describe('link service no safelink', async () => {
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
|
||||
it('normal redirect with query params', async () => {
|
||||
const urlToRedirect = 'https://bsky.app/settings'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
url.searchParams.set('utm_source', 'test')
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"ts-node": {
|
||||
"logError": true,
|
||||
"pretty": true /* <= technically not required */
|
||||
}
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {type SVGAttributes} from 'react'
|
||||
|
||||
export function Butterfly(props: React.SVGAttributes<SVGSVGElement>) {
|
||||
export function Butterfly(props: SVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import {type ImgHTMLAttributes} from 'react'
|
||||
|
||||
// @NOTE satori does not currently support webp, see vercel/satori#273
|
||||
function detectMime(buf: Buffer): string {
|
||||
@@ -10,7 +10,7 @@ function detectMime(buf: Buffer): string {
|
||||
}
|
||||
|
||||
export function Img(
|
||||
props: Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & {src: Buffer},
|
||||
props: Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> & {src: Buffer},
|
||||
) {
|
||||
const {src, ...others} = props
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable bsky-internal/avoid-unwrapped-text */
|
||||
import React from 'react'
|
||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
|
||||
import {Butterfly} from './Butterfly.js'
|
||||
import {Img} from './Img.js'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
import React from 'react'
|
||||
import {type AppBskyGraphDefs, AtUri} from '@atproto/api'
|
||||
import resvg from '@resvg/resvg-js'
|
||||
import {type Express} from 'express'
|
||||
|
||||
@@ -590,14 +590,6 @@ func (srv *Server) WebPost(c echo.Context) error {
|
||||
if postView.Embed.EmbedVideo_View.Thumbnail != nil {
|
||||
data["imgThumbUrls"] = []string{*postView.Embed.EmbedVideo_View.Thumbnail}
|
||||
}
|
||||
if postView.Embed.EmbedVideo_View.Playlist != "" {
|
||||
data["videoUrl"] = postView.Embed.EmbedVideo_View.Playlist
|
||||
data["videoType"] = "application/vnd.apple.mpegurl"
|
||||
if postView.Embed.EmbedVideo_View.AspectRatio != nil {
|
||||
data["videoWidth"] = postView.Embed.EmbedVideo_View.AspectRatio.Width
|
||||
data["videoHeight"] = postView.Embed.EmbedVideo_View.AspectRatio.Height
|
||||
}
|
||||
}
|
||||
} else if hasMediaImages {
|
||||
var thumbUrls []string
|
||||
for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images {
|
||||
@@ -608,14 +600,6 @@ func (srv *Server) WebPost(c echo.Context) error {
|
||||
if postView.Embed.EmbedRecordWithMedia_View.Media.EmbedVideo_View.Thumbnail != nil {
|
||||
data["imgThumbUrls"] = []string{*postView.Embed.EmbedRecordWithMedia_View.Media.EmbedVideo_View.Thumbnail}
|
||||
}
|
||||
if postView.Embed.EmbedRecordWithMedia_View.Media.EmbedVideo_View.Playlist != "" {
|
||||
data["videoUrl"] = postView.Embed.EmbedRecordWithMedia_View.Media.EmbedVideo_View.Playlist
|
||||
data["videoType"] = "application/vnd.apple.mpegurl"
|
||||
if postView.Embed.EmbedRecordWithMedia_View.Media.EmbedVideo_View.AspectRatio != nil {
|
||||
data["videoWidth"] = postView.Embed.EmbedRecordWithMedia_View.Media.EmbedVideo_View.AspectRatio.Width
|
||||
data["videoHeight"] = postView.Embed.EmbedRecordWithMedia_View.Media.EmbedVideo_View.AspectRatio.Height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,14 +34,6 @@
|
||||
<meta property="twitter:image" content="{{ imgThumbUrl }}">
|
||||
{% endfor %}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{%- if videoUrl %}
|
||||
<meta property="og:video" content="{{ videoUrl }}">
|
||||
<meta property="og:video:type" content="{{ videoType }}">
|
||||
{%- if videoWidth %}
|
||||
<meta property="og:video:width" content="{{ videoWidth }}">
|
||||
<meta property="og:video:height" content="{{ videoHeight }}">
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% else %}
|
||||
<meta property="og:image" content="{{ postView.Author.Avatar|avatar_thumbnail }}">
|
||||
<meta property="twitter:image" content="{{ postView.Author.Avatar|avatar_thumbnail }}">
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"name": "dev-env",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"start": "NODE_ENV=development ./dev-infra/with-test-redis-and-db.sh ts-node ./mock-server.ts"
|
||||
"e2e:mock-server": "NODE_ENV=development ./dev-infra/with-test-redis-and-db.sh ts-node ./mock-server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/dev-env": "^0.3.215",
|
||||
"@atproto/dev-env": "^0.3.213",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^6.0.2"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"#/*": ["./src/*"],
|
||||
"lib/*": ["./src/lib/*"],
|
||||
@@ -44,4 +43,4 @@
|
||||
"metro.config.js",
|
||||
"jest.config.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
+77
-193
@@ -64,14 +64,14 @@
|
||||
"@atproto/xrpc" "^0.7.6"
|
||||
"@atproto/xrpc-server" "^0.10.0"
|
||||
|
||||
"@atproto/api@^0.19.4":
|
||||
version "0.19.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.4.tgz#f3ff850baf4d85538c082fb91aa0982737eb68be"
|
||||
integrity sha512-fYNM62vdXxer0h8a9Jzl4/ag9uFIe0nTO+LkC6KTlx1yUDigrAoQMMbllIiCWj62GhUMxAkHabk/BZjjVAfKng==
|
||||
"@atproto/api@^0.19.2":
|
||||
version "0.19.3"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.3.tgz#61de8d2e31abe9eb2b4c8f4ad124ed79d4a77e89"
|
||||
integrity sha512-G8YpBpRouHdTAIagi/QQIUZOhGd1jfBQWkJy9QfxAzjjEpPvaVOSk4e1S85QzGLm/xbzVONzGkmdtiOSfP6wVg==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.4.18"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
"@atproto/xrpc" "^0.7.7"
|
||||
await-lock "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
@@ -96,23 +96,23 @@
|
||||
multiformats "^9.9.0"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/bsky@^0.0.221":
|
||||
version "0.0.221"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.221.tgz#b574456225db66c866848526947473d60bb4d0e7"
|
||||
integrity sha512-feNR6xkJ9HCJbQdJU3ytsnAfaSBXJdaXIMi0tNPrsLfoqCWWbTfXIcMczXeY4SOhKGFR5oMYxE9zrRC/TTAssw==
|
||||
"@atproto/bsky@^0.0.219":
|
||||
version "0.0.219"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.219.tgz#6a9f82eb4ab999e121d04ad2b3f08ff60cc75fba"
|
||||
integrity sha512-Vm7JpIyCqd7sHzHsXqppGSaKkXKUhvdZ/UOldc247Bgmx+L/U+E6IeR028hzMr1YyDvU+bkO1hlqT8uUovOCdA==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch-node" "^0.2.0"
|
||||
"@atproto-labs/xrpc-utils" "^0.0.24"
|
||||
"@atproto/api" "^0.19.4"
|
||||
"@atproto/common" "^0.5.15"
|
||||
"@atproto/api" "^0.19.2"
|
||||
"@atproto/common" "^0.5.14"
|
||||
"@atproto/crypto" "^0.4.5"
|
||||
"@atproto/did" "^0.3.0"
|
||||
"@atproto/identity" "^0.4.12"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
"@atproto/repo" "^0.8.13"
|
||||
"@atproto/repo" "^0.8.12"
|
||||
"@atproto/sync" "^0.1.40"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/xrpc-server" "^0.10.17"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
"@atproto/xrpc-server" "^0.10.15"
|
||||
"@bufbuild/protobuf" "^1.5.0"
|
||||
"@connectrpc/connect" "^1.1.4"
|
||||
"@connectrpc/connect-express" "^1.1.4"
|
||||
@@ -146,13 +146,13 @@
|
||||
undici "^6.19.8"
|
||||
zod "3.23.8"
|
||||
|
||||
"@atproto/bsync@^0.0.25":
|
||||
version "0.0.25"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.25.tgz#0d6056f844c0b2579d9dfc04b6727974dc03fcd6"
|
||||
integrity sha512-5tjP5QbUcNtMBw7FJeyRfA0OHQRKrg97Jva6Q26cKqLMICGYNbx0fpD7nZhGTP2/s1gd6UZkG9Zdh4GRZpbxWg==
|
||||
"@atproto/bsync@^0.0.24":
|
||||
version "0.0.24"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.24.tgz#6b0d4b02c0c0241687456ab817471d36ee81ae61"
|
||||
integrity sha512-JN+oncaPBNRjzjTPGR7Q1fkKF3cqOQ6oLRrAh9kVU04ZS3FhWUG8cQvnr8wb1PUhFb/XYpWkwDw5+GIhdb7Lfw==
|
||||
dependencies:
|
||||
"@atproto/common" "^0.5.14"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
"@bufbuild/protobuf" "^1.5.0"
|
||||
"@connectrpc/connect" "^1.1.4"
|
||||
"@connectrpc/connect-node" "^1.1.4"
|
||||
@@ -172,16 +172,6 @@
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/common-web@^0.4.19":
|
||||
version "0.4.19"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.19.tgz#bbd7f84f545ebe73ca3bc00314ccf4ee66e7069e"
|
||||
integrity sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==
|
||||
dependencies:
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
"@atproto/lex-json" "^0.0.14"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/common@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210"
|
||||
@@ -213,17 +203,6 @@
|
||||
multiformats "^9.9.0"
|
||||
pino "^8.21.0"
|
||||
|
||||
"@atproto/common@^0.5.15":
|
||||
version "0.5.15"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.5.15.tgz#3c43c25d3493d868cc4281d6ac2f923b00644463"
|
||||
integrity sha512-+cdfdMPAIbH9zQGLfH1gNY2KEZsMxj0EelVQL5uJUFL+UkkAXiiqWj7J5mbax8sf02cC/afJnfkWzERNAheKoA==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.4.19"
|
||||
"@atproto/lex-cbor" "^0.0.15"
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
multiformats "^9.9.0"
|
||||
pino "^8.21.0"
|
||||
|
||||
"@atproto/crypto@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568"
|
||||
@@ -244,23 +223,23 @@
|
||||
"@noble/hashes" "^1.6.1"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/dev-env@^0.3.215":
|
||||
version "0.3.215"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.215.tgz#9da8c4a73abb4501ac72c73da9536e2ec86cb46f"
|
||||
integrity sha512-zwZwGWYLgP2Zdie6/gMtxuDbSs7/UV/gPJYfOlXln1ZDoMkfFbgCTq44PWRnJpUKTzrq7gt19E0GsL7DMkppjA==
|
||||
"@atproto/dev-env@^0.3.213":
|
||||
version "0.3.213"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.213.tgz#30ca66f827d44ccabb02b359119f68549fa3edf0"
|
||||
integrity sha512-Bjhv+zzcQxhwV4I7si+yDls8sSetksILeiBemfRe3cvE4GOgs7KfsYI5+pqNmBZ5rrFPt3KUeN9vIo31LCgZOw==
|
||||
dependencies:
|
||||
"@atproto/api" "^0.19.4"
|
||||
"@atproto/bsky" "^0.0.221"
|
||||
"@atproto/bsync" "^0.0.25"
|
||||
"@atproto/common-web" "^0.4.19"
|
||||
"@atproto/api" "^0.19.2"
|
||||
"@atproto/bsky" "^0.0.219"
|
||||
"@atproto/bsync" "^0.0.24"
|
||||
"@atproto/common-web" "^0.4.18"
|
||||
"@atproto/crypto" "^0.4.5"
|
||||
"@atproto/identity" "^0.4.12"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
"@atproto/ozone" "^0.1.167"
|
||||
"@atproto/pds" "^0.4.216"
|
||||
"@atproto/ozone" "^0.1.166"
|
||||
"@atproto/pds" "^0.4.214"
|
||||
"@atproto/sync" "^0.1.40"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/xrpc-server" "^0.10.17"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
"@atproto/xrpc-server" "^0.10.15"
|
||||
"@did-plc/lib" "^0.0.1"
|
||||
"@did-plc/server" "^0.0.1"
|
||||
dotenv "^16.0.3"
|
||||
@@ -309,14 +288,6 @@
|
||||
"@atproto/lex-data" "^0.0.13"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-cbor@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-cbor/-/lex-cbor-0.0.15.tgz#ae4558d8ce22119710ad22feb458d22774b3ca3b"
|
||||
integrity sha512-3osDicK9bAMXJlKjLKqwYrhLQ60bOguWBNjE+fuNjMuizNzC0aqaClE3d+qMsFuFq9bjEHFw+4Vr9Qmd/m6VYg==
|
||||
dependencies:
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-client@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-client/-/lex-client-0.0.15.tgz#c647d14e91ca3f52feebf4b34f80abb7e93b3bee"
|
||||
@@ -327,16 +298,6 @@
|
||||
"@atproto/lex-schema" "^0.0.14"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-client@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-client/-/lex-client-0.0.17.tgz#566689a288f8b2af31f4a0fa081496dfaaef7278"
|
||||
integrity sha512-lZ9clUjWgpno1XhSawQP+1/JeIYA9qBh759b/NSU0OiypQqgq7IxDvmzaBsiHK1sqjo0tyEkmG4X5Ym7YXjv0Q==
|
||||
dependencies:
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
"@atproto/lex-json" "^0.0.14"
|
||||
"@atproto/lex-schema" "^0.0.16"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-data@^0.0.13":
|
||||
version "0.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.13.tgz#db1bcfa12d5056210f6eb7f3b8bac909909d6b9c"
|
||||
@@ -347,22 +308,12 @@
|
||||
uint8arrays "3.0.0"
|
||||
unicode-segmenter "^0.14.0"
|
||||
|
||||
"@atproto/lex-data@^0.0.14":
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.14.tgz#2f2f3c64699925a0d4785e5afd0e7731ba1d46c0"
|
||||
integrity sha512-53DUa9664SS76nGAMYopWsO10OH0AAdf7P/HSKB6Wzx3iqe6lk/K61QZnKxOG1LreYl5CfvIJU6eNf4txI6GlQ==
|
||||
"@atproto/lex-document@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.15.tgz#b2f19756291a0d259cd99f5ebe4872e9133069b6"
|
||||
integrity sha512-QT2MbICG4cTFrrA19SIHpZJ33WRLdzjhDsEhSknQ4dE5CjqPf4BP9LaC4pOeW8NE5Kn92hgIm3JWNjoak8blXw==
|
||||
dependencies:
|
||||
multiformats "^9.9.0"
|
||||
tslib "^2.8.1"
|
||||
uint8arrays "3.0.0"
|
||||
unicode-segmenter "^0.14.0"
|
||||
|
||||
"@atproto/lex-document@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-document/-/lex-document-0.0.17.tgz#8460096235910bf5ec8305f03a8da6bab8d8a12b"
|
||||
integrity sha512-rQiDCSYQwze4+kaArUtmp4bjZ9rV3vYUMhjdDwmZCKodpppNEYrP5AQzyKlxBtKO+MRdLYwHDDwwvakU8atRww==
|
||||
dependencies:
|
||||
"@atproto/lex-schema" "^0.0.16"
|
||||
"@atproto/lex-schema" "^0.0.14"
|
||||
core-js "^3"
|
||||
tslib "^2.8.1"
|
||||
|
||||
@@ -374,27 +325,19 @@
|
||||
"@atproto/lex-data" "^0.0.13"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-json@^0.0.14":
|
||||
version "0.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.14.tgz#717e533ab583aa5f580acb2a77d9aa3e7eddaa17"
|
||||
integrity sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww==
|
||||
dependencies:
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-resolver@^0.0.19":
|
||||
version "0.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.19.tgz#806fcb71e72d0db51e2eb29c594eddb8c4087414"
|
||||
integrity sha512-oATn4RpZNLh5rp9doN5/UOYS/Cd25GOD90ohB5jnnmeoF8jTupqIYTVhntbnx1EFn+5tTlgkyXEBV+XESBUcdQ==
|
||||
"@atproto/lex-resolver@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-resolver/-/lex-resolver-0.0.17.tgz#2c474f6babeb54665656bf28b8d27a98de69deae"
|
||||
integrity sha512-6nI5bYZUYh50ZI8r4erLRP9EbNcW226VShpVN3vHyOSgTje4VP1RTcvBhROBAPj4rL3vc+Oa8OiL6IQXkYrQBg==
|
||||
dependencies:
|
||||
"@atproto-labs/did-resolver" "^0.2.6"
|
||||
"@atproto/crypto" "^0.4.5"
|
||||
"@atproto/lex-client" "^0.0.17"
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
"@atproto/lex-document" "^0.0.17"
|
||||
"@atproto/lex-schema" "^0.0.16"
|
||||
"@atproto/repo" "^0.8.13"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/lex-client" "^0.0.15"
|
||||
"@atproto/lex-data" "^0.0.13"
|
||||
"@atproto/lex-document" "^0.0.15"
|
||||
"@atproto/lex-schema" "^0.0.14"
|
||||
"@atproto/repo" "^0.8.12"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-schema@^0.0.14":
|
||||
@@ -406,17 +349,6 @@
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lex-schema@^0.0.16":
|
||||
version "0.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lex-schema/-/lex-schema-0.0.16.tgz#8362932e239b7eaa7c5d6982d06c3147e3afd138"
|
||||
integrity sha512-O+IorivZHJPeV3kU3NDD2yI8ATfckOphgvDfeiyKHRTxRUKS+lHMCpGUiSTC3fJrfMvYITrruUVViUHVEScrbA==
|
||||
dependencies:
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@standard-schema/spec" "^1.1.0"
|
||||
iso-datestring-validator "^2.2.2"
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/lexicon@^0.6.0", "@atproto/lexicon@^0.6.2":
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.6.2.tgz#f6152a2119df953236ca127c4b30e332265e81e7"
|
||||
@@ -450,28 +382,28 @@
|
||||
optionalDependencies:
|
||||
"@atproto/oauth-provider-api" "0.3.7"
|
||||
|
||||
"@atproto/oauth-provider@^0.15.14":
|
||||
version "0.15.14"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.15.14.tgz#d969018b4ad5c0dd5863150cb8c1b65458738589"
|
||||
integrity sha512-arA3O+Ye1YBhoIUnZtn8wfatnVnwiZrGyNkxhH0nqGbh/RRfwA5W0tgnSDq0VMclLkrPY/OnZ4v3oo9N81yWGg==
|
||||
"@atproto/oauth-provider@^0.15.12":
|
||||
version "0.15.12"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.15.12.tgz#9dbbfdd6808399d9d7ff8993ac888938fbf4c515"
|
||||
integrity sha512-Ri4aVx2I4lOKxViB92jwPhAs/NctWEwV0tgYSHcaRpvqr2SVlC2LxTVjUq14ohdbVfv4VFRzj0vZypEX+mclHg==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch" "^0.2.3"
|
||||
"@atproto-labs/fetch-node" "^0.2.0"
|
||||
"@atproto-labs/pipe" "^0.1.1"
|
||||
"@atproto-labs/simple-store" "^0.3.0"
|
||||
"@atproto-labs/simple-store-memory" "^0.1.4"
|
||||
"@atproto/common" "^0.5.15"
|
||||
"@atproto/common" "^0.5.14"
|
||||
"@atproto/did" "^0.3.0"
|
||||
"@atproto/jwk" "^0.6.0"
|
||||
"@atproto/jwk-jose" "^0.1.11"
|
||||
"@atproto/lex-document" "^0.0.17"
|
||||
"@atproto/lex-resolver" "^0.0.19"
|
||||
"@atproto/lex-document" "^0.0.15"
|
||||
"@atproto/lex-resolver" "^0.0.17"
|
||||
"@atproto/oauth-provider-api" "0.3.7"
|
||||
"@atproto/oauth-provider-frontend" "0.2.9"
|
||||
"@atproto/oauth-provider-ui" "0.4.3"
|
||||
"@atproto/oauth-scopes" "^0.3.2"
|
||||
"@atproto/oauth-types" "^0.6.3"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
"@hapi/accept" "^6.0.3"
|
||||
"@hapi/address" "^5.1.1"
|
||||
"@hapi/bourne" "^3.0.0"
|
||||
@@ -510,20 +442,20 @@
|
||||
"@atproto/jwk" "^0.6.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/ozone@^0.1.167":
|
||||
version "0.1.167"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.167.tgz#c3974bbe06f0926f165f90d5ba51ba9ce2032fff"
|
||||
integrity sha512-AFquyND8zsskjkDc3WrQObUnZlEky05pFo0YYLy5JoqQN0WXIePLgGY0SC0EIokfF8iYXJE1ZM1u/dgA7DHqGQ==
|
||||
"@atproto/ozone@^0.1.166":
|
||||
version "0.1.166"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.166.tgz#9e65d6f67ef1fe285d0880e5f5a1b282dc63bd70"
|
||||
integrity sha512-XZ77P/V/tt3SqTQYRsi5nM3P2h+QaNT7Nz3GTf+TMLVolACufRHPoDgp/PoTuS2FaLj1ndHnGyIPvZi/o8he6g==
|
||||
dependencies:
|
||||
"@atproto/api" "^0.19.4"
|
||||
"@atproto/api" "^0.19.2"
|
||||
"@atproto/common" "^0.5.14"
|
||||
"@atproto/crypto" "^0.4.5"
|
||||
"@atproto/identity" "^0.4.12"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
"@atproto/ws-client" "^0.0.4"
|
||||
"@atproto/xrpc" "^0.7.7"
|
||||
"@atproto/xrpc-server" "^0.10.16"
|
||||
"@atproto/xrpc-server" "^0.10.15"
|
||||
"@did-plc/lib" "^0.0.1"
|
||||
compression "^1.7.4"
|
||||
cors "^2.8.5"
|
||||
@@ -541,30 +473,30 @@
|
||||
undici "^6.14.1"
|
||||
ws "^8.12.0"
|
||||
|
||||
"@atproto/pds@^0.4.216":
|
||||
version "0.4.216"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.216.tgz#4d9c73a529bd00893753aba1e7bb33f99b9aaaf4"
|
||||
integrity sha512-yPNatCb2kvudRp5DMbPemN1+uMsLOJDydw2PCBMxuzdimOf10PsekOhhZxtfGGRvk2NbvKoyfUvkROoFmTr+ew==
|
||||
"@atproto/pds@^0.4.214":
|
||||
version "0.4.214"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.214.tgz#c68d55ec0b00a4e35f4801c826d44b65cd62f984"
|
||||
integrity sha512-bTWeWg3H0TlELfE2eI2ySQuC6ojsCBSSmPtCXBh3Td9TFNpIoZQ/tYLUJMtMXaiVUi6HxzXQL1/iuYfC4Y+ZYQ==
|
||||
dependencies:
|
||||
"@atproto-labs/fetch-node" "^0.2.0"
|
||||
"@atproto-labs/simple-store" "^0.3.0"
|
||||
"@atproto-labs/simple-store-memory" "^0.1.4"
|
||||
"@atproto-labs/simple-store-redis" "^0.0.1"
|
||||
"@atproto-labs/xrpc-utils" "^0.0.24"
|
||||
"@atproto/api" "^0.19.4"
|
||||
"@atproto/api" "^0.19.2"
|
||||
"@atproto/aws" "^0.2.31"
|
||||
"@atproto/common" "^0.5.15"
|
||||
"@atproto/common" "^0.5.14"
|
||||
"@atproto/crypto" "^0.4.5"
|
||||
"@atproto/identity" "^0.4.12"
|
||||
"@atproto/lex-cbor" "^0.0.15"
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
"@atproto/lex-cbor" "^0.0.14"
|
||||
"@atproto/lex-data" "^0.0.13"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
"@atproto/oauth-provider" "^0.15.14"
|
||||
"@atproto/oauth-provider" "^0.15.12"
|
||||
"@atproto/oauth-scopes" "^0.3.2"
|
||||
"@atproto/repo" "^0.8.13"
|
||||
"@atproto/syntax" "^0.5.1"
|
||||
"@atproto/repo" "^0.8.12"
|
||||
"@atproto/syntax" "^0.5.0"
|
||||
"@atproto/xrpc" "^0.7.7"
|
||||
"@atproto/xrpc-server" "^0.10.17"
|
||||
"@atproto/xrpc-server" "^0.10.15"
|
||||
"@did-plc/lib" "^0.0.4"
|
||||
"@hapi/address" "^5.1.1"
|
||||
better-sqlite3 "^10.0.0"
|
||||
@@ -608,21 +540,6 @@
|
||||
varint "^6.0.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/repo@^0.8.13":
|
||||
version "0.8.13"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.8.13.tgz#70160b8b3f78b6addcba7cf3e3ae06306e6b6641"
|
||||
integrity sha512-VS8XHaBMGdq60xwRI5zQmXzsMF1hU7NKPjmkdr65tJdrv2z0VW77mG01Ui19Xh9O0mUc/LG6GEhwVrabB9Txow==
|
||||
dependencies:
|
||||
"@atproto/common" "^0.5.14"
|
||||
"@atproto/common-web" "^0.4.18"
|
||||
"@atproto/crypto" "^0.4.5"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
"@ipld/dag-cbor" "^7.0.0"
|
||||
multiformats "^9.9.0"
|
||||
uint8arrays "3.0.0"
|
||||
varint "^6.0.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/sync@^0.1.40":
|
||||
version "0.1.40"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.40.tgz#b8b467ac4fbf2e682d36cd5697508f993e9e645a"
|
||||
@@ -645,13 +562,6 @@
|
||||
dependencies:
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/syntax@^0.5.1":
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.5.1.tgz#78257b903a0723720dca32110379208791ac3c24"
|
||||
integrity sha512-J8DJjgKgACIyCTbpfvoTnf7+ofTx1kxTGO7KAftkC+jczaMdQhKdgIBAg2DaYy+80cvYGTHy5q/HI9qMAwGbWw==
|
||||
dependencies:
|
||||
tslib "^2.8.1"
|
||||
|
||||
"@atproto/ws-client@^0.0.4":
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ws-client/-/ws-client-0.0.4.tgz#9e436c0e72abea5da0d5a7e8ec862cec0fdb10cd"
|
||||
@@ -681,27 +591,6 @@
|
||||
rate-limiter-flexible "^2.4.1"
|
||||
ws "^8.12.0"
|
||||
|
||||
"@atproto/xrpc-server@^0.10.16", "@atproto/xrpc-server@^0.10.17":
|
||||
version "0.10.17"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.10.17.tgz#1016d4a6d97966a3f80e8785e5d5cba7b68c1c60"
|
||||
integrity sha512-FjexO6P/LRTx6/FdiWTzycFF4TgACW9npsFOitnocydTCOLYouP0OvUwdkxkreFC7qerT4+ARKpqrxRzyj0MNA==
|
||||
dependencies:
|
||||
"@atproto/common" "^0.5.15"
|
||||
"@atproto/crypto" "^0.4.5"
|
||||
"@atproto/lex-cbor" "^0.0.15"
|
||||
"@atproto/lex-client" "^0.0.17"
|
||||
"@atproto/lex-data" "^0.0.14"
|
||||
"@atproto/lex-json" "^0.0.14"
|
||||
"@atproto/lex-schema" "^0.0.16"
|
||||
"@atproto/lexicon" "^0.6.2"
|
||||
"@atproto/ws-client" "^0.0.4"
|
||||
"@atproto/xrpc" "^0.7.7"
|
||||
express "^4.17.2"
|
||||
http-errors "^2.0.0"
|
||||
mime-types "^2.1.35"
|
||||
rate-limiter-flexible "^2.4.1"
|
||||
ws "^8.12.0"
|
||||
|
||||
"@atproto/xrpc@^0.7.6", "@atproto/xrpc@^0.7.7":
|
||||
version "0.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.7.tgz#c0e3106c854cb9bc7d3129de2f31b8256eb0ed11"
|
||||
@@ -2257,11 +2146,6 @@
|
||||
dependencies:
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@standard-schema/spec@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
|
||||
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
|
||||
|
||||
"@tokenizer/token@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
|
||||
@@ -4262,10 +4146,10 @@ typed-emitter@^2.1.0:
|
||||
optionalDependencies:
|
||||
rxjs "^7.5.2"
|
||||
|
||||
typescript@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.2.tgz#0b1bfb15f68c64b97032f3d78abbf98bdbba501f"
|
||||
integrity sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==
|
||||
typescript@^5.9.3:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.19.3"
|
||||
|
||||
@@ -47,7 +47,8 @@ Every night, a GitHub action will run `yarn intl:extract` to update the english
|
||||
### Release process
|
||||
|
||||
1. Pull main and create a branch.
|
||||
1. Run `yarn intl:release` to fetch all translation updates from Crowdin and extract all `.po` files so that they're synced with the latest code. Commit that.
|
||||
1. Run `yarn intl:pull` to fetch all translation updates from Crowdin. Commit.
|
||||
1. Run `yarn intl:extract:all` to ensure all `.po` files are synced with the current state of the code. Commit.
|
||||
1. Create a PR, ensure the translations all look correct, and merge.
|
||||
1. If needed:
|
||||
1. Merge all approved translation PRs (contributions from outside crowdin).
|
||||
|
||||
@@ -37,7 +37,6 @@ export default defineConfig(
|
||||
'*.e2e.ts',
|
||||
'*.e2e.tsx',
|
||||
'eslint.config.mjs',
|
||||
'.jscodeshift/**',
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
+3
-13
@@ -33,27 +33,17 @@ class BottomSheetView(
|
||||
private var eventDispatcher: EventDispatcher? = null
|
||||
|
||||
// Native content height observation (eliminates JS bridge round-trip)
|
||||
private var contentLayoutListener: OnLayoutChangeListener? = null
|
||||
private var contentLayoutListener: View.OnLayoutChangeListener? = null
|
||||
private var observedChildren: List<View> = emptyList()
|
||||
private var lastObservedContentHeight: Float = 0f
|
||||
private var pendingLayoutUpdate: Boolean = false
|
||||
|
||||
private val screenHeight: Float =
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
// API 35+: edge-to-edge is mandatory, heightPixels is the full display
|
||||
context.resources.displayMetrics.heightPixels.toFloat()
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
// API 30-34: heightPixels may exclude nav bar, use currentWindowMetrics
|
||||
} else {
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
||||
wm.currentWindowMetrics.bounds.height().toFloat()
|
||||
} else {
|
||||
// API < 30: currentWindowMetrics not available, use getRealSize
|
||||
// which includes system bars (heightPixels may exclude them)
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
||||
val size = android.graphics.Point()
|
||||
@Suppress("DEPRECATION")
|
||||
wm.defaultDisplay.getRealSize(size)
|
||||
size.y.toFloat()
|
||||
}
|
||||
|
||||
private fun getNavigationBarHeight(): Int {
|
||||
@@ -365,7 +355,7 @@ class BottomSheetView(
|
||||
|
||||
val innerViewGroup = this.innerView as? ViewGroup ?: return
|
||||
|
||||
val listener = OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, oldBottom ->
|
||||
val listener = View.OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, oldBottom ->
|
||||
val newHeight = bottom - top
|
||||
val oldHeight = oldBottom - oldTop
|
||||
if (newHeight != oldHeight) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react'
|
||||
import {Component, createRef} from 'react'
|
||||
import {type ComponentType, type ContextType, type RefObject} from 'react'
|
||||
import {
|
||||
Dimensions,
|
||||
type LayoutChangeEvent,
|
||||
@@ -21,9 +22,9 @@ import {
|
||||
Context as PortalContext,
|
||||
} from './BottomSheetPortal'
|
||||
|
||||
const NativeView: React.ComponentType<
|
||||
const NativeView: ComponentType<
|
||||
BottomSheetViewProps & {
|
||||
ref: React.RefObject<any>
|
||||
ref: RefObject<any>
|
||||
style: StyleProp<ViewStyle>
|
||||
}
|
||||
> = requireNativeViewManager('BottomSheet')
|
||||
@@ -39,14 +40,14 @@ const IS_IOS15 =
|
||||
const IS_NON_E2E_ANDROID =
|
||||
Platform.OS === 'android' && Number(Platform.Version) < 35
|
||||
|
||||
export class BottomSheetNativeComponent extends React.Component<
|
||||
export class BottomSheetNativeComponent extends Component<
|
||||
BottomSheetViewProps,
|
||||
{
|
||||
open: boolean
|
||||
viewHeight?: number
|
||||
}
|
||||
> {
|
||||
ref = React.createRef<any>()
|
||||
ref = createRef<any>()
|
||||
|
||||
static contextType = PortalContext
|
||||
|
||||
@@ -79,7 +80,7 @@ export class BottomSheetNativeComponent extends React.Component<
|
||||
}
|
||||
|
||||
render() {
|
||||
const Portal = this.context as React.ContextType<typeof PortalContext>
|
||||
const Portal = this.context as ContextType<typeof PortalContext>
|
||||
if (!Portal) {
|
||||
throw new Error(
|
||||
'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
|
||||
@@ -139,7 +140,7 @@ function BottomSheetNativeComponentInner({
|
||||
onStateChange: (
|
||||
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||
) => void
|
||||
nativeViewRef: React.RefObject<View>
|
||||
nativeViewRef: RefObject<View>
|
||||
onLayout?: (event: LayoutChangeEvent) => void
|
||||
}) {
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import React from 'react'
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {type ElementType, type ReactNode} from 'react'
|
||||
|
||||
import {createPortalGroup_INTERNAL} from './lib/Portal'
|
||||
|
||||
type PortalContext = React.ElementType<{children: React.ReactNode}>
|
||||
type PortalContext = ElementType<{children: ReactNode}>
|
||||
|
||||
export const Context = React.createContext({} as PortalContext)
|
||||
export const Context = createContext({} as PortalContext)
|
||||
Context.displayName = 'BottomSheetPortalContext'
|
||||
|
||||
export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
|
||||
export const useBottomSheetPortal_INTERNAL = () => useContext(Context)
|
||||
|
||||
export function BottomSheetPortalProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const portal = React.useMemo(() => {
|
||||
export function BottomSheetPortalProvider({children}: {children: ReactNode}) {
|
||||
const portal = useMemo(() => {
|
||||
return createPortalGroup_INTERNAL()
|
||||
}, [])
|
||||
|
||||
@@ -32,7 +29,7 @@ const defaultPortal = createPortalGroup_INTERNAL()
|
||||
|
||||
export const BottomSheetOutlet = defaultPortal.Outlet
|
||||
|
||||
export function BottomSheetProvider({children}: {children: React.ReactNode}) {
|
||||
export function BottomSheetProvider({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<Context.Provider value={defaultPortal.Portal}>
|
||||
<defaultPortal.Provider>{children}</defaultPortal.Provider>
|
||||
|
||||
+9
-9
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import {createContext, useContext, useEffect, useMemo, useState} from 'react'
|
||||
import {type ReactNode} from 'react'
|
||||
|
||||
import {BackgroundNotificationHandlerPreferences} from './ExpoBackgroundNotificationHandler.types'
|
||||
import {type BackgroundNotificationHandlerPreferences} from './ExpoBackgroundNotificationHandler.types'
|
||||
import {BackgroundNotificationHandler} from './ExpoBackgroundNotificationHandlerModule'
|
||||
|
||||
interface BackgroundNotificationPreferencesContext {
|
||||
@@ -11,30 +12,29 @@ interface BackgroundNotificationPreferencesContext {
|
||||
) => void
|
||||
}
|
||||
|
||||
const Context = React.createContext<BackgroundNotificationPreferencesContext>(
|
||||
const Context = createContext<BackgroundNotificationPreferencesContext>(
|
||||
{} as BackgroundNotificationPreferencesContext,
|
||||
)
|
||||
export const useBackgroundNotificationPreferences = () =>
|
||||
React.useContext(Context)
|
||||
export const useBackgroundNotificationPreferences = () => useContext(Context)
|
||||
|
||||
export function BackgroundNotificationPreferencesProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: ReactNode
|
||||
}) {
|
||||
const [preferences, setPreferences] =
|
||||
React.useState<BackgroundNotificationHandlerPreferences>({
|
||||
useState<BackgroundNotificationHandlerPreferences>({
|
||||
playSoundChat: true,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const prefs = await BackgroundNotificationHandler.getAllPrefsAsync()
|
||||
setPreferences(prefs)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo(
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
preferences,
|
||||
setPref: async <
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import {createRef, PureComponent} from 'react'
|
||||
import {type ComponentType, type RefObject} from 'react'
|
||||
import {requireNativeModule} from 'expo'
|
||||
import {requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {GifViewProps} from './GifView.types'
|
||||
import {type GifViewProps} from './GifView.types'
|
||||
|
||||
const NativeModule = requireNativeModule('ExpoBlueskyGifView')
|
||||
const NativeView: React.ComponentType<
|
||||
GifViewProps & {ref: React.RefObject<any>}
|
||||
> = requireNativeViewManager('ExpoBlueskyGifView')
|
||||
const NativeView: ComponentType<GifViewProps & {ref: RefObject<any>}> =
|
||||
requireNativeViewManager('ExpoBlueskyGifView')
|
||||
|
||||
export class GifView extends React.PureComponent<GifViewProps> {
|
||||
export class GifView extends PureComponent<GifViewProps> {
|
||||
// TODO native types, should all be the same as those in this class
|
||||
private nativeRef: React.RefObject<any> = React.createRef()
|
||||
private nativeRef: RefObject<any> = createRef()
|
||||
|
||||
constructor(props: GifViewProps | Readonly<GifViewProps>) {
|
||||
super(props)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as React from 'react'
|
||||
import {createRef, PureComponent} from 'react'
|
||||
import {type RefObject} from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
|
||||
import {GifViewProps} from './GifView.types'
|
||||
import {type GifViewProps} from './GifView.types'
|
||||
|
||||
export class GifView extends React.PureComponent<GifViewProps> {
|
||||
private readonly videoPlayerRef: React.RefObject<HTMLMediaElement> =
|
||||
React.createRef()
|
||||
export class GifView extends PureComponent<GifViewProps> {
|
||||
private readonly videoPlayerRef: RefObject<HTMLMediaElement> = createRef()
|
||||
private isLoaded = false
|
||||
|
||||
constructor(props: GifViewProps | Readonly<GifViewProps>) {
|
||||
|
||||
+24
-29
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.120.0",
|
||||
"version": "1.119.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -52,7 +52,7 @@
|
||||
"lint-native": "swiftlint ./modules && ktlint ./modules",
|
||||
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
|
||||
"typecheck": "tsc --project ./tsconfig.check.json",
|
||||
"e2e:mock-server": "cd dev-env && yarn start",
|
||||
"e2e:mock-server": "cd dev-env && yarn e2e:mock-server",
|
||||
"e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:start": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo start -c",
|
||||
@@ -70,7 +70,6 @@
|
||||
"intl:pull": "crowdin download translations --verbose -b main",
|
||||
"intl:push": "crowdin push translations --verbose -b main",
|
||||
"intl:push-sources": "crowdin push sources --verbose -b main",
|
||||
"intl:release": "yarn intl:pull && yarn intl:extract:all",
|
||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
||||
"update-extensions": "bash scripts/updateExtensions.sh",
|
||||
"export": "npx expo export --dump-sourcemap && yarn upload-native-sourcemaps",
|
||||
@@ -86,7 +85,7 @@
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@bsky.app/alf": "^0.1.7",
|
||||
"@bsky.app/expo-image-crop-tool": "^0.5.0",
|
||||
"@bsky.app/expo-translate-text": "^0.2.9",
|
||||
"@bsky.app/expo-translate-text": "^0.2.7",
|
||||
"@bsky.app/react-native-mmkv": "2.12.5",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
@@ -144,7 +143,7 @@
|
||||
"emoji-mart": "^5.6.0",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^54.0.33",
|
||||
"expo": "^54.0.27",
|
||||
"expo-application": "~7.0.8",
|
||||
"expo-blur": "~15.0.8",
|
||||
"expo-build-properties": "~1.0.10",
|
||||
@@ -153,28 +152,28 @@
|
||||
"expo-contacts": "^15.0.10",
|
||||
"expo-dev-client": "~6.0.20",
|
||||
"expo-device": "~8.0.10",
|
||||
"expo-file-system": "~19.0.21",
|
||||
"expo-font": "~14.0.11",
|
||||
"expo-file-system": "~19.0.20",
|
||||
"expo-font": "~14.0.10",
|
||||
"expo-haptics": "~15.0.8",
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-image-manipulator": "~14.0.8",
|
||||
"expo-image-picker": "~17.0.10",
|
||||
"expo-image-picker": "~17.0.9",
|
||||
"expo-intent-launcher": "~13.0.8",
|
||||
"expo-keep-awake": "~15.0.8",
|
||||
"expo-linear-gradient": "~15.0.8",
|
||||
"expo-linking": "~8.0.11",
|
||||
"expo-linking": "~8.0.10",
|
||||
"expo-localization": "~17.0.8",
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-media-library": "~18.2.1",
|
||||
"expo-notifications": "~0.32.16",
|
||||
"expo-notifications": "~0.32.14",
|
||||
"expo-privacy-sensitive": "^0.1.0",
|
||||
"expo-screen-orientation": "~9.0.8",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-sms": "^14.0.7",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
"expo-splash-screen": "~31.0.12",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"expo-updates": "~29.0.16",
|
||||
"expo-video": "~3.0.16",
|
||||
"expo-updates": "~29.0.14",
|
||||
"expo-video": "~3.0.15",
|
||||
"expo-video-thumbnails": "^10.0.8",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -209,7 +208,7 @@
|
||||
"react-native-drawer-layout": "^4.2.2",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-keyboard-controller": "^1.21.0",
|
||||
"react-native-keyboard-controller": "^1.20.7",
|
||||
"react-native-pager-view": "6.8.0",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
@@ -239,9 +238,8 @@
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@crowdin/cli": "^4.14.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@expo/config-plugins": "~54.0.4",
|
||||
"@expo/config-plugins": "~54.0.1",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.9.2",
|
||||
"@lingui/cli": "^5.9.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
@@ -260,7 +258,7 @@
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.3",
|
||||
"babel-preset-expo": "~54.0.10",
|
||||
"babel-preset-expo": "~54.0.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-bsky-internal": "link:./eslint",
|
||||
@@ -277,7 +275,7 @@
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~54.0.17",
|
||||
"jest-expo": "~54.0.14",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
@@ -286,22 +284,20 @@
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
"ts-plugin-sort-import-suggestions": "^1.0.4",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.0",
|
||||
"webpack-bundle-analyzer": "^4.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@react-native/babel-preset": "0.81.5",
|
||||
"@react-native/normalize-colors": "0.81.5",
|
||||
"**/@expo/image-utils": "0.8.12",
|
||||
"**/@expo/image-utils": "0.8.7",
|
||||
"**/@react-native-async-storage/async-storage": "2.2.0",
|
||||
"**/expo-constants": "18.0.8",
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/multiformats": "9.9.0",
|
||||
"unicode-segmenter": "0.14.5",
|
||||
"@types/estree": "1.0.6",
|
||||
"metro": "0.83.3",
|
||||
"metro-core": "0.83.3",
|
||||
"metro-config": "0.83.3",
|
||||
"metro-runtime": "0.83.3",
|
||||
"metro-source-map": "0.83.3"
|
||||
"@types/estree": "1.0.6"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo/ios",
|
||||
@@ -324,8 +320,7 @@
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
"__tests__/.*/__mocks__",
|
||||
"__e2e__/.*",
|
||||
"bskylink/.*"
|
||||
"__e2e__/.*"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt
|
||||
index d300fc2..0890878 100644
|
||||
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt
|
||||
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultLauncher.kt
|
||||
@@ -3,8 +3,8 @@ package expo.modules.kotlin.activityresult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import java.io.Serializable
|
||||
+import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
-import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* A launcher for a previously-[AppContextActivityResultCaller.registerForActivityResult] prepared call
|
||||
@@ -22,8 +22,12 @@ abstract class AppContextActivityResultLauncher<I : Serializable, O> {
|
||||
*/
|
||||
abstract fun launch(input: I, callback: ActivityResultCallback<O>)
|
||||
|
||||
- suspend fun launch(input: I): O = suspendCoroutine { continuation ->
|
||||
- launch(input) { output -> continuation.resume(output) }
|
||||
+ suspend fun launch(input: I): O = suspendCancellableCoroutine { continuation ->
|
||||
+ launch(input) { output ->
|
||||
+ if (continuation.isActive) {
|
||||
+ continuation.resume(output)
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
abstract val contract: AppContextActivityResultContract<I, O>
|
||||
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
index 47c4d15..afe138d 100644
|
||||
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
|
||||
}
|
||||
|
||||
internal fun shouldParseBody(response: Response): Boolean {
|
||||
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
|
||||
+ return false
|
||||
+ }
|
||||
+
|
||||
// Check for Content-Type
|
||||
val skipContentTypes = listOf(
|
||||
"text/event-stream", // Server Sent Events
|
||||
@@ -1,15 +0,0 @@
|
||||
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
index 47c4d15..afe138d 100644
|
||||
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
|
||||
}
|
||||
|
||||
internal fun shouldParseBody(response: Response): Boolean {
|
||||
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
|
||||
+ return false
|
||||
+ }
|
||||
+
|
||||
// Check for Content-Type
|
||||
val skipContentTypes = listOf(
|
||||
"text/event-stream", // Server Sent Events
|
||||
@@ -0,0 +1,992 @@
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/8.10/checksums/checksums.lock b/node_modules/expo-notifications/android/.gradle/8.10/checksums/checksums.lock
|
||||
new file mode 100644
|
||||
index 0000000..883ef6a
|
||||
Binary files /dev/null and b/node_modules/expo-notifications/android/.gradle/8.10/checksums/checksums.lock differ
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/8.10/dependencies-accessors/gc.properties b/node_modules/expo-notifications/android/.gradle/8.10/dependencies-accessors/gc.properties
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/8.10/fileChanges/last-build.bin b/node_modules/expo-notifications/android/.gradle/8.10/fileChanges/last-build.bin
|
||||
new file mode 100644
|
||||
index 0000000..f76dd23
|
||||
Binary files /dev/null and b/node_modules/expo-notifications/android/.gradle/8.10/fileChanges/last-build.bin differ
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/8.10/fileHashes/fileHashes.lock b/node_modules/expo-notifications/android/.gradle/8.10/fileHashes/fileHashes.lock
|
||||
new file mode 100644
|
||||
index 0000000..774caf7
|
||||
Binary files /dev/null and b/node_modules/expo-notifications/android/.gradle/8.10/fileHashes/fileHashes.lock differ
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/8.10/gc.properties b/node_modules/expo-notifications/android/.gradle/8.10/gc.properties
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/node_modules/expo-notifications/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
|
||||
new file mode 100644
|
||||
index 0000000..a3c1514
|
||||
Binary files /dev/null and b/node_modules/expo-notifications/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/buildOutputCleanup/cache.properties b/node_modules/expo-notifications/android/.gradle/buildOutputCleanup/cache.properties
|
||||
new file mode 100644
|
||||
index 0000000..0e5b4da
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.gradle/buildOutputCleanup/cache.properties
|
||||
@@ -0,0 +1,2 @@
|
||||
+#Thu Apr 24 20:44:36 PDT 2025
|
||||
+gradle.version=8.10
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/config.properties b/node_modules/expo-notifications/android/.gradle/config.properties
|
||||
new file mode 100644
|
||||
index 0000000..0bd71c6
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.gradle/config.properties
|
||||
@@ -0,0 +1,2 @@
|
||||
+#Thu Apr 24 20:44:32 PDT 2025
|
||||
+java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
|
||||
diff --git a/node_modules/expo-notifications/android/.gradle/vcs-1/gc.properties b/node_modules/expo-notifications/android/.gradle/vcs-1/gc.properties
|
||||
new file mode 100644
|
||||
index 0000000..e69de29
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/.gitignore b/node_modules/expo-notifications/android/.idea/.gitignore
|
||||
new file mode 100644
|
||||
index 0000000..26d3352
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/.gitignore
|
||||
@@ -0,0 +1,3 @@
|
||||
+# Default ignored files
|
||||
+/shelf/
|
||||
+/workspace.xml
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/AndroidProjectSystem.xml b/node_modules/expo-notifications/android/.idea/AndroidProjectSystem.xml
|
||||
new file mode 100644
|
||||
index 0000000..4a53bee
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/AndroidProjectSystem.xml
|
||||
@@ -0,0 +1,6 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<project version="4">
|
||||
+ <component name="AndroidProjectSystem">
|
||||
+ <option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
+ </component>
|
||||
+</project>
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/caches/deviceStreaming.xml b/node_modules/expo-notifications/android/.idea/caches/deviceStreaming.xml
|
||||
new file mode 100644
|
||||
index 0000000..9e9ba09
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/caches/deviceStreaming.xml
|
||||
@@ -0,0 +1,607 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<project version="4">
|
||||
+ <component name="DeviceStreaming">
|
||||
+ <option name="deviceSelectionList">
|
||||
+ <list>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="27" />
|
||||
+ <option name="brand" value="DOCOMO" />
|
||||
+ <option name="codename" value="F01L" />
|
||||
+ <option name="id" value="F01L" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="FUJITSU" />
|
||||
+ <option name="name" value="F-01L" />
|
||||
+ <option name="screenDensity" value="360" />
|
||||
+ <option name="screenX" value="720" />
|
||||
+ <option name="screenY" value="1280" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="OnePlus" />
|
||||
+ <option name="codename" value="OP5552L1" />
|
||||
+ <option name="id" value="OP5552L1" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="OnePlus" />
|
||||
+ <option name="name" value="CPH2415" />
|
||||
+ <option name="screenDensity" value="480" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2412" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="OPPO" />
|
||||
+ <option name="codename" value="OP573DL1" />
|
||||
+ <option name="id" value="OP573DL1" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="OPPO" />
|
||||
+ <option name="name" value="CPH2557" />
|
||||
+ <option name="screenDensity" value="480" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="28" />
|
||||
+ <option name="brand" value="DOCOMO" />
|
||||
+ <option name="codename" value="SH-01L" />
|
||||
+ <option name="id" value="SH-01L" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="SHARP" />
|
||||
+ <option name="name" value="AQUOS sense2 SH-01L" />
|
||||
+ <option name="screenDensity" value="480" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2160" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="Lenovo" />
|
||||
+ <option name="codename" value="TB370FU" />
|
||||
+ <option name="formFactor" value="Tablet" />
|
||||
+ <option name="id" value="TB370FU" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Lenovo" />
|
||||
+ <option name="name" value="Tab P12" />
|
||||
+ <option name="screenDensity" value="340" />
|
||||
+ <option name="screenX" value="1840" />
|
||||
+ <option name="screenY" value="2944" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="a15" />
|
||||
+ <option name="id" value="a15" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="A15" />
|
||||
+ <option name="screenDensity" value="450" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2340" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="a35x" />
|
||||
+ <option name="id" value="a35x" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="A35" />
|
||||
+ <option name="screenDensity" value="450" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2340" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="31" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="a51" />
|
||||
+ <option name="id" value="a51" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy A51" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="akita" />
|
||||
+ <option name="id" value="akita" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 8a" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="motorola" />
|
||||
+ <option name="codename" value="arcfox" />
|
||||
+ <option name="id" value="arcfox" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Motorola" />
|
||||
+ <option name="name" value="razr plus 2024" />
|
||||
+ <option name="screenDensity" value="360" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="1272" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="motorola" />
|
||||
+ <option name="codename" value="austin" />
|
||||
+ <option name="id" value="austin" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Motorola" />
|
||||
+ <option name="name" value="moto g 5G (2022)" />
|
||||
+ <option name="screenDensity" value="280" />
|
||||
+ <option name="screenX" value="720" />
|
||||
+ <option name="screenY" value="1600" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="b0q" />
|
||||
+ <option name="id" value="b0q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy S22 Ultra" />
|
||||
+ <option name="screenDensity" value="600" />
|
||||
+ <option name="screenX" value="1440" />
|
||||
+ <option name="screenY" value="3088" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="32" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="bluejay" />
|
||||
+ <option name="id" value="bluejay" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 6a" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="caiman" />
|
||||
+ <option name="id" value="caiman" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 9 Pro" />
|
||||
+ <option name="screenDensity" value="360" />
|
||||
+ <option name="screenX" value="960" />
|
||||
+ <option name="screenY" value="2142" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="comet" />
|
||||
+ <option name="default" value="true" />
|
||||
+ <option name="id" value="comet" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 9 Pro Fold" />
|
||||
+ <option name="screenDensity" value="390" />
|
||||
+ <option name="screenX" value="2076" />
|
||||
+ <option name="screenY" value="2152" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="29" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="crownqlteue" />
|
||||
+ <option name="id" value="crownqlteue" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy Note9" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="2220" />
|
||||
+ <option name="screenY" value="1080" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="dm2q" />
|
||||
+ <option name="id" value="dm2q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="S23 Plus" />
|
||||
+ <option name="screenDensity" value="450" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2340" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="dm3q" />
|
||||
+ <option name="id" value="dm3q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy S23 Ultra" />
|
||||
+ <option name="screenDensity" value="600" />
|
||||
+ <option name="screenX" value="1440" />
|
||||
+ <option name="screenY" value="3088" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="e1q" />
|
||||
+ <option name="default" value="true" />
|
||||
+ <option name="id" value="e1q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy S24" />
|
||||
+ <option name="screenDensity" value="480" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2340" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="e3q" />
|
||||
+ <option name="id" value="e3q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy S24 Ultra" />
|
||||
+ <option name="screenDensity" value="450" />
|
||||
+ <option name="screenX" value="1440" />
|
||||
+ <option name="screenY" value="3120" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="eos" />
|
||||
+ <option name="id" value="eos" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Eos" />
|
||||
+ <option name="screenDensity" value="320" />
|
||||
+ <option name="screenX" value="384" />
|
||||
+ <option name="screenY" value="384" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="felix" />
|
||||
+ <option name="id" value="felix" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel Fold" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="2208" />
|
||||
+ <option name="screenY" value="1840" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="felix" />
|
||||
+ <option name="id" value="felix" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel Fold" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="2208" />
|
||||
+ <option name="screenY" value="1840" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="felix_camera" />
|
||||
+ <option name="id" value="felix_camera" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="2208" />
|
||||
+ <option name="screenY" value="1840" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="motorola" />
|
||||
+ <option name="codename" value="fogona" />
|
||||
+ <option name="id" value="fogona" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Motorola" />
|
||||
+ <option name="name" value="moto g play - 2024" />
|
||||
+ <option name="screenDensity" value="280" />
|
||||
+ <option name="screenX" value="720" />
|
||||
+ <option name="screenY" value="1600" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="g0q" />
|
||||
+ <option name="id" value="g0q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="SM-S906U1" />
|
||||
+ <option name="screenDensity" value="450" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2340" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="gta9pwifi" />
|
||||
+ <option name="id" value="gta9pwifi" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="SM-X210" />
|
||||
+ <option name="screenDensity" value="240" />
|
||||
+ <option name="screenX" value="1200" />
|
||||
+ <option name="screenY" value="1920" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="gts7xllite" />
|
||||
+ <option name="id" value="gts7xllite" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="SM-T738U" />
|
||||
+ <option name="screenDensity" value="340" />
|
||||
+ <option name="screenX" value="1600" />
|
||||
+ <option name="screenY" value="2560" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="gts8uwifi" />
|
||||
+ <option name="formFactor" value="Tablet" />
|
||||
+ <option name="id" value="gts8uwifi" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
+ <option name="screenDensity" value="320" />
|
||||
+ <option name="screenX" value="1848" />
|
||||
+ <option name="screenY" value="2960" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="gts8wifi" />
|
||||
+ <option name="formFactor" value="Tablet" />
|
||||
+ <option name="id" value="gts8wifi" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy Tab S8" />
|
||||
+ <option name="screenDensity" value="274" />
|
||||
+ <option name="screenX" value="1600" />
|
||||
+ <option name="screenY" value="2560" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="gts9fe" />
|
||||
+ <option name="id" value="gts9fe" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy Tab S9 FE 5G" />
|
||||
+ <option name="screenDensity" value="280" />
|
||||
+ <option name="screenX" value="1440" />
|
||||
+ <option name="screenY" value="2304" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="husky" />
|
||||
+ <option name="id" value="husky" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 8 Pro" />
|
||||
+ <option name="screenDensity" value="390" />
|
||||
+ <option name="screenX" value="1008" />
|
||||
+ <option name="screenY" value="2244" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="30" />
|
||||
+ <option name="brand" value="motorola" />
|
||||
+ <option name="codename" value="java" />
|
||||
+ <option name="id" value="java" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Motorola" />
|
||||
+ <option name="name" value="G20" />
|
||||
+ <option name="screenDensity" value="280" />
|
||||
+ <option name="screenX" value="720" />
|
||||
+ <option name="screenY" value="1600" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="komodo" />
|
||||
+ <option name="id" value="komodo" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 9 Pro XL" />
|
||||
+ <option name="screenDensity" value="360" />
|
||||
+ <option name="screenX" value="1008" />
|
||||
+ <option name="screenY" value="2244" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="lynx" />
|
||||
+ <option name="id" value="lynx" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 7a" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="motorola" />
|
||||
+ <option name="codename" value="maui" />
|
||||
+ <option name="id" value="maui" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Motorola" />
|
||||
+ <option name="name" value="moto g play - 2023" />
|
||||
+ <option name="screenDensity" value="280" />
|
||||
+ <option name="screenX" value="720" />
|
||||
+ <option name="screenY" value="1600" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="o1q" />
|
||||
+ <option name="id" value="o1q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy S21" />
|
||||
+ <option name="screenDensity" value="421" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="31" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="oriole" />
|
||||
+ <option name="id" value="oriole" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 6" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="panther" />
|
||||
+ <option name="id" value="panther" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 7" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="q5q" />
|
||||
+ <option name="id" value="q5q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy Z Fold5" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1812" />
|
||||
+ <option name="screenY" value="2176" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="q6q" />
|
||||
+ <option name="id" value="q6q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy Z Fold6" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1856" />
|
||||
+ <option name="screenY" value="2160" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="30" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="r11" />
|
||||
+ <option name="formFactor" value="Wear OS" />
|
||||
+ <option name="id" value="r11" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel Watch" />
|
||||
+ <option name="screenDensity" value="320" />
|
||||
+ <option name="screenX" value="384" />
|
||||
+ <option name="screenY" value="384" />
|
||||
+ <option name="type" value="WEAR_OS" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="r11q" />
|
||||
+ <option name="id" value="r11q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="SM-S711U" />
|
||||
+ <option name="screenDensity" value="450" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2340" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="30" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="redfin" />
|
||||
+ <option name="id" value="redfin" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 5" />
|
||||
+ <option name="screenDensity" value="440" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2340" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="shiba" />
|
||||
+ <option name="id" value="shiba" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 8" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="samsung" />
|
||||
+ <option name="codename" value="t2q" />
|
||||
+ <option name="id" value="t2q" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Samsung" />
|
||||
+ <option name="name" value="Galaxy S21 Plus" />
|
||||
+ <option name="screenDensity" value="394" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2400" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="33" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="tangorpro" />
|
||||
+ <option name="formFactor" value="Tablet" />
|
||||
+ <option name="id" value="tangorpro" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel Tablet" />
|
||||
+ <option name="screenDensity" value="320" />
|
||||
+ <option name="screenX" value="1600" />
|
||||
+ <option name="screenY" value="2560" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="34" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="tokay" />
|
||||
+ <option name="default" value="true" />
|
||||
+ <option name="id" value="tokay" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 9" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2424" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ <PersistentDeviceSelectionData>
|
||||
+ <option name="api" value="35" />
|
||||
+ <option name="brand" value="google" />
|
||||
+ <option name="codename" value="tokay" />
|
||||
+ <option name="default" value="true" />
|
||||
+ <option name="id" value="tokay" />
|
||||
+ <option name="labId" value="google" />
|
||||
+ <option name="manufacturer" value="Google" />
|
||||
+ <option name="name" value="Pixel 9" />
|
||||
+ <option name="screenDensity" value="420" />
|
||||
+ <option name="screenX" value="1080" />
|
||||
+ <option name="screenY" value="2424" />
|
||||
+ </PersistentDeviceSelectionData>
|
||||
+ </list>
|
||||
+ </option>
|
||||
+ </component>
|
||||
+</project>
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/gradle.xml b/node_modules/expo-notifications/android/.idea/gradle.xml
|
||||
new file mode 100644
|
||||
index 0000000..b838237
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/gradle.xml
|
||||
@@ -0,0 +1,12 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<project version="4">
|
||||
+ <component name="GradleSettings">
|
||||
+ <option name="linkedExternalProjectsSettings">
|
||||
+ <GradleProjectSettings>
|
||||
+ <option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
+ <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
+ </GradleProjectSettings>
|
||||
+ </option>
|
||||
+ </component>
|
||||
+</project>
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/migrations.xml b/node_modules/expo-notifications/android/.idea/migrations.xml
|
||||
new file mode 100644
|
||||
index 0000000..f8051a6
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/migrations.xml
|
||||
@@ -0,0 +1,10 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<project version="4">
|
||||
+ <component name="ProjectMigrations">
|
||||
+ <option name="MigrateToGradleLocalJavaHome">
|
||||
+ <set>
|
||||
+ <option value="$PROJECT_DIR$" />
|
||||
+ </set>
|
||||
+ </option>
|
||||
+ </component>
|
||||
+</project>
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/misc.xml b/node_modules/expo-notifications/android/.idea/misc.xml
|
||||
new file mode 100644
|
||||
index 0000000..3040d03
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/misc.xml
|
||||
@@ -0,0 +1,10 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<project version="4">
|
||||
+ <component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
+ <component name="ProjectRootManager">
|
||||
+ <output url="file://$PROJECT_DIR$/build/classes" />
|
||||
+ </component>
|
||||
+ <component name="ProjectType">
|
||||
+ <option name="id" value="Android" />
|
||||
+ </component>
|
||||
+</project>
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/runConfigurations.xml b/node_modules/expo-notifications/android/.idea/runConfigurations.xml
|
||||
new file mode 100644
|
||||
index 0000000..16660f1
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/runConfigurations.xml
|
||||
@@ -0,0 +1,17 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<project version="4">
|
||||
+ <component name="RunConfigurationProducerService">
|
||||
+ <option name="ignoredProducers">
|
||||
+ <set>
|
||||
+ <option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
+ <option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
+ <option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
+ <option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
+ <option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
+ <option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
+ <option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
+ <option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
+ </set>
|
||||
+ </option>
|
||||
+ </component>
|
||||
+</project>
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/expo-notifications/android/.idea/workspace.xml b/node_modules/expo-notifications/android/.idea/workspace.xml
|
||||
new file mode 100644
|
||||
index 0000000..df26928
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/.idea/workspace.xml
|
||||
@@ -0,0 +1,47 @@
|
||||
+<?xml version="1.0" encoding="UTF-8"?>
|
||||
+<project version="4">
|
||||
+ <component name="AutoImportSettings">
|
||||
+ <option name="autoReloadType" value="NONE" />
|
||||
+ </component>
|
||||
+ <component name="ChangeListManager">
|
||||
+ <list default="true" id="fed6a9c0-2e93-4b6e-953a-d1cd1e93b59f" name="Changes" comment="" />
|
||||
+ <option name="SHOW_DIALOG" value="false" />
|
||||
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
+ <option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
+ </component>
|
||||
+ <component name="ClangdSettings">
|
||||
+ <option name="formatViaClangd" value="false" />
|
||||
+ </component>
|
||||
+ <component name="ProjectColorInfo"><![CDATA[{
|
||||
+ "associatedIndex": 4
|
||||
+}]]></component>
|
||||
+ <component name="ProjectId" id="2wCjuanPzVGKP91vdmftQVgUlaM" />
|
||||
+ <component name="ProjectViewState">
|
||||
+ <option name="hideEmptyMiddlePackages" value="true" />
|
||||
+ <option name="showLibraryContents" value="true" />
|
||||
+ </component>
|
||||
+ <component name="PropertiesComponent"><![CDATA[{
|
||||
+ "keyToString": {
|
||||
+ "RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
+ "RunOnceActivity.cidr.known.project.marker": "true",
|
||||
+ "RunOnceActivity.readMode.enableVisualFormatting": "true",
|
||||
+ "android.gradle.sync.needed": "true",
|
||||
+ "cf.first.check.clang-format": "false",
|
||||
+ "cidr.known.project.marker": "true",
|
||||
+ "kotlin-language-version-configured": "true",
|
||||
+ "last_opened_file_path": "/Users/hailey/bsky/social-app/node_modules/expo-notifications/android"
|
||||
+ }
|
||||
+}]]></component>
|
||||
+ <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
+ <component name="TaskManager">
|
||||
+ <task active="true" id="Default" summary="Default task">
|
||||
+ <changelist id="fed6a9c0-2e93-4b6e-953a-d1cd1e93b59f" name="Changes" comment="" />
|
||||
+ <created>1745552672693</created>
|
||||
+ <option name="number" value="Default" />
|
||||
+ <option name="presentableId" value="Default" />
|
||||
+ <updated>1745552672693</updated>
|
||||
+ </task>
|
||||
+ <servers />
|
||||
+ </component>
|
||||
+</project>
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
|
||||
index bc479ee..1ebfa00 100644
|
||||
--- a/node_modules/expo-notifications/android/build.gradle
|
||||
+++ b/node_modules/expo-notifications/android/build.gradle
|
||||
@@ -42,6 +42,7 @@ dependencies {
|
||||
implementation 'com.google.firebase:firebase-messaging:24.0.1'
|
||||
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
+ implementation project(':expo-background-notification-handler')
|
||||
|
||||
if (project.findProject(':expo-modules-test-core')) {
|
||||
testImplementation project(':expo-modules-test-core')
|
||||
diff --git a/node_modules/expo-notifications/android/local.properties b/node_modules/expo-notifications/android/local.properties
|
||||
new file mode 100644
|
||||
index 0000000..ab4c86d
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-notifications/android/local.properties
|
||||
@@ -0,0 +1,8 @@
|
||||
+## This file must *NOT* be checked into Version Control Systems,
|
||||
+# as it contains information specific to your local configuration.
|
||||
+#
|
||||
+# Location of the SDK. This is only used by Gradle.
|
||||
+# For customization when using a Version Control System, please read the
|
||||
+# header note.
|
||||
+#Thu Apr 24 20:44:32 PDT 2025
|
||||
+sdk.dir=/Users/hailey/Library/Android/sdk
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
|
||||
index 7b99e6c..45a450d 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
|
||||
@@ -15,6 +15,7 @@ import org.json.JSONObject
|
||||
* This interface exists to provide a common API for both classes.
|
||||
* */
|
||||
interface INotificationContent : Parcelable {
|
||||
+ val channelId: String?
|
||||
val title: String?
|
||||
val text: String?
|
||||
val subText: String?
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
index 191b64e..fe8b3c5 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
@@ -35,6 +35,7 @@ import kotlin.coroutines.Continuation;
|
||||
* Refactoring this class may require a migration strategy for the data stored in SharedPreferences.
|
||||
*/
|
||||
public class NotificationContent implements Parcelable, Serializable, INotificationContent {
|
||||
+ private String mChannelId;
|
||||
private String mTitle;
|
||||
private String mText;
|
||||
private String mSubtitle;
|
||||
@@ -65,6 +66,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
}
|
||||
};
|
||||
|
||||
+ @Nullable
|
||||
+ public String getChannelId() {
|
||||
+ return mChannelId;
|
||||
+ }
|
||||
+
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
@@ -158,6 +164,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
}
|
||||
|
||||
protected NotificationContent(Parcel in) {
|
||||
+ mChannelId = in.readString();
|
||||
mTitle = in.readString();
|
||||
mText = in.readString();
|
||||
mSubtitle = in.readString();
|
||||
@@ -183,6 +190,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
+ dest.writeString(mChannelId);
|
||||
dest.writeString(mTitle);
|
||||
dest.writeString(mText);
|
||||
dest.writeString(mSubtitle);
|
||||
@@ -203,6 +211,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
private static final long serialVersionUID = 397666843266836802L;
|
||||
|
||||
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
|
||||
+ out.writeObject(mChannelId);
|
||||
out.writeObject(mTitle);
|
||||
out.writeObject(mText);
|
||||
out.writeObject(mSubtitle);
|
||||
@@ -285,6 +294,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
useDefaultVibrationPattern();
|
||||
}
|
||||
|
||||
+ public Builder setChannelId(String channelId) {
|
||||
+ content.mChannelId = channelId;
|
||||
+ return this;
|
||||
+ }
|
||||
+
|
||||
public Builder setTitle(String title) {
|
||||
content.mTitle = title;
|
||||
return this;
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
|
||||
index 3af254c..3c77e9d 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
|
||||
@@ -11,6 +11,9 @@ import org.json.JSONObject
|
||||
* */
|
||||
@JvmInline
|
||||
value class NotificationData(private val data: Map<String, String>) {
|
||||
+ val channelId: String?
|
||||
+ get() = data["channelId"]
|
||||
+
|
||||
val title: String?
|
||||
get() = data["title"]
|
||||
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
|
||||
index d2cc6cf..6a48ff2 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
|
||||
@@ -31,6 +31,8 @@ class RemoteNotificationContent(private val remoteMessage: RemoteMessage) : INot
|
||||
return remoteMessage.notification?.imageUrl != null
|
||||
}
|
||||
|
||||
+ override val channelId = remoteMessage.notification?.channelId ?: notificationData.channelId
|
||||
+
|
||||
override val title = remoteMessage.notification?.title ?: notificationData.title
|
||||
|
||||
override val text = remoteMessage.notification?.body ?: notificationData.message
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
|
||||
index 98f003f..2f745e8 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
|
||||
@@ -101,6 +101,9 @@ open class ExpoNotificationBuilder(
|
||||
builder.setOngoing(content.isSticky)
|
||||
|
||||
// see "Notification anatomy" https://developer.android.com/develop/ui/views/notifications#Templates
|
||||
+ content.channelId?.let {
|
||||
+ builder.setChannelId(it)
|
||||
+ }
|
||||
builder.setContentTitle(content.title)
|
||||
builder.setContentText(content.text)
|
||||
builder.setSubText(content.subText)
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
|
||||
index 90ca4ff..9d4cb09 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
|
||||
@@ -3,6 +3,9 @@ package expo.modules.notifications.service.delegates
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandler
|
||||
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandlerInterface
|
||||
+import expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule
|
||||
import expo.modules.interfaces.taskManager.TaskServiceProviderHelper
|
||||
import expo.modules.notifications.notifications.RemoteMessageSerializer
|
||||
import expo.modules.notifications.notifications.background.BackgroundRemoteNotificationTaskConsumer
|
||||
@@ -18,7 +21,7 @@ import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
|
||||
+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface{
|
||||
companion object {
|
||||
// Unfortunately we cannot save state between instances of a service other way
|
||||
// than by static properties. Fortunately, using weak references we can
|
||||
@@ -105,8 +108,19 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
|
||||
DebugLogging.logRemoteMessage("FirebaseMessagingDelegate.onMessageReceived: message", remoteMessage)
|
||||
val notification = createNotification(remoteMessage)
|
||||
DebugLogging.logNotification("FirebaseMessagingDelegate.onMessageReceived: notification", notification)
|
||||
- NotificationsService.receive(context, notification)
|
||||
- runTaskManagerTasks(context.applicationContext, RemoteMessageSerializer.toBundle(remoteMessage))
|
||||
+ if (!ExpoBackgroundNotificationHandlerModule.isForegrounded) {
|
||||
+ BackgroundNotificationHandler(context, this).handleMessage(remoteMessage)
|
||||
+ } else {
|
||||
+ NotificationsService.receive(context, notification)
|
||||
+ runTaskManagerTasks(
|
||||
+ context.applicationContext,
|
||||
+ RemoteMessageSerializer.toBundle(remoteMessage)
|
||||
+ )
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ override fun showMessage(remoteMessage: RemoteMessage) {
|
||||
+ NotificationsService.receive(context, createNotification(remoteMessage))
|
||||
}
|
||||
|
||||
protected fun createNotification(remoteMessage: RemoteMessage): Notification {
|
||||
@@ -1,170 +0,0 @@
|
||||
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
|
||||
index bc479ee..1ebfa00 100644
|
||||
--- a/node_modules/expo-notifications/android/build.gradle
|
||||
+++ b/node_modules/expo-notifications/android/build.gradle
|
||||
@@ -42,6 +42,7 @@ dependencies {
|
||||
implementation 'com.google.firebase:firebase-messaging:24.0.1'
|
||||
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
+ implementation project(':expo-background-notification-handler')
|
||||
|
||||
if (project.findProject(':expo-modules-test-core')) {
|
||||
testImplementation project(':expo-modules-test-core')
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
|
||||
index 7b99e6c..45a450d 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
|
||||
@@ -15,6 +15,7 @@ import org.json.JSONObject
|
||||
* This interface exists to provide a common API for both classes.
|
||||
* */
|
||||
interface INotificationContent : Parcelable {
|
||||
+ val channelId: String?
|
||||
val title: String?
|
||||
val text: String?
|
||||
val subText: String?
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
index 191b64e..fe8b3c5 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
|
||||
@@ -35,6 +35,7 @@ import kotlin.coroutines.Continuation;
|
||||
* Refactoring this class may require a migration strategy for the data stored in SharedPreferences.
|
||||
*/
|
||||
public class NotificationContent implements Parcelable, Serializable, INotificationContent {
|
||||
+ private String mChannelId;
|
||||
private String mTitle;
|
||||
private String mText;
|
||||
private String mSubtitle;
|
||||
@@ -65,6 +66,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
}
|
||||
};
|
||||
|
||||
+ @Nullable
|
||||
+ public String getChannelId() {
|
||||
+ return mChannelId;
|
||||
+ }
|
||||
+
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
@@ -158,6 +164,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
}
|
||||
|
||||
protected NotificationContent(Parcel in) {
|
||||
+ mChannelId = in.readString();
|
||||
mTitle = in.readString();
|
||||
mText = in.readString();
|
||||
mSubtitle = in.readString();
|
||||
@@ -183,6 +190,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
+ dest.writeString(mChannelId);
|
||||
dest.writeString(mTitle);
|
||||
dest.writeString(mText);
|
||||
dest.writeString(mSubtitle);
|
||||
@@ -203,6 +211,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
private static final long serialVersionUID = 397666843266836802L;
|
||||
|
||||
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
|
||||
+ out.writeObject(mChannelId);
|
||||
out.writeObject(mTitle);
|
||||
out.writeObject(mText);
|
||||
out.writeObject(mSubtitle);
|
||||
@@ -285,6 +294,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
|
||||
useDefaultVibrationPattern();
|
||||
}
|
||||
|
||||
+ public Builder setChannelId(String channelId) {
|
||||
+ content.mChannelId = channelId;
|
||||
+ return this;
|
||||
+ }
|
||||
+
|
||||
public Builder setTitle(String title) {
|
||||
content.mTitle = title;
|
||||
return this;
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
|
||||
index 3af254c..3c77e9d 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
|
||||
@@ -11,6 +11,9 @@ import org.json.JSONObject
|
||||
* */
|
||||
@JvmInline
|
||||
value class NotificationData(private val data: Map<String, String>) {
|
||||
+ val channelId: String?
|
||||
+ get() = data["channelId"]
|
||||
+
|
||||
val title: String?
|
||||
get() = data["title"]
|
||||
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
|
||||
index d2cc6cf..6a48ff2 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
|
||||
@@ -31,6 +31,8 @@ class RemoteNotificationContent(private val remoteMessage: RemoteMessage) : INot
|
||||
return remoteMessage.notification?.imageUrl != null
|
||||
}
|
||||
|
||||
+ override val channelId = remoteMessage.notification?.channelId ?: notificationData.channelId
|
||||
+
|
||||
override val title = remoteMessage.notification?.title ?: notificationData.title
|
||||
|
||||
override val text = remoteMessage.notification?.body ?: notificationData.message
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
|
||||
index 98f003f..2f745e8 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
|
||||
@@ -101,6 +101,9 @@ open class ExpoNotificationBuilder(
|
||||
builder.setOngoing(content.isSticky)
|
||||
|
||||
// see "Notification anatomy" https://developer.android.com/develop/ui/views/notifications#Templates
|
||||
+ content.channelId?.let {
|
||||
+ builder.setChannelId(it)
|
||||
+ }
|
||||
builder.setContentTitle(content.title)
|
||||
builder.setContentText(content.text)
|
||||
builder.setSubText(content.subText)
|
||||
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
|
||||
index 90ca4ff..9d4cb09 100644
|
||||
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
|
||||
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
|
||||
@@ -3,6 +3,9 @@ package expo.modules.notifications.service.delegates
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandler
|
||||
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandlerInterface
|
||||
+import expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule
|
||||
import expo.modules.interfaces.taskManager.TaskServiceProviderHelper
|
||||
import expo.modules.notifications.notifications.RemoteMessageSerializer
|
||||
import expo.modules.notifications.notifications.background.BackgroundRemoteNotificationTaskConsumer
|
||||
@@ -18,7 +21,7 @@ import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
|
||||
+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface{
|
||||
companion object {
|
||||
// Unfortunately we cannot save state between instances of a service other way
|
||||
// than by static properties. Fortunately, using weak references we can
|
||||
@@ -105,8 +108,19 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
|
||||
DebugLogging.logRemoteMessage("FirebaseMessagingDelegate.onMessageReceived: message", remoteMessage)
|
||||
val notification = createNotification(remoteMessage)
|
||||
DebugLogging.logNotification("FirebaseMessagingDelegate.onMessageReceived: notification", notification)
|
||||
- NotificationsService.receive(context, notification)
|
||||
- runTaskManagerTasks(context.applicationContext, RemoteMessageSerializer.toBundle(remoteMessage))
|
||||
+ if (!ExpoBackgroundNotificationHandlerModule.isForegrounded) {
|
||||
+ BackgroundNotificationHandler(context, this).handleMessage(remoteMessage)
|
||||
+ } else {
|
||||
+ NotificationsService.receive(context, notification)
|
||||
+ runTaskManagerTasks(
|
||||
+ context.applicationContext,
|
||||
+ RemoteMessageSerializer.toBundle(remoteMessage)
|
||||
+ )
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ override fun showMessage(remoteMessage: RemoteMessage) {
|
||||
+ NotificationsService.receive(context, createNotification(remoteMessage))
|
||||
}
|
||||
|
||||
protected fun createNotification(remoteMessage: RemoteMessage): Notification {
|
||||
@@ -1,16 +0,0 @@
|
||||
diff --git a/node_modules/react-native/third-party-podspecs/fmt.podspec b/node_modules/react-native/third-party-podspecs/fmt.podspec
|
||||
index 2f38990..9b02e48 100644
|
||||
--- a/node_modules/react-native/third-party-podspecs/fmt.podspec
|
||||
+++ b/node_modules/react-native/third-party-podspecs/fmt.podspec
|
||||
@@ -26,4 +26,11 @@ Pod::Spec.new do |spec|
|
||||
spec.public_header_files = "include/fmt/*.h"
|
||||
spec.header_mappings_dir = "include"
|
||||
spec.source_files = ["include/fmt/*.h", "src/format.cc"]
|
||||
+
|
||||
+ # TODO: Remove after upgrading React Native past 0.83.x
|
||||
+ # Fix fmt 11.0.2 consteval build error with Xcode 26.4 (facebook/react-native#55601)
|
||||
+ # Fixed in RN 0.84+ which bumps fmt to a compatible version.
|
||||
+ spec.prepare_command = <<~SCRIPT
|
||||
+ perl -i -pe 's/^# define FMT_USE_CONSTEVAL 1$/# define FMT_USE_CONSTEVAL 0/' include/fmt/base.h
|
||||
+ SCRIPT
|
||||
end
|
||||
+11
-9
@@ -1,7 +1,8 @@
|
||||
import '#/logger/sentry/setup'
|
||||
import '#/view/icons'
|
||||
|
||||
import {Fragment, useEffect, useState} from 'react'
|
||||
import {useEffect, useState} from 'react'
|
||||
import * as React from 'react'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
|
||||
import {
|
||||
@@ -58,6 +59,7 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||
@@ -67,7 +69,6 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
|
||||
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {ToastOutlet} from '#/components/Toast'
|
||||
import {
|
||||
prefetchAgeAssuranceConfig,
|
||||
@@ -111,7 +112,7 @@ prefetchLiveEvents()
|
||||
prefetchAppConfig()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
const {currentAccount} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
@@ -139,9 +140,10 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), {
|
||||
type: 'info',
|
||||
})
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please sign in again.`),
|
||||
'info',
|
||||
)
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -151,7 +153,7 @@ function InnerApp() {
|
||||
<ContextMenuProvider>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<VideoVolumeProvider>
|
||||
<Fragment
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<AnalyticsFeaturesContext>
|
||||
@@ -207,7 +209,7 @@ function InnerApp() {
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</AnalyticsFeaturesContext>
|
||||
</Fragment>
|
||||
</React.Fragment>
|
||||
</VideoVolumeProvider>
|
||||
</Splash>
|
||||
</ContextMenuProvider>
|
||||
@@ -219,7 +221,7 @@ function InnerApp() {
|
||||
function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
|
||||
setReady(true),
|
||||
)
|
||||
|
||||
+28
-30
@@ -3,7 +3,6 @@ import '#/view/icons'
|
||||
import './style.css'
|
||||
|
||||
import {Fragment, useEffect, useState} from 'react'
|
||||
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -48,6 +47,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell/index'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||
@@ -58,7 +58,6 @@ import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdate
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {ToastOutlet} from '#/components/Toast'
|
||||
import {
|
||||
prefetchAgeAssuranceConfig,
|
||||
@@ -116,9 +115,10 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), {
|
||||
type: 'info',
|
||||
})
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please sign in again.`),
|
||||
'info',
|
||||
)
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -212,31 +212,29 @@ function App() {
|
||||
<Geo.Provider>
|
||||
<AppConfigProvider>
|
||||
<A11yProvider>
|
||||
<KeyboardControllerProvider>
|
||||
<OnboardingProvider>
|
||||
<AnalyticsContext>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</AnalyticsContext>
|
||||
</OnboardingProvider>
|
||||
</KeyboardControllerProvider>
|
||||
<OnboardingProvider>
|
||||
<AnalyticsContext>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</AnalyticsContext>
|
||||
</OnboardingProvider>
|
||||
</A11yProvider>
|
||||
</AppConfigProvider>
|
||||
</Geo.Provider>
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import {forwardRef, useCallback, useEffect, useState} from 'react'
|
||||
import {type PropsWithChildren} from 'react'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
Image as RNImage,
|
||||
@@ -51,7 +52,7 @@ type Props = {
|
||||
isReady: boolean
|
||||
}
|
||||
|
||||
export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
export function Splash(props: PropsWithChildren<Props>) {
|
||||
'use no memo'
|
||||
const insets = useSafeAreaInsets()
|
||||
const intro = useSharedValue(0)
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||
import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
|
||||
import {useSessionApi} from '#/state/session'
|
||||
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
|
||||
import {DeleteAccountDialog} from '#/screens/Settings/components/DeleteAccountDialog'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
|
||||
@@ -51,8 +49,6 @@ export function NoAccessScreen() {
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const insets = useSafeAreaInsets()
|
||||
const birthdateControl = useDialogControl()
|
||||
const deactivateAccountControl = useDialogControl()
|
||||
const deleteAccountControl = useDialogControl()
|
||||
const {data} = useAgeAssuranceDataContext()
|
||||
const region = useAgeAssuranceRegionConfig()
|
||||
const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
|
||||
@@ -75,7 +71,6 @@ export function NoAccessScreen() {
|
||||
hasDeclaredAge,
|
||||
canUpdateBirthday,
|
||||
})
|
||||
// TODO This can be cleaned up with useEffectEvent once we're on 19.2
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
@@ -239,38 +234,18 @@ export function NoAccessScreen() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[a.pt_lg, a.gap_xl, {maxWidth: 280}]}>
|
||||
<View style={[a.pt_lg, a.gap_xl]}>
|
||||
<Logo width={120} textFill={t.atoms.text.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.italic,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Text style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
To log out,{' '}
|
||||
<SimpleInlineLinkText
|
||||
label={_(msg`Click here to log out`)}
|
||||
{...createStaticClick(() => {
|
||||
onPressLogout()
|
||||
})}
|
||||
style={[a.italic]}>
|
||||
})}>
|
||||
click here
|
||||
</SimpleInlineLinkText>
|
||||
. Or if you’d prefer, you can{' '}
|
||||
<SimpleInlineLinkText
|
||||
label={_(msg`Click here to delete your account`)}
|
||||
{...createStaticClick(() => {
|
||||
ax.metric(
|
||||
'ageAssurance:noAccessScreen:openDeleteAccountDialog',
|
||||
{},
|
||||
)
|
||||
deleteAccountControl.open()
|
||||
})}
|
||||
style={[a.italic]}>
|
||||
delete your account
|
||||
</SimpleInlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
@@ -280,11 +255,6 @@ export function NoAccessScreen() {
|
||||
</View>
|
||||
|
||||
<BirthDateSettingsDialog control={birthdateControl} />
|
||||
<DeactivateAccountDialog control={deactivateAccountControl} />
|
||||
<DeleteAccountDialog
|
||||
control={deleteAccountControl}
|
||||
deactivateDialogControl={deactivateAccountControl}
|
||||
/>
|
||||
|
||||
{/*
|
||||
* While this blocking overlay is up, other dialogs in the shell
|
||||
|
||||
@@ -57,7 +57,7 @@ export const otherRequiredData: OtherRequiredData = {
|
||||
birthdate: new Date(2000, 1, 1).toISOString(),
|
||||
}
|
||||
|
||||
const serverStateEnabled = false || IS_E2E
|
||||
const serverStateEnabled = false
|
||||
export const serverState: AppBskyAgeassuranceGetState.OutputSchema | undefined =
|
||||
serverStateEnabled
|
||||
? {
|
||||
|
||||
@@ -2,10 +2,8 @@ import {createContext, useCallback, useContext, useEffect, useMemo} from 'react'
|
||||
|
||||
import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications'
|
||||
import {Provider as RedirectOverlayProvider} from '#/ageAssurance/components/RedirectOverlay'
|
||||
import {
|
||||
AgeAssuranceDataProvider,
|
||||
useAgeAssuranceDataContext,
|
||||
} from '#/ageAssurance/data'
|
||||
import {AgeAssuranceDataProvider} from '#/ageAssurance/data'
|
||||
import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
|
||||
import {logger} from '#/ageAssurance/logger'
|
||||
import {
|
||||
useAgeAssuranceState,
|
||||
|
||||
+2
-10
@@ -77,18 +77,10 @@ export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable contextual alternates and emoji overrides in Inter
|
||||
* Disable contextual alternates in Inter
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant}
|
||||
*/
|
||||
if (IS_WEB) {
|
||||
// @ts-expect-error - web supports 'unicode' as a valid value for fontVariant
|
||||
style.fontVariant = (style.fontVariant || []).concat(
|
||||
'no-contextual',
|
||||
'unicode',
|
||||
)
|
||||
} else {
|
||||
style.fontVariant = (style.fontVariant || []).concat('no-contextual')
|
||||
}
|
||||
style.fontVariant = (style.fontVariant || []).concat('no-contextual')
|
||||
} else {
|
||||
// fallback families only supported on web
|
||||
if (IS_WEB) {
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import {createContext, useCallback, useContext, useMemo, useState} from 'react'
|
||||
import {type PropsWithChildren} from 'react'
|
||||
import {type Theme, type ThemeName} from '@bsky.app/alf'
|
||||
|
||||
import {
|
||||
@@ -64,7 +65,7 @@ Context.displayName = 'AlfContext'
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
theme: themeName,
|
||||
}: React.PropsWithChildren<{theme: ThemeName}>) {
|
||||
}: PropsWithChildren<{theme: ThemeName}>) {
|
||||
const [fontScale, setFontScale] = useState<Alf['fonts']['scale']>(() =>
|
||||
getFontScale(),
|
||||
)
|
||||
|
||||
@@ -9,8 +9,6 @@ export enum Features {
|
||||
ImportContactsOnboardingDisable = 'import_contacts:onboarding:disable',
|
||||
ImportContactsSettingsDisable = 'import_contacts:settings:disable',
|
||||
LiveNowBetaDisable = 'live_now_beta:disable',
|
||||
ImageUploadsHighResolution = 'image_uploads:high_resolution',
|
||||
GroupChatsEnable = 'group_chats:enable',
|
||||
|
||||
AATest = 'aa-test',
|
||||
}
|
||||
|
||||
+19
-128
@@ -230,9 +230,6 @@ export type Events = {
|
||||
|
||||
'composer:gif:open': {}
|
||||
'composer:gif:select': {}
|
||||
'composer:image:edit': {
|
||||
platform: Platform['OS']
|
||||
}
|
||||
'composerPrompt:press': {}
|
||||
'composerPrompt:camera:press': {}
|
||||
'composerPrompt:gallery:press': {}
|
||||
@@ -473,19 +470,13 @@ export type Events = {
|
||||
profileDid: string
|
||||
position?: number
|
||||
}
|
||||
'profile:mute': {}
|
||||
'profile:unmute': {}
|
||||
'profile:block': {}
|
||||
'profile:unblock': {}
|
||||
'suggestedUser:follow': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'DiscoverInterstitial'
|
||||
| 'ProfileInterstitial'
|
||||
| 'ProfileHeader'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
| 'Onboarding'
|
||||
| 'SeeMoreSuggestedUsers'
|
||||
| 'ProgressGuide'
|
||||
location: 'Card' | 'Profile' | 'FollowAll'
|
||||
recId?: number | string
|
||||
position: number
|
||||
@@ -495,11 +486,9 @@ export type Events = {
|
||||
'suggestedUser:press': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'DiscoverInterstitial'
|
||||
| 'ProfileInterstitial'
|
||||
| 'ProfileHeader'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Onboarding'
|
||||
| 'SeeMoreSuggestedUsers'
|
||||
recId?: number | string
|
||||
position: number
|
||||
suggestedDid: string
|
||||
@@ -508,11 +497,10 @@ export type Events = {
|
||||
'suggestedUser:seen': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'DiscoverInterstitial'
|
||||
| 'ProfileInterstitial'
|
||||
| 'ProfileHeader'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
| 'Onboarding'
|
||||
| 'SeeMoreSuggestedUsers'
|
||||
| 'ProgressGuide'
|
||||
recId?: number | string
|
||||
position: number
|
||||
@@ -522,14 +510,13 @@ export type Events = {
|
||||
'suggestedUser:seeMore': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'DiscoverInterstitial'
|
||||
| 'ProfileInterstitial'
|
||||
| 'ProfileHeader'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
| 'Onboarding'
|
||||
recId?: number | string
|
||||
}
|
||||
'suggestedUser:dismiss': {
|
||||
logContext: 'DiscoverInterstitial' | 'ProfileInterstitial' | 'ProfileHeader'
|
||||
logContext: 'InterstitialDiscover' | 'InterstitialProfile'
|
||||
recId?: number | string
|
||||
position: number
|
||||
suggestedDid: string
|
||||
@@ -716,115 +703,20 @@ export type Events = {
|
||||
'reportDialog:failure': {}
|
||||
|
||||
translate: {
|
||||
os: Platform['OS']
|
||||
/**
|
||||
* The languages the content might be in, such as the user-supplied
|
||||
* language codes on posts. Currently only available on posts.
|
||||
*/
|
||||
possibleSourceLanguages: string[] | undefined
|
||||
/**
|
||||
* This is the user's configured primary language, which is always defined.
|
||||
*/
|
||||
expectedTargetLanguage: string
|
||||
/**
|
||||
* The length of the text being translated. We assume shorter texts are
|
||||
* more likely to have inaccurate translations.
|
||||
*/
|
||||
sourceLanguages: string[]
|
||||
targetLanguage: string
|
||||
textLength: number
|
||||
googleTranslate: boolean
|
||||
}
|
||||
'translate:result': {
|
||||
success: boolean
|
||||
method: 'on-device' | 'google-translate' | 'fallback-alert'
|
||||
os: Platform['OS']
|
||||
/**
|
||||
* The languages the content might be in, such as the user-supplied
|
||||
* language codes on posts. Currently only available on posts.
|
||||
*/
|
||||
possibleSourceLanguages: string[] | undefined
|
||||
/**
|
||||
* The language we expected the content to be in. This could be based on
|
||||
* user selection or on our confidence in the detected language. This is
|
||||
* nullable because we may not always have an expected source language.
|
||||
*/
|
||||
expectedSourceLanguage: string | null
|
||||
/**
|
||||
* This is the user's configured primary language, which is always defined.
|
||||
*/
|
||||
expectedTargetLanguage: string
|
||||
/**
|
||||
* The language the translation result was actually in. This is nullable
|
||||
* because the translation could have failed, in which case we won't have a
|
||||
* result source language.
|
||||
*/
|
||||
resultSourceLanguage: string | null
|
||||
/**
|
||||
* The language the translation result was translated into. This should be
|
||||
* the same as `expectedTargetLanguage`, but we include it for completeness
|
||||
* and in case there are any edge cases where they differ. This is nullable
|
||||
* because if the translation failed, we won't have a result target
|
||||
* language.
|
||||
*/
|
||||
resultTargetLanguage: string | null
|
||||
/**
|
||||
* The length of the text being translated. We assume shorter texts are
|
||||
* more likely to have inaccurate translations.
|
||||
*/
|
||||
textLength: number
|
||||
sourceLanguage: string | null
|
||||
targetLanguage: string
|
||||
}
|
||||
'translate:override': {
|
||||
os: Platform['OS']
|
||||
/**
|
||||
* The languages the content might be in, such as the user-supplied
|
||||
* language codes on posts. Currently only available on posts.
|
||||
*/
|
||||
possibleSourceLanguages: string[] | undefined
|
||||
/**
|
||||
* The language the user has indicated the content is actually in, which
|
||||
* may be different from the expected source language if the user is
|
||||
* overriding the auto-detected language. This is the language the user
|
||||
* wants to translate from after overriding.
|
||||
*/
|
||||
expectedSourceLanguage: string
|
||||
/**
|
||||
* This is the user's configured primary language, which is always defined.
|
||||
*/
|
||||
expectedTargetLanguage: string
|
||||
/**
|
||||
* The language the translation result was actually in, which the user now
|
||||
* wishes to override.
|
||||
*/
|
||||
resultSourceLanguage: string
|
||||
}
|
||||
|
||||
'postMenu:openMuteWordsDialog': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'postMenu:muteAccount': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'postMenu:unmuteAccount': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'postMenu:blockAccount': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'postMenu:reportPost': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
sourceLanguage: string
|
||||
targetLanguage: string
|
||||
}
|
||||
|
||||
'verification:create': {}
|
||||
@@ -920,7 +812,6 @@ export type Events = {
|
||||
canUpdateBirthday: boolean
|
||||
}
|
||||
'ageAssurance:noAccessScreen:openBirthdateDialog': {}
|
||||
'ageAssurance:noAccessScreen:openDeleteAccountDialog': {}
|
||||
|
||||
/*
|
||||
* Specifically for the `BlockedGeoOverlay`
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {type ComponentType, type ReactElement, type ReactNode} from 'react'
|
||||
import {
|
||||
type AccessibilityProps,
|
||||
type GestureResponderEvent,
|
||||
@@ -82,8 +83,8 @@ export type ButtonState = {
|
||||
export type ButtonContext = VariantProps & ButtonState
|
||||
|
||||
type NonTextElements =
|
||||
| React.ReactElement<any>
|
||||
| Iterable<React.ReactElement<any> | null | undefined | boolean>
|
||||
| ReactElement<any>
|
||||
| Iterable<ReactElement<any> | null | undefined | boolean>
|
||||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
@@ -109,7 +110,7 @@ export type ButtonProps = Pick<
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
|
||||
PressableComponent?: React.ComponentType<PressableProps>
|
||||
PressableComponent?: ComponentType<PressableProps>
|
||||
}
|
||||
|
||||
export type ButtonTextProps = TextProps &
|
||||
@@ -776,7 +777,7 @@ export function ButtonIcon({
|
||||
icon: Comp,
|
||||
size,
|
||||
}: {
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
icon: ComponentType<SVGIconProps>
|
||||
/**
|
||||
* @deprecated no longer needed
|
||||
*/
|
||||
@@ -894,8 +895,8 @@ export type StackedButtonProps = Omit<
|
||||
keyof VariantProps | 'children'
|
||||
> &
|
||||
Pick<VariantProps, 'color'> & {
|
||||
children: React.ReactNode
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
children: ReactNode
|
||||
icon: ComponentType<SVGIconProps>
|
||||
}
|
||||
|
||||
export function StackedButton({children, ...props}: StackedButtonProps) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {type ReactNode} from 'react'
|
||||
import {
|
||||
BackHandler,
|
||||
Keyboard,
|
||||
@@ -102,7 +103,7 @@ const SPRING_OUT: WithSpringConfig = {
|
||||
/**
|
||||
* Needs placing near the top of the provider stack, but BELOW the theme provider.
|
||||
*/
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
export function Provider({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<PortalProvider>
|
||||
{children}
|
||||
@@ -111,7 +112,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function Root({children}: {children: React.ReactNode}) {
|
||||
export function Root({children}: {children: ReactNode}) {
|
||||
const playHaptic = useHaptics()
|
||||
const [mode, setMode] = useState<'full' | 'auxiliary-only'>('full')
|
||||
const [measurement, setMeasurement] = useState<Measurement | null>(null)
|
||||
@@ -572,7 +573,7 @@ export function Outer({
|
||||
style,
|
||||
align = 'left',
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
align?: 'left' | 'right'
|
||||
}) {
|
||||
@@ -895,7 +896,7 @@ export function ItemRadio({selected}: {selected: boolean}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function LabelText({children}: {children: React.ReactNode}) {
|
||||
export function LabelText({children}: {children: ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {type PropsWithChildren, type ReactNode} from 'react'
|
||||
import {
|
||||
FlatList,
|
||||
type FlatListProps,
|
||||
@@ -52,7 +53,7 @@ export function Outer({
|
||||
control,
|
||||
onClose,
|
||||
webOptions,
|
||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||
}: PropsWithChildren<DialogOuterProps>) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@@ -265,7 +266,7 @@ export function FlatListFooter({
|
||||
children,
|
||||
onLayout,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: ReactNode
|
||||
onLayout?: (event: LayoutChangeEvent) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
useRemoveFeedMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, select, useTheme} from '#/alf'
|
||||
import {
|
||||
@@ -32,7 +33,6 @@ import {Link as InternalLink, type LinkProps} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText, type RichTextProps} from '#/components/RichText'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useActiveLiveEventFeedUris} from '#/features/liveEvents/context'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
@@ -313,9 +313,7 @@ function SaveButtonInner({
|
||||
Toast.show(l({message: 'Feeds updated!', context: 'toast'}))
|
||||
} catch (err: any) {
|
||||
logger.error(err, {message: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(l`Failed to update feeds`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`Failed to update feeds`, 'xmark')
|
||||
}
|
||||
},
|
||||
[l, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
|
||||
|
||||
@@ -8,7 +8,9 @@ import Animated, {
|
||||
LinearTransition,
|
||||
} from 'react-native-reanimated'
|
||||
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
@@ -16,7 +18,10 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorWithDismiss} from '#/state/queries/suggested-follows'
|
||||
import {
|
||||
useSuggestedFollowsByActorQuery,
|
||||
useSuggestedFollowsQuery,
|
||||
} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {type SeenPost} from '#/state/userActionHistory'
|
||||
@@ -165,12 +170,10 @@ function useExperimentalSuggestedUsersQuery() {
|
||||
if (followSuggestions.length > 0) {
|
||||
suggestedDids = [
|
||||
// It's ok if these will pick the same item (weighed by its frequency)
|
||||
/* eslint-disable react-hooks/purity */
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
/* eslint-enable react-hooks/purity */
|
||||
]
|
||||
}
|
||||
const seenDids = seen
|
||||
@@ -213,14 +216,86 @@ export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
|
||||
}
|
||||
|
||||
export function SuggestedFollowsProfile({did}: {did: string}) {
|
||||
const {profiles, recId, onDismiss, isLoading, error} =
|
||||
useSuggestedFollowsByActorWithDismiss({did})
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const maxLength = gtMobile ? 4 : 6
|
||||
const {
|
||||
isLoading: isSuggestionsLoading,
|
||||
data,
|
||||
error,
|
||||
} = useSuggestedFollowsByActorQuery({
|
||||
did,
|
||||
})
|
||||
const {
|
||||
data: moreSuggestions,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useSuggestedFollowsQuery({limit: 25})
|
||||
|
||||
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set())
|
||||
|
||||
const onDismiss = useCallback((dismissedDid: string) => {
|
||||
setDismissedDids(prev => new Set(prev).add(dismissedDid))
|
||||
}, [])
|
||||
|
||||
// Combine profiles from the actor-specific query with fallback suggestions
|
||||
const allProfiles = useMemo(() => {
|
||||
const actorProfiles = data?.suggestions ?? []
|
||||
const fallbackProfiles =
|
||||
moreSuggestions?.pages.flatMap(page =>
|
||||
page.actors.map(actor => ({actor, recId: page.recId})),
|
||||
) ?? []
|
||||
|
||||
// Dedupe by did, preferring actor-specific profiles
|
||||
const seen = new Set<string>()
|
||||
const combined: {actor: bsky.profile.AnyProfileView; recId?: number}[] = []
|
||||
|
||||
for (const profile of actorProfiles) {
|
||||
if (!seen.has(profile.did)) {
|
||||
seen.add(profile.did)
|
||||
combined.push({actor: profile, recId: data?.recId})
|
||||
}
|
||||
}
|
||||
|
||||
for (const profile of fallbackProfiles) {
|
||||
if (!seen.has(profile.actor.did) && profile.actor.did !== did) {
|
||||
seen.add(profile.actor.did)
|
||||
combined.push(profile)
|
||||
}
|
||||
}
|
||||
|
||||
return combined
|
||||
}, [data?.suggestions, moreSuggestions?.pages, did, data?.recId])
|
||||
|
||||
const filteredProfiles = useMemo(() => {
|
||||
return allProfiles.filter(p => !dismissedDids.has(p.actor.did))
|
||||
}, [allProfiles, dismissedDids])
|
||||
|
||||
// Fetch more when running low
|
||||
useEffect(() => {
|
||||
if (
|
||||
moderationOpts &&
|
||||
filteredProfiles.length < maxLength &&
|
||||
hasNextPage &&
|
||||
!isFetchingNextPage
|
||||
) {
|
||||
void fetchNextPage()
|
||||
}
|
||||
}, [
|
||||
filteredProfiles.length,
|
||||
maxLength,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
moderationOpts,
|
||||
])
|
||||
|
||||
return (
|
||||
<ProfileGrid
|
||||
isSuggestionsLoading={isLoading}
|
||||
profiles={profiles}
|
||||
recId={recId}
|
||||
isSuggestionsLoading={isSuggestionsLoading}
|
||||
profiles={filteredProfiles}
|
||||
totalProfileCount={allProfiles.length}
|
||||
error={error}
|
||||
viewContext="profile"
|
||||
onDismiss={onDismiss}
|
||||
@@ -229,11 +304,21 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
|
||||
}
|
||||
|
||||
export function SuggestedFollowsHome() {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const maxLength = gtMobile ? 4 : 6
|
||||
const {
|
||||
isLoading: isSuggestionsLoading,
|
||||
profiles: experimentalProfiles,
|
||||
error: experimentalError,
|
||||
} = useExperimentalSuggestedUsersQuery()
|
||||
const {
|
||||
data: moreSuggestions,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
error: suggestionsError,
|
||||
} = useSuggestedFollowsQuery({limit: 25})
|
||||
|
||||
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set())
|
||||
|
||||
@@ -241,29 +326,66 @@ export function SuggestedFollowsHome() {
|
||||
setDismissedDids(prev => new Set(prev).add(did))
|
||||
}, [])
|
||||
|
||||
// Combine profiles from experimental query with paginated suggestions
|
||||
const allProfiles = useMemo(() => {
|
||||
const result: Array<{
|
||||
const fallbackProfiles =
|
||||
moreSuggestions?.pages.flatMap(page =>
|
||||
page.actors.map(actor => ({actor, recId: page.recId})),
|
||||
) ?? []
|
||||
|
||||
// Dedupe by did, preferring experimental profiles
|
||||
const seen = new Set<string>()
|
||||
const combined: Array<{
|
||||
actor: bsky.profile.AnyProfileView
|
||||
recId?: string
|
||||
recId?: number
|
||||
}> = []
|
||||
|
||||
for (const profile of experimentalProfiles) {
|
||||
result.push({actor: profile, recId: undefined})
|
||||
if (!seen.has(profile.did)) {
|
||||
seen.add(profile.did)
|
||||
combined.push({actor: profile, recId: undefined})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [experimentalProfiles])
|
||||
for (const profile of fallbackProfiles) {
|
||||
if (!seen.has(profile.actor.did)) {
|
||||
seen.add(profile.actor.did)
|
||||
combined.push(profile)
|
||||
}
|
||||
}
|
||||
|
||||
return combined
|
||||
}, [experimentalProfiles, moreSuggestions?.pages])
|
||||
|
||||
const filteredProfiles = useMemo(() => {
|
||||
return allProfiles.filter(p => !dismissedDids.has(p.actor.did))
|
||||
}, [allProfiles, dismissedDids])
|
||||
|
||||
// Fetch more when running low
|
||||
useEffect(() => {
|
||||
if (
|
||||
moderationOpts &&
|
||||
filteredProfiles.length < maxLength &&
|
||||
hasNextPage &&
|
||||
!isFetchingNextPage
|
||||
) {
|
||||
void fetchNextPage()
|
||||
}
|
||||
}, [
|
||||
filteredProfiles.length,
|
||||
maxLength,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
moderationOpts,
|
||||
])
|
||||
|
||||
return (
|
||||
<ProfileGrid
|
||||
isSuggestionsLoading={isSuggestionsLoading}
|
||||
profiles={filteredProfiles}
|
||||
totalProfileCount={allProfiles.length}
|
||||
error={experimentalError}
|
||||
error={experimentalError || suggestionsError}
|
||||
viewContext="feed"
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
@@ -274,26 +396,22 @@ export function ProfileGrid({
|
||||
isSuggestionsLoading,
|
||||
error,
|
||||
profiles,
|
||||
recId,
|
||||
totalProfileCount,
|
||||
viewContext = 'feed',
|
||||
onDismiss,
|
||||
isVisible = true,
|
||||
onRequestHide,
|
||||
}: {
|
||||
isSuggestionsLoading: boolean
|
||||
profiles: {actor: bsky.profile.AnyProfileView; recId?: string}[]
|
||||
recId?: string
|
||||
profiles: {actor: bsky.profile.AnyProfileView; recId?: number}[]
|
||||
totalProfileCount?: number
|
||||
error: Error | null
|
||||
viewContext: 'profile' | 'profileHeader' | 'feed'
|
||||
onDismiss?: (did: string) => void
|
||||
isVisible?: boolean
|
||||
onRequestHide?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const followDialogControl = useDialogControl()
|
||||
@@ -310,10 +428,10 @@ export function ProfileGrid({
|
||||
const containerRef = useRef<View>(null)
|
||||
const hasTrackedRef = useRef(false)
|
||||
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
|
||||
? 'DiscoverInterstitial'
|
||||
? 'InterstitialDiscover'
|
||||
: isProfileHeaderContext
|
||||
? 'ProfileHeader'
|
||||
: 'ProfileInterstitial'
|
||||
? 'Profile'
|
||||
: 'InterstitialProfile'
|
||||
|
||||
// Callback to fire seen events
|
||||
const fireSeen = useCallback(() => {
|
||||
@@ -334,7 +452,7 @@ export function ProfileGrid({
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [isLoading, error, profiles, maxLength, ax, logContext])
|
||||
}, [ax, isLoading, error, profiles, maxLength, logContext])
|
||||
|
||||
// For profile header, fire when isVisible becomes true
|
||||
useEffect(() => {
|
||||
@@ -422,7 +540,9 @@ export function ProfileGrid({
|
||||
profile={profile.actor}
|
||||
onPress={() => {
|
||||
ax.metric('suggestedUser:press', {
|
||||
logContext,
|
||||
logContext: isFeedContext
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
recId: profile.recId,
|
||||
position: index,
|
||||
suggestedDid: profile.actor.did,
|
||||
@@ -438,12 +558,14 @@ export function ProfileGrid({
|
||||
<ProfileCard.Outer>
|
||||
{onDismiss && (
|
||||
<Button
|
||||
label={l`Dismiss this suggestion`}
|
||||
label={_(msg`Dismiss this suggestion`)}
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
onDismiss(profile.actor.did)
|
||||
ax.metric('suggestedUser:dismiss', {
|
||||
logContext,
|
||||
logContext: isFeedContext
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
position: index,
|
||||
suggestedDid: profile.actor.did,
|
||||
recId: profile.recId,
|
||||
@@ -509,8 +631,10 @@ export function ProfileGrid({
|
||||
style={[a.rounded_sm]}
|
||||
onFollow={() => {
|
||||
ax.metric('suggestedUser:follow', {
|
||||
logContext,
|
||||
location: 'Profile',
|
||||
logContext: isFeedContext
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
location: 'Card',
|
||||
recId: profile.recId,
|
||||
position: index,
|
||||
suggestedDid: profile.actor.did,
|
||||
@@ -527,13 +651,6 @@ export function ProfileGrid({
|
||||
|
||||
// Use totalProfileCount (before dismissals) for minLength check on initial render.
|
||||
const profileCountForMinCheck = totalProfileCount ?? profiles.length
|
||||
|
||||
useEffect(() => {
|
||||
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
|
||||
onRequestHide?.()
|
||||
}
|
||||
}, [error, isLoading, onRequestHide, profileCountForMinCheck, minLength])
|
||||
|
||||
if (error || (!isLoading && profileCountForMinCheck < minLength)) {
|
||||
ax.logger.debug(`Not enough profiles to show suggested follows`)
|
||||
return null
|
||||
@@ -562,12 +679,11 @@ export function ProfileGrid({
|
||||
</Text>
|
||||
{!isProfileHeaderContext && (
|
||||
<Button
|
||||
label={l`See more suggested profiles`}
|
||||
label={_(msg`See more suggested profiles`)}
|
||||
onPress={() => {
|
||||
followDialogControl.open()
|
||||
ax.metric('suggestedUser:seeMore', {
|
||||
logContext,
|
||||
recId,
|
||||
logContext: isFeedContext ? 'Explore' : 'Profile',
|
||||
})
|
||||
}}>
|
||||
{({hovered}) => (
|
||||
@@ -587,7 +703,9 @@ export function ProfileGrid({
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<FollowDialogWithoutGuide control={followDialogControl} />
|
||||
|
||||
<LayoutAnimationConfig skipExiting skipEntering>
|
||||
{gtMobile ? (
|
||||
<View style={[a.p_lg, a.pt_md]}>
|
||||
@@ -610,7 +728,7 @@ export function ProfileGrid({
|
||||
onPress={() => {
|
||||
followDialogControl.open()
|
||||
ax.metric('suggestedUser:seeMore', {
|
||||
logContext,
|
||||
logContext: 'Explore',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
@@ -624,11 +742,11 @@ export function ProfileGrid({
|
||||
}
|
||||
|
||||
function SeeMoreSuggestedProfilesCard({onPress}: {onPress: () => void}) {
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={l`Browse more accounts`}
|
||||
label={_(msg`Browse more accounts`)}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
a.flex_col,
|
||||
@@ -652,7 +770,7 @@ const numFeedsToDisplay = 3
|
||||
export function SuggestedFeeds() {
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const {data, isLoading, error} = useGetPopularFeedsQuery({
|
||||
limit: numFeedsToDisplay,
|
||||
})
|
||||
@@ -739,7 +857,7 @@ export function SuggestedFeeds() {
|
||||
a.gap_md,
|
||||
]}>
|
||||
<InlineLinkText
|
||||
label={l`Browse more suggestions`}
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
@@ -758,7 +876,7 @@ export function SuggestedFeeds() {
|
||||
{content}
|
||||
|
||||
<Button
|
||||
label={l`Browse more feeds on the Explore page`}
|
||||
label={_(msg`Browse more feeds on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {View} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, type ViewStyleProp} from '#/alf'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {type AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
import type React from 'react'
|
||||
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import type React from 'react'
|
||||
|
||||
import {gradients} from '#/alf/tokens'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {type PropsWithChildren} from 'react'
|
||||
import {type GestureResponderEvent, Linking} from 'react-native'
|
||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||
import {
|
||||
@@ -318,7 +319,7 @@ export function Link({
|
||||
)
|
||||
}
|
||||
|
||||
export type InlineLinkProps = React.PropsWithChildren<
|
||||
export type InlineLinkProps = PropsWithChildren<
|
||||
BaseLinkProps &
|
||||
TextStyleProp &
|
||||
Pick<TextProps, 'selectable' | 'numberOfLines' | 'emoji'> &
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {StyleSheet} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, platform, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import {Fill} from '#/components/Fill'
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type GestureResponderEvent,
|
||||
type PressableProps,
|
||||
} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {type TextStyleProp, type ViewStyleProp} from '#/alf'
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
|
||||
@@ -27,7 +27,6 @@ export function NewskieDialog({
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const control = useDialogControl()
|
||||
|
||||
@@ -53,7 +52,7 @@ export function NewskieDialog({
|
||||
{({hovered, pressed}) => (
|
||||
<Newskie
|
||||
size="lg"
|
||||
fill={t.palette.yellow}
|
||||
fill="#FFC404"
|
||||
style={{
|
||||
opacity: hovered || pressed ? 0.5 : 1,
|
||||
}}
|
||||
@@ -133,7 +132,7 @@ function DialogInner({
|
||||
<Newskie
|
||||
width={64}
|
||||
height={64}
|
||||
fill={t.palette.yellow}
|
||||
fill="#FFC404"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {useMemo} from 'react'
|
||||
import {type ReactNode} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -30,8 +31,7 @@ export function Row({
|
||||
children,
|
||||
style,
|
||||
size = 'sm',
|
||||
}: {children: React.ReactNode | React.ReactNode[]} & CommonProps &
|
||||
ViewStyleProp) {
|
||||
}: {children: ReactNode | ReactNode[]} & CommonProps & ViewStyleProp) {
|
||||
const styles = useMemo(() => {
|
||||
switch (size) {
|
||||
case 'lg':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useRef, useState} from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
type GestureResponderEvent,
|
||||
@@ -31,16 +31,16 @@ export function ExternalGif({
|
||||
const consentDialogControl = useDialogControl()
|
||||
|
||||
// Tracking if the placer has been activated
|
||||
const [isPlayerActive, setIsPlayerActive] = useState(false)
|
||||
const [isPlayerActive, setIsPlayerActive] = React.useState(false)
|
||||
// Tracking whether the gif has been loaded yet
|
||||
const [isPrefetched, setIsPrefetched] = useState(false)
|
||||
const [isPrefetched, setIsPrefetched] = React.useState(false)
|
||||
// Tracking whether the image is animating
|
||||
const [isAnimating, setIsAnimating] = useState(true)
|
||||
const [isAnimating, setIsAnimating] = React.useState(true)
|
||||
|
||||
// Used for controlling animation
|
||||
const imageRef = useRef<Image>(null)
|
||||
const imageRef = React.useRef<Image>(null)
|
||||
|
||||
const load = useCallback(() => {
|
||||
const load = React.useCallback(() => {
|
||||
setIsPlayerActive(true)
|
||||
Image.prefetch(params.playerUri).then(() => {
|
||||
// Replace the image once it's fetched
|
||||
@@ -48,7 +48,7 @@ export function ExternalGif({
|
||||
})
|
||||
}, [params.playerUri])
|
||||
|
||||
const onPlayPress = useCallback(
|
||||
const onPlayPress = React.useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
// Don't propagate on web
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
type GestureResponderEvent,
|
||||
@@ -84,7 +84,7 @@ function Player({
|
||||
}) {
|
||||
// ensures we only load what's requested
|
||||
// when it's a youtube video, we need to allow both bsky.app and youtube.com
|
||||
const onShouldStartLoadWithRequest = useCallback(
|
||||
const onShouldStartLoadWithRequest = React.useCallback(
|
||||
(event: ShouldStartLoadRequest) =>
|
||||
event.url === params.playerUri ||
|
||||
(params.source.startsWith('youtube') &&
|
||||
@@ -129,10 +129,10 @@ export function ExternalPlayer({
|
||||
const externalEmbedsPrefs = useExternalEmbedsPrefs()
|
||||
const consentDialogControl = useDialogControl()
|
||||
|
||||
const [isPlayerActive, setPlayerActive] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isPlayerActive, setPlayerActive] = React.useState(false)
|
||||
const [isLoading, setIsLoading] = React.useState(true)
|
||||
|
||||
const aspect = useMemo(() => {
|
||||
const aspect = React.useMemo(() => {
|
||||
return getPlayerAspect({
|
||||
type: params.type,
|
||||
width: windowDims.width,
|
||||
@@ -166,7 +166,7 @@ export function ExternalPlayer({
|
||||
}, false) // False here disables autostarting the callback
|
||||
|
||||
// watch for leaving the viewport due to scrolling
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
// We don't want to do anything if the player isn't active
|
||||
if (!isPlayerActive) return
|
||||
|
||||
@@ -185,11 +185,11 @@ export function ExternalPlayer({
|
||||
}
|
||||
}, [navigation, isPlayerActive, frameCallback])
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
const onLoad = React.useCallback(() => {
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
const onPlayPress = useCallback(
|
||||
const onPlayPress = React.useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
// Prevent this from propagating upward on web
|
||||
event.preventDefault()
|
||||
@@ -204,7 +204,7 @@ export function ExternalPlayer({
|
||||
[externalEmbedsPrefs, consentDialogControl, params.source],
|
||||
)
|
||||
|
||||
const onAcceptConsent = useCallback(() => {
|
||||
const onAcceptConsent = React.useCallback(() => {
|
||||
setPlayerActive(true)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
@@ -38,7 +38,7 @@ export const ExternalEmbed = ({
|
||||
const externalEmbedPrefs = useExternalEmbedsPrefs()
|
||||
const niceUrl = toNiceDomain(link.uri)
|
||||
const imageUri = link.thumb
|
||||
const embedPlayerParams = useMemo(() => {
|
||||
const embedPlayerParams = React.useMemo(() => {
|
||||
const params = parseEmbedPlayerFromUrl(link.uri)
|
||||
|
||||
if (params && externalEmbedPrefs?.[params.source] !== 'hide') {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
createContext,
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
@@ -12,7 +10,7 @@ import {useWindowDimensions} from 'react-native'
|
||||
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
|
||||
const Context = createContext<{
|
||||
const Context = React.createContext<{
|
||||
activeViewId: string | null
|
||||
setActiveView: (viewId: string) => void
|
||||
sendViewPosition: (viewId: string, y: number) => void
|
||||
@@ -96,7 +94,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
||||
}
|
||||
|
||||
export function useActiveVideoWeb() {
|
||||
const context = useContext(Context)
|
||||
const context = React.useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useActiveVideoWeb must be used within a ActiveVideoWebProvider',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useEffect, useId, useRef, useState} from 'react'
|
||||
import {useEffect, useId, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
@@ -37,7 +37,7 @@ export function VideoEmbedInnerWeb({
|
||||
throw error
|
||||
}
|
||||
|
||||
const {hlsRef, loop, updateCuePositions} = useHLS({
|
||||
const {hlsRef, loop} = useHLS({
|
||||
playlist: embed.playlist,
|
||||
setHasSubtitleTrack,
|
||||
setError,
|
||||
@@ -90,7 +90,6 @@ export function VideoEmbedInnerWeb({
|
||||
hasSubtitleTrack={hasSubtitleTrack}
|
||||
isGif={embed.presentation === 'gif'}
|
||||
altText={embed.alt}
|
||||
updateCuePositions={updateCuePositions}
|
||||
/>
|
||||
</div>
|
||||
</View>
|
||||
@@ -146,47 +145,6 @@ function useHLS({
|
||||
}, [Hls, setHlsLoading])
|
||||
|
||||
const hlsRef = useRef<HlsTypes.default | undefined>(undefined)
|
||||
const controlsVisibleRef = useRef(false)
|
||||
|
||||
/**
|
||||
* Repositions VTT subtitle cues using percentage-based line values
|
||||
* (snapToLines=false) so that multi-line/wrapped cues grow upward
|
||||
* instead of extending offscreen. Moves cues higher when controls
|
||||
* are visible to avoid occlusion by the scrub bar.
|
||||
*
|
||||
* Called from two sites:
|
||||
* - SUBTITLE_FRAG_PROCESSED: applies positioning to newly loaded cues
|
||||
* - VideoControls effect: updates positioning when controls show/hide
|
||||
*/
|
||||
const updateCuePositions = useCallback(
|
||||
(controlsVisible?: boolean) => {
|
||||
if (controlsVisible != null) {
|
||||
// save controlsVisible state so that when it's called from SUBTITLE_FRAG_PROCESSED,
|
||||
// the most recent value is used (as we won't know the control state there)
|
||||
controlsVisibleRef.current = controlsVisible
|
||||
}
|
||||
// magic numbers: cue position, % from top of video
|
||||
const line = controlsVisibleRef.current ? 70 : 85
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
for (let i = 0; i < video.textTracks.length; i++) {
|
||||
const track = video.textTracks[i]
|
||||
if (track.cues) {
|
||||
for (let j = 0; j < track.cues.length; j++) {
|
||||
const cue = track.cues[j] as VTTCue
|
||||
cue.snapToLines = false
|
||||
cue.line = line
|
||||
}
|
||||
}
|
||||
// toggle track mode to force the browser to re-render active cues
|
||||
if (track.mode === 'showing') {
|
||||
track.mode = 'hidden'
|
||||
track.mode = 'showing'
|
||||
}
|
||||
}
|
||||
},
|
||||
[videoRef],
|
||||
)
|
||||
const [lowQualityFragments, setLowQualityFragments] = useState<
|
||||
HlsTypes.Fragment[]
|
||||
>([])
|
||||
@@ -262,10 +220,6 @@ function useHLS({
|
||||
}
|
||||
})
|
||||
|
||||
hls.on(Hls.Events.SUBTITLE_FRAG_PROCESSED, () => {
|
||||
updateCuePositions()
|
||||
})
|
||||
|
||||
hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => {
|
||||
if (frag.level === 0) {
|
||||
setLowQualityFragments(prev => [...prev, frag])
|
||||
@@ -353,6 +307,5 @@ function useHLS({
|
||||
return {
|
||||
hlsRef,
|
||||
loop: !hasLowQualityFragmentAtStart,
|
||||
updateCuePositions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ export function Controls({
|
||||
hasSubtitleTrack,
|
||||
isGif,
|
||||
altText,
|
||||
updateCuePositions,
|
||||
}: {
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>
|
||||
hlsRef: React.RefObject<Hls | undefined | null>
|
||||
@@ -62,7 +61,6 @@ export function Controls({
|
||||
hasSubtitleTrack: boolean
|
||||
isGif: boolean
|
||||
altText?: string
|
||||
updateCuePositions: (controlsVisible?: boolean) => void
|
||||
}) {
|
||||
const {
|
||||
play,
|
||||
@@ -296,13 +294,6 @@ export function Controls({
|
||||
((focused || autoplayDisabled) && !playing) ||
|
||||
(interactingViaKeypress ? hasFocus : hovered)
|
||||
|
||||
// adjust subtitle cue positioning to avoid occlusion by controls
|
||||
// uses percentage-based positioning (snapToLines=false) so wrapped
|
||||
// multi-line cues grow upward instead of extending offscreen
|
||||
useEffect(() => {
|
||||
updateCuePositions(showControls)
|
||||
}, [showControls, updateCuePositions])
|
||||
|
||||
if (isGif) {
|
||||
return (
|
||||
<GifPresentationControls
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {createContext, useContext, useMemo, useState} from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const Context = createContext<{
|
||||
const Context = React.createContext<{
|
||||
// native
|
||||
muted: boolean
|
||||
setMuted: React.Dispatch<React.SetStateAction<boolean>>
|
||||
// web
|
||||
@@ -10,10 +11,10 @@ const Context = createContext<{
|
||||
Context.displayName = 'VideoVolumeContext'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [muted, setMuted] = useState(true)
|
||||
const [volume, setVolume] = useState(1)
|
||||
const [muted, setMuted] = React.useState(true)
|
||||
const [volume, setVolume] = React.useState(1)
|
||||
|
||||
const value = useMemo(
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
muted,
|
||||
setMuted,
|
||||
@@ -27,7 +28,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
||||
}
|
||||
|
||||
export function useVideoVolumeState() {
|
||||
const context = useContext(Context)
|
||||
const context = React.useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useVideoVolumeState must be used within a VideoVolumeProvider',
|
||||
@@ -37,7 +38,7 @@ export function useVideoVolumeState() {
|
||||
}
|
||||
|
||||
export function useVideoMuteState() {
|
||||
const context = useContext(Context)
|
||||
const context = React.useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useVideoMuteState must be used within a VideoVolumeProvider',
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {Platform, type StyleProp, type TextStyle, View} from 'react-native'
|
||||
import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
|
||||
import {useTranslate} from '#/lib/translation'
|
||||
import {
|
||||
type TranslationFunction,
|
||||
type TranslationFunctionParams,
|
||||
} from '#/lib/translation'
|
||||
import {type TranslationFunction} from '#/lib/translation'
|
||||
import {
|
||||
codeToLanguageName,
|
||||
getPostLanguageTags,
|
||||
isPostInLanguage,
|
||||
languageName,
|
||||
} from '#/locale/helpers'
|
||||
@@ -28,17 +25,18 @@ import * as Select from '#/components/Select'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_WEB} from '#/env'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const X_ICON_OFFSET = 16
|
||||
|
||||
export function TranslatedPost({
|
||||
hideTranslateLink = false,
|
||||
post,
|
||||
postText,
|
||||
postTextStyle = a.text_md,
|
||||
}: {
|
||||
hideTranslateLink?: boolean
|
||||
post: AppBskyFeedDefs.PostView
|
||||
postText: string
|
||||
postTextStyle?: StyleProp<TextStyle>
|
||||
}) {
|
||||
const langPrefs = useLanguagePrefs()
|
||||
@@ -46,21 +44,6 @@ export function TranslatedPost({
|
||||
key: post.uri,
|
||||
})
|
||||
|
||||
const record = useMemo<AppBskyFeedPost.Record | undefined>(() => {
|
||||
return bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
? post.record
|
||||
: undefined
|
||||
}, [post])
|
||||
const initialTranslationParams = useMemo<TranslationFunctionParams>(() => {
|
||||
return {
|
||||
text: record?.text || '',
|
||||
expectedTargetLanguage: langPrefs.primaryLanguage,
|
||||
possibleSourceLanguages: getPostLanguageTags(post),
|
||||
}
|
||||
}, [post, record, langPrefs])
|
||||
const needsTranslation = useMemo(() => {
|
||||
if (hideTranslateLink) return false
|
||||
return !isPostInLanguage(post, [langPrefs.primaryLanguage])
|
||||
@@ -72,11 +55,11 @@ export function TranslatedPost({
|
||||
case 'success':
|
||||
return (
|
||||
<TranslationResult
|
||||
translate={translate}
|
||||
clearTranslation={clearTranslation}
|
||||
initialTranslationParams={initialTranslationParams}
|
||||
translate={translate}
|
||||
postText={postText}
|
||||
postTextStyle={postTextStyle}
|
||||
resultSourceLanguage={
|
||||
sourceLanguage={
|
||||
translationState.sourceLanguage ?? null // Fallback primarily for iOS
|
||||
}
|
||||
translatedText={translationState.translatedText}
|
||||
@@ -85,18 +68,19 @@ export function TranslatedPost({
|
||||
case 'error':
|
||||
return (
|
||||
<TranslationError
|
||||
translate={translate}
|
||||
clearTranslation={clearTranslation}
|
||||
message={translationState.message}
|
||||
initialTranslationParams={initialTranslationParams}
|
||||
postText={postText}
|
||||
primaryLanguage={langPrefs.primaryLanguage}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
needsTranslation && (
|
||||
<TranslationLink
|
||||
postText={postText}
|
||||
primaryLanguage={langPrefs.primaryLanguage}
|
||||
translate={translate}
|
||||
initialTranslationParams={initialTranslationParams}
|
||||
/>
|
||||
)
|
||||
)
|
||||
@@ -119,18 +103,30 @@ function TranslationLoading() {
|
||||
}
|
||||
|
||||
function TranslationLink({
|
||||
postText,
|
||||
primaryLanguage,
|
||||
translate,
|
||||
initialTranslationParams,
|
||||
}: {
|
||||
postText: string
|
||||
primaryLanguage: string
|
||||
translate: TranslationFunction
|
||||
initialTranslationParams: TranslationFunctionParams
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
|
||||
const handleTranslate = useCallback(() => {
|
||||
void translate(initialTranslationParams)
|
||||
}, [initialTranslationParams, translate])
|
||||
void translate({
|
||||
text: postText,
|
||||
targetLangCode: primaryLanguage,
|
||||
})
|
||||
|
||||
ax.metric('translate', {
|
||||
sourceLanguages: [], // todo: get from post maybe?
|
||||
targetLanguage: primaryLanguage,
|
||||
textLength: postText.length,
|
||||
})
|
||||
}, [ax, postText, primaryLanguage, translate])
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -162,24 +158,22 @@ function TranslationLink({
|
||||
}
|
||||
|
||||
function TranslationError({
|
||||
translate,
|
||||
clearTranslation,
|
||||
message,
|
||||
initialTranslationParams,
|
||||
postText,
|
||||
primaryLanguage,
|
||||
}: {
|
||||
translate: TranslationFunction
|
||||
clearTranslation: () => void
|
||||
message: string
|
||||
initialTranslationParams: TranslationFunctionParams
|
||||
postText: string
|
||||
primaryLanguage: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const translate = useGoogleTranslate()
|
||||
|
||||
const handleFallback = () => {
|
||||
void translate({
|
||||
...initialTranslationParams,
|
||||
forceGoogleTranslate: true,
|
||||
})
|
||||
void translate(postText, primaryLanguage)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -250,24 +244,24 @@ function TranslationError({
|
||||
function TranslationResult({
|
||||
clearTranslation,
|
||||
translate,
|
||||
postText,
|
||||
postTextStyle,
|
||||
resultSourceLanguage,
|
||||
sourceLanguage,
|
||||
translatedText,
|
||||
initialTranslationParams,
|
||||
}: {
|
||||
clearTranslation: () => void
|
||||
translate: TranslationFunction
|
||||
postText: string
|
||||
postTextStyle?: StyleProp<TextStyle>
|
||||
resultSourceLanguage: string | null
|
||||
sourceLanguage: string | null
|
||||
translatedText: string
|
||||
initialTranslationParams: TranslationFunctionParams
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const {i18n, t: l} = useLingui()
|
||||
|
||||
const langName = resultSourceLanguage
|
||||
? codeToLanguageName(resultSourceLanguage, i18n.locale)
|
||||
const langName = sourceLanguage
|
||||
? codeToLanguageName(sourceLanguage, i18n.locale)
|
||||
: undefined
|
||||
|
||||
const flattenedStyle = flatten(postTextStyle) ?? {}
|
||||
@@ -326,7 +320,7 @@ function TranslationResult({
|
||||
<Trans>Translated</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{resultSourceLanguage != null && (
|
||||
{sourceLanguage != null && (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
@@ -339,9 +333,9 @@ function TranslationResult({
|
||||
·{' '}
|
||||
</Text>
|
||||
<TranslationLanguageSelect
|
||||
resultSourceLanguage={resultSourceLanguage}
|
||||
sourceLanguage={sourceLanguage}
|
||||
translate={translate}
|
||||
initialTranslationParams={initialTranslationParams}
|
||||
postText={postText}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -365,12 +359,12 @@ function TranslationResult({
|
||||
|
||||
function TranslationLanguageSelect({
|
||||
translate,
|
||||
resultSourceLanguage,
|
||||
initialTranslationParams,
|
||||
postText,
|
||||
sourceLanguage,
|
||||
}: {
|
||||
translate: TranslationFunction
|
||||
resultSourceLanguage: string
|
||||
initialTranslationParams: TranslationFunctionParams
|
||||
postText: string
|
||||
sourceLanguage: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
@@ -386,8 +380,8 @@ function TranslationLanguageSelect({
|
||||
)
|
||||
.sort((a, b) => {
|
||||
// Prioritize sourceLanguage at the top
|
||||
if (a.code2 === resultSourceLanguage) return -1
|
||||
if (b.code2 === resultSourceLanguage) return 1
|
||||
if (a.code2 === sourceLanguage) return -1
|
||||
if (b.code2 === sourceLanguage) return 1
|
||||
// Localized sort
|
||||
return languageName(a, langPrefs.appLanguage).localeCompare(
|
||||
languageName(b, langPrefs.appLanguage),
|
||||
@@ -398,28 +392,25 @@ function TranslationLanguageSelect({
|
||||
label: languageName(l, langPrefs.appLanguage), // The viewer may not be familiar with the source language, so localize the name
|
||||
value: l.code2,
|
||||
})),
|
||||
[langPrefs, resultSourceLanguage],
|
||||
[langPrefs, sourceLanguage],
|
||||
)
|
||||
|
||||
const handleChangeTranslationLanguage = (sourceLangCode: string) => {
|
||||
ax.metric('translate:override', {
|
||||
os: Platform.OS,
|
||||
possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages,
|
||||
expectedSourceLanguage: sourceLangCode,
|
||||
expectedTargetLanguage: initialTranslationParams.expectedTargetLanguage,
|
||||
resultSourceLanguage,
|
||||
sourceLanguage: sourceLangCode,
|
||||
targetLanguage: langPrefs.primaryLanguage,
|
||||
})
|
||||
void translate({
|
||||
text: initialTranslationParams.text,
|
||||
expectedTargetLanguage: initialTranslationParams.expectedTargetLanguage,
|
||||
expectedSourceLanguage: sourceLangCode,
|
||||
possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages,
|
||||
text: postText,
|
||||
targetLangCode: langPrefs.primaryLanguage,
|
||||
sourceLangCode,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Select.Root
|
||||
value={resultSourceLanguage}
|
||||
value={sourceLanguage}
|
||||
onValueChange={handleChangeTranslationLanguage}>
|
||||
<Select.Trigger label={l`Change the source language`}>
|
||||
{({props}) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import type React from 'react'
|
||||
|
||||
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
type RichText as RichTextAPI,
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {useTranslate} from '#/lib/translation'
|
||||
import {getPostLanguageTags} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -57,6 +56,7 @@ import {
|
||||
} from '#/state/queries/threadgate'
|
||||
import {useRequireAuth, useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {
|
||||
@@ -93,9 +93,9 @@ import {
|
||||
useReportDialogControl,
|
||||
} from '#/components/moderation/ReportDialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_INTERNAL} from '#/env'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
let PostMenuItems = ({
|
||||
post,
|
||||
@@ -216,9 +216,7 @@ let PostMenuItems = ({
|
||||
},
|
||||
e => {
|
||||
logger.error('Failed to delete post', {message: e})
|
||||
Toast.show(l`Failed to delete post, please try again`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`Failed to delete post, please try again`, 'xmark')
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -248,38 +246,36 @@ let PostMenuItems = ({
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to toggle thread mute', {message: e})
|
||||
Toast.show(l`Failed to toggle thread mute, please try again`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`Failed to toggle thread mute, please try again`, 'xmark')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onToggleWordsAndTagsMute = () => {
|
||||
ax.metric('postMenu:openMuteWordsDialog', {
|
||||
uri: postUri,
|
||||
authorDid: postAuthor.did,
|
||||
logContext,
|
||||
feedDescriptor: feedFeedback.feedDescriptor,
|
||||
})
|
||||
mutedWordsDialogControl.open()
|
||||
}
|
||||
|
||||
const onCopyPostText = () => {
|
||||
const str = richTextToString(richText, true)
|
||||
|
||||
void Clipboard.setStringAsync(str)
|
||||
Toast.show(l`Copied to clipboard`, {
|
||||
type: 'success',
|
||||
})
|
||||
Toast.show(l`Copied to clipboard`, 'clipboard-check')
|
||||
}
|
||||
|
||||
const onPressTranslate = () => {
|
||||
void translate({
|
||||
text: record.text,
|
||||
expectedTargetLanguage: langPrefs.primaryLanguage,
|
||||
possibleSourceLanguages: getPostLanguageTags(post),
|
||||
targetLangCode: langPrefs.primaryLanguage,
|
||||
})
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
ax.metric('translate', {
|
||||
sourceLanguages: post.record.langs ?? [],
|
||||
targetLanguage: langPrefs.primaryLanguage,
|
||||
textLength: post.record.text.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onHidePost = () => {
|
||||
@@ -428,17 +424,8 @@ let PostMenuItems = ({
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to block account', {message: e})
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, 'xmark')
|
||||
}
|
||||
} finally {
|
||||
ax.metric('postMenu:blockAccount', {
|
||||
uri: postUri,
|
||||
authorDid: postAuthor.did,
|
||||
logContext,
|
||||
feedDescriptor: feedFeedback.feedDescriptor,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,17 +438,8 @@ let PostMenuItems = ({
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unmute account', {message: e})
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, 'xmark')
|
||||
}
|
||||
} finally {
|
||||
ax.metric('postMenu:unmuteAccount', {
|
||||
uri: postUri,
|
||||
authorDid: postAuthor.did,
|
||||
logContext,
|
||||
feedDescriptor: feedFeedback.feedDescriptor,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -471,17 +449,8 @@ let PostMenuItems = ({
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to mute account', {message: e})
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, 'xmark')
|
||||
}
|
||||
} finally {
|
||||
ax.metric('postMenu:muteAccount', {
|
||||
uri: postUri,
|
||||
authorDid: postAuthor.did,
|
||||
logContext,
|
||||
feedDescriptor: feedFeedback.feedDescriptor,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,7 +601,7 @@ let PostMenuItems = ({
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteWordsBtn"
|
||||
label={l`Mute words & tags`}
|
||||
onPress={onToggleWordsAndTagsMute}>
|
||||
onPress={() => mutedWordsDialogControl.open()}>
|
||||
<Menu.ItemText>{l`Mute words & tags`}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Filter} position="right" />
|
||||
</Menu.Item>
|
||||
@@ -816,14 +785,6 @@ let PostMenuItems = ({
|
||||
...post,
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
}}
|
||||
onAfterSubmit={() => {
|
||||
ax.metric('postMenu:reportPost', {
|
||||
uri: postUri,
|
||||
authorDid: postAuthor.did,
|
||||
logContext,
|
||||
feedDescriptor: feedFeedback.feedDescriptor,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<PostInteractionSettingsDialog
|
||||
control={postInteractionSettingsDialogControl}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {shareText, shareUrl} from '#/lib/sharing'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
@@ -21,7 +22,6 @@ import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/i
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_IOS} from '#/env'
|
||||
@@ -71,9 +71,7 @@ let ShareMenuItems = ({
|
||||
} else {
|
||||
await ExpoClipboard.setStringAsync(url)
|
||||
}
|
||||
Toast.show(_(msg`Copied to clipboard`), {
|
||||
type: 'success',
|
||||
})
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
onShareProp()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ import {
|
||||
ProgressGuideAction,
|
||||
useProgressGuideControls,
|
||||
} from '#/state/shell/progress-guide'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Reply as Bubble} from '#/components/icons/Reply'
|
||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
import * as Skele from '#/components/Skeleton'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {BookmarkButton} from './BookmarkButton'
|
||||
import {
|
||||
@@ -106,9 +106,7 @@ let PostControls = ({
|
||||
|
||||
const onPressToggleLike = async () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(l`Cannot interact with a blocked user`, {
|
||||
type: 'warning',
|
||||
})
|
||||
Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,9 +135,7 @@ let PostControls = ({
|
||||
|
||||
const onRepost = async () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(l`Cannot interact with a blocked user`, {
|
||||
type: 'warning',
|
||||
})
|
||||
Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -165,9 +161,7 @@ let PostControls = ({
|
||||
|
||||
const onQuote = () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(l`Cannot interact with a blocked user`, {
|
||||
type: 'warning',
|
||||
})
|
||||
Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {
|
||||
atoms as a,
|
||||
@@ -42,7 +43,6 @@ import {Link as InternalLink, type LinkProps} from '#/components/Link'
|
||||
import * as Pills from '#/components/Pills'
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type Metrics} from '#/analytics'
|
||||
import {useActorStatus} from '#/features/liveNow'
|
||||
@@ -145,7 +145,6 @@ export function Link({
|
||||
|
||||
return (
|
||||
<InternalLink
|
||||
testID={`profileCard-${profile.handle}-link`}
|
||||
label={l`View ${
|
||||
profile.displayName || sanitizeHandle(profile.handle)
|
||||
}’s profile`}
|
||||
@@ -505,9 +504,7 @@ export function FollowButtonInner({
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(l`An issue occurred, please try again.`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`An issue occurred, please try again.`, 'xmark')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,9 +524,7 @@ export function FollowButtonInner({
|
||||
} catch (e) {
|
||||
const err = e as Error
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(l`An issue occurred, please try again.`, {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(l`An issue occurred, please try again.`, 'xmark')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {memo, useCallback, useEffect, useMemo, useReducer, useRef} from 'react'
|
||||
import {useCallback} from 'react'
|
||||
import * as React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
@@ -61,7 +62,7 @@ const floatingMiddlewares = [
|
||||
|
||||
export function ProfileHoverCard(props: ProfileHoverCardProps) {
|
||||
const prefetchProfileQuery = usePrefetchProfileQuery()
|
||||
const prefetchedProfile = useRef(false)
|
||||
const prefetchedProfile = React.useRef(false)
|
||||
const onPointerMove = () => {
|
||||
if (!prefetchedProfile.current) {
|
||||
prefetchedProfile.current = true
|
||||
@@ -116,7 +117,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||
middleware: floatingMiddlewares,
|
||||
})
|
||||
|
||||
const [currentState, dispatch] = useReducer(
|
||||
const [currentState, dispatch] = React.useReducer(
|
||||
// Tip: console.log(state, action) when debugging.
|
||||
(state: State, action: Action): State => {
|
||||
// Pressing within a card should always hide it.
|
||||
@@ -262,7 +263,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||
{stage: 'hidden'},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (currentState.effect) {
|
||||
const effect = currentState.effect
|
||||
return effect()
|
||||
@@ -270,16 +271,16 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||
}, [currentState])
|
||||
|
||||
const prefetchProfileQuery = usePrefetchProfileQuery()
|
||||
const prefetchedProfile = useRef(false)
|
||||
const prefetchIfNeeded = useCallback(async () => {
|
||||
const prefetchedProfile = React.useRef(false)
|
||||
const prefetchIfNeeded = React.useCallback(async () => {
|
||||
if (!prefetchedProfile.current) {
|
||||
prefetchedProfile.current = true
|
||||
prefetchProfileQuery(props.did)
|
||||
}
|
||||
}, [prefetchProfileQuery, props.did])
|
||||
|
||||
const didFireHover = useRef(false)
|
||||
const onPointerMoveTarget = useCallback(() => {
|
||||
const didFireHover = React.useRef(false)
|
||||
const onPointerMoveTarget = React.useCallback(() => {
|
||||
prefetchIfNeeded()
|
||||
// Conceptually we want something like onPointerEnter,
|
||||
// but we want to ignore entering only due to scrolling.
|
||||
@@ -290,20 +291,20 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
|
||||
}
|
||||
}, [prefetchIfNeeded])
|
||||
|
||||
const onPointerLeaveTarget = useCallback(() => {
|
||||
const onPointerLeaveTarget = React.useCallback(() => {
|
||||
didFireHover.current = false
|
||||
dispatch('unhovered-target')
|
||||
}, [])
|
||||
|
||||
const onPointerEnterCard = useCallback(() => {
|
||||
const onPointerEnterCard = React.useCallback(() => {
|
||||
dispatch('hovered-card')
|
||||
}, [])
|
||||
|
||||
const onPointerLeaveCard = useCallback(() => {
|
||||
const onPointerLeaveCard = React.useCallback(() => {
|
||||
dispatch('unhovered-card')
|
||||
}, [])
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
const onPress = React.useCallback(() => {
|
||||
dispatch('pressed')
|
||||
}, [])
|
||||
|
||||
@@ -411,7 +412,7 @@ let Card = ({
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Card = memo(Card)
|
||||
Card = React.memo(Card)
|
||||
|
||||
function Inner({
|
||||
profile,
|
||||
@@ -425,7 +426,7 @@ function Inner({
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const moderation = useMemo(
|
||||
const moderation = React.useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
[profile, moderationOpts],
|
||||
)
|
||||
@@ -453,7 +454,7 @@ function Inner({
|
||||
did: profile.did,
|
||||
handle: profile.handle,
|
||||
})
|
||||
const isMe = useMemo(
|
||||
const isMe = React.useMemo(
|
||||
() => currentAccount?.did === profile.did,
|
||||
[currentAccount, profile],
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {TextInput, View, type ViewToken} from 'react-native'
|
||||
import {type ModerationOpts} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
@@ -62,14 +64,14 @@ export function FollowDialog({
|
||||
showArrow?: boolean
|
||||
}) {
|
||||
const ax = useAnalytics()
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const control = Dialog.useDialogControl()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={l`Find people to follow`}
|
||||
label={_(msg`Find people to follow`)}
|
||||
onPress={() => {
|
||||
control.open()
|
||||
ax.metric('progressGuide:followDialog:open', {})
|
||||
@@ -110,7 +112,7 @@ let lastSelectedInterest = ''
|
||||
let lastSearchText = ''
|
||||
|
||||
function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
@@ -180,7 +182,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
_items.push({
|
||||
type: 'empty',
|
||||
key: 'empty',
|
||||
message: l`We're having network issues, try again`,
|
||||
message: _(msg`We're having network issues, try again`),
|
||||
})
|
||||
} else {
|
||||
const seen = new Set<string>()
|
||||
@@ -206,12 +208,12 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
!_items.length &&
|
||||
!isSearchResultsError
|
||||
) {
|
||||
_items.push({type: 'empty', key: 'empty', message: l`No results`})
|
||||
_items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
|
||||
}
|
||||
|
||||
return _items
|
||||
}, [
|
||||
l,
|
||||
_,
|
||||
suggestions,
|
||||
suggestionsError,
|
||||
isFetchingSuggestions,
|
||||
@@ -224,9 +226,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
isSearchResultsError,
|
||||
])
|
||||
|
||||
const isGuide = Boolean(guide)
|
||||
const recIdForLogging = hasSearchText ? undefined : suggestions?.recId
|
||||
|
||||
const renderItems = useCallback(
|
||||
({item, index}: {item: Item; index: number}) => {
|
||||
switch (item.type) {
|
||||
@@ -236,9 +235,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
profile={item.profile}
|
||||
moderationOpts={moderationOpts!}
|
||||
noBorder={index === 0}
|
||||
position={index}
|
||||
recId={recIdForLogging}
|
||||
isGuide={isGuide}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -252,7 +248,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
[moderationOpts, recIdForLogging, isGuide],
|
||||
[moderationOpts],
|
||||
)
|
||||
|
||||
// Track seen profiles
|
||||
@@ -273,8 +269,8 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
i => i.type === 'profile' && i.profile.did === item.profile.did,
|
||||
)
|
||||
ax.metric('suggestedUser:seen', {
|
||||
logContext: isGuide ? 'ProgressGuide' : 'SeeMoreSuggestedUsers',
|
||||
recId: recIdForLogging,
|
||||
logContext: 'ProgressGuide',
|
||||
recId: hasSearchText ? undefined : suggestions?.recId,
|
||||
position: position !== -1 ? position : 0,
|
||||
suggestedDid: item.profile.did,
|
||||
category: selectedInterestRef.current,
|
||||
@@ -408,7 +404,7 @@ let Header = ({
|
||||
Header = memo(Header)
|
||||
|
||||
function HeaderTop({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const control = Dialog.useDialogContext()
|
||||
return (
|
||||
@@ -442,7 +438,7 @@ function HeaderTop({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
)}
|
||||
{IS_WEB ? (
|
||||
<Button
|
||||
label={l`Close`}
|
||||
label={_(msg`Close`)}
|
||||
size="small"
|
||||
shape="round"
|
||||
variant={IS_WEB ? 'ghost' : 'solid'}
|
||||
@@ -478,18 +474,22 @@ let Tab = ({
|
||||
onLayout: (index: number, x: number, width: number) => void
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const label = active
|
||||
? l({
|
||||
message: `Search for "${interestsDisplayName}" (active)`,
|
||||
comment:
|
||||
'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is currently selected.',
|
||||
})
|
||||
: l({
|
||||
message: `Search for "${interestsDisplayName}"`,
|
||||
comment:
|
||||
'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is not currently active and can be selected.',
|
||||
})
|
||||
? _(
|
||||
msg({
|
||||
message: `Search for "${interestsDisplayName}" (active)`,
|
||||
comment:
|
||||
'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is currently selected.',
|
||||
}),
|
||||
)
|
||||
: _(
|
||||
msg({
|
||||
message: `Search for "${interestsDisplayName}"`,
|
||||
comment:
|
||||
'Accessibility label for a tab that searches for accounts in a category (e.g. Art, Video Games, Sports, etc.) that are suggested for the user to follow. The tab is not currently active and can be selected.',
|
||||
}),
|
||||
)
|
||||
return (
|
||||
<View
|
||||
key={interest}
|
||||
@@ -532,25 +532,16 @@ let FollowProfileCard = ({
|
||||
profile,
|
||||
moderationOpts,
|
||||
noBorder,
|
||||
position,
|
||||
recId,
|
||||
isGuide,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
noBorder?: boolean
|
||||
position: number
|
||||
recId?: string
|
||||
isGuide: boolean
|
||||
}): React.ReactNode => {
|
||||
return (
|
||||
<FollowProfileCardInner
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
noBorder={noBorder}
|
||||
position={position}
|
||||
recId={recId}
|
||||
isGuide={isGuide}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -561,21 +552,14 @@ function FollowProfileCardInner({
|
||||
moderationOpts,
|
||||
onFollow,
|
||||
noBorder,
|
||||
position,
|
||||
recId,
|
||||
isGuide,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onFollow?: () => void
|
||||
noBorder?: boolean
|
||||
position: number
|
||||
recId?: string
|
||||
isGuide: boolean
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
return (
|
||||
<ProfileCard.Link
|
||||
profile={profile}
|
||||
@@ -604,19 +588,7 @@ function FollowProfileCardInner({
|
||||
moderationOpts={moderationOpts}
|
||||
logContext="PostOnboardingFindFollows"
|
||||
shape="round"
|
||||
onPress={() => {
|
||||
ax.metric('suggestedUser:follow', {
|
||||
logContext: isGuide
|
||||
? 'ProgressGuide'
|
||||
: 'SeeMoreSuggestedUsers',
|
||||
location: 'Card',
|
||||
recId,
|
||||
position,
|
||||
suggestedDid: profile.did,
|
||||
category: null,
|
||||
})
|
||||
onFollow?.()
|
||||
}}
|
||||
onPress={onFollow}
|
||||
colorInverted
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
@@ -660,7 +632,7 @@ function SearchInput({
|
||||
defaultValue: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
@@ -680,9 +652,10 @@ function SearchInput({
|
||||
size="md"
|
||||
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
placeholder={l`Search by name or interest`}
|
||||
placeholder={_(msg`Search by name or interest`)}
|
||||
defaultValue={defaultValue}
|
||||
onChangeText={onChangeText}
|
||||
onFocus={onFocus}
|
||||
@@ -701,8 +674,8 @@ function SearchInput({
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
accessibilityLabel={l`Search profiles`}
|
||||
accessibilityHint={l`Searches for profiles`}
|
||||
accessibilityLabel={_(msg`Search profiles`)}
|
||||
accessibilityHint={_(msg`Searches for profiles`)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import Animated, {
|
||||
SlideInLeft,
|
||||
SlideInRight,
|
||||
} from 'react-native-reanimated'
|
||||
import type React from 'react'
|
||||
|
||||
import {IS_WEB} from '#/env'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {type ListRenderItemInfo, View} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
|
||||
@@ -19,9 +19,9 @@ interface ProfilesListProps {
|
||||
scrollElRef: ListRef
|
||||
}
|
||||
|
||||
export const FeedsList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
function FeedsListImpl({feeds, headerHeight, scrollElRef}, ref) {
|
||||
const [initialHeaderHeight] = useState(headerHeight)
|
||||
const [initialHeaderHeight] = React.useState(headerHeight)
|
||||
const bottomBarOffset = useBottomBarOffset(20)
|
||||
const t = useTheme()
|
||||
|
||||
@@ -32,7 +32,7 @@ export const FeedsList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
})
|
||||
}, [scrollElRef, headerHeight])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {forwardRef, useCallback, useImperativeHandle} from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -17,7 +17,7 @@ interface ProfilesListProps {
|
||||
scrollElRef: ListRef
|
||||
}
|
||||
|
||||
export const PostsList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
|
||||
const feed: FeedDescriptor = `list|${listUri}`
|
||||
const {_} = useLingui()
|
||||
@@ -29,7 +29,7 @@ export const PostsList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
})
|
||||
}, [scrollElRef, headerHeight])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {type ListRenderItemInfo, View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
@@ -37,7 +37,7 @@ interface ProfilesListProps {
|
||||
scrollElRef: ListRef
|
||||
}
|
||||
|
||||
export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
function ProfilesListImpl(
|
||||
{listUri, moderationOpts, headerHeight, scrollElRef},
|
||||
ref,
|
||||
@@ -48,7 +48,7 @@ export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
const {currentAccount} = useSession()
|
||||
const {data, refetch, isError} = useAllListMembersQuery(listUri)
|
||||
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
|
||||
// The server returns these sorted by descending creation date, so we want to invert
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
|
||||
})
|
||||
}, [scrollElRef, headerHeight])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {isValidElement} from 'react'
|
||||
import {type ReactNode} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
import {toast as sonner, Toaster} from 'sonner-native'
|
||||
@@ -25,7 +26,7 @@ export function ToastOutlet() {
|
||||
return <Toaster pauseWhenPageIsHidden gap={a.gap_sm.gap} />
|
||||
}
|
||||
|
||||
export function Outer({children}: {children: React.ReactNode}) {
|
||||
export function Outer({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<View style={[a.px_xl, a.w_full]}>
|
||||
<BaseOuter>{children}</BaseOuter>
|
||||
@@ -42,7 +43,7 @@ export const api = sonner
|
||||
* Our base toast API, using the `Toast` export of this file.
|
||||
*/
|
||||
export function show(
|
||||
content: React.ReactNode,
|
||||
content: ReactNode,
|
||||
{type = 'default', ...options}: BaseToastOptions = {},
|
||||
) {
|
||||
const id = nanoid()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {isValidElement} from 'react'
|
||||
import {type ReactNode} from 'react'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
import {toast as sonner, Toaster} from 'sonner'
|
||||
|
||||
@@ -40,7 +41,7 @@ export const api = sonner
|
||||
* Our base toast API, using the `Toast` export of this file.
|
||||
*/
|
||||
export function show(
|
||||
content: React.ReactNode,
|
||||
content: ReactNode,
|
||||
{type = 'default', ...options}: BaseToastOptions = {},
|
||||
) {
|
||||
const id = nanoid()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {atoms as a, type TextStyleProp, useAlf, useTheme, web} from '#/alf'
|
||||
import {atoms, useAlf, useTheme, web} from '#/alf'
|
||||
import {
|
||||
childHasEmoji,
|
||||
normalizeTextStyles,
|
||||
@@ -22,24 +22,15 @@ export function Text({
|
||||
selectable,
|
||||
title,
|
||||
dataSet,
|
||||
numberOfLines,
|
||||
...rest
|
||||
}: TextProps) {
|
||||
const {fonts, flags} = useAlf()
|
||||
const t = useTheme()
|
||||
const s = normalizeTextStyles(
|
||||
[
|
||||
a.text_sm,
|
||||
t.atoms.text,
|
||||
web(numberOfLines === 1 && numberOfLinesClippingFix),
|
||||
style,
|
||||
],
|
||||
{
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags,
|
||||
},
|
||||
)
|
||||
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, style], {
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags,
|
||||
})
|
||||
|
||||
if (__DEV__) {
|
||||
if (!emoji && childHasEmoji(children)) {
|
||||
@@ -53,7 +44,6 @@ export function Text({
|
||||
const shared = {
|
||||
uiTextView: true,
|
||||
selectable,
|
||||
numberOfLines,
|
||||
style: s,
|
||||
dataSet: Object.assign({tooltip: title}, dataSet || {}),
|
||||
...rest,
|
||||
@@ -92,26 +82,10 @@ export function P({style, ...rest}: TextProps) {
|
||||
role: 'paragraph',
|
||||
}) || {}
|
||||
return (
|
||||
<Text {...attr} {...rest} style={[a.text_md, a.leading_relaxed, style]} />
|
||||
<Text
|
||||
{...attr}
|
||||
{...rest}
|
||||
style={[atoms.text_md, atoms.leading_relaxed, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* HACKFIX: React Native Web applies `overflow: hidden` to
|
||||
* text when using the `numberOfLines` prop, which causes it to clip
|
||||
* ascenders/descenders. It only needs to be doing this for the X axis,
|
||||
* so override the style with `overflowX: 'hidden'`.
|
||||
* Note this only works for `numberOfLines={1}` -sfn
|
||||
*
|
||||
* @see https://github.com/necolas/react-native-web/pull/2836
|
||||
*/
|
||||
const numberOfLinesClippingFix = {
|
||||
overflowY: 'visible',
|
||||
overflowX: 'clip',
|
||||
// mimic browser default behavior of `min-width: 0` on `overflow: hidden`
|
||||
// elements to allow text to shrink smaller than its intrinsic width when
|
||||
// necessary
|
||||
minWidth: 0,
|
||||
// this is neater and supports vertical writing modes, but it's only baseline newly available
|
||||
// overflowInline: 'clip',
|
||||
} satisfies React.CSSProperties as TextStyleProp
|
||||
|
||||
@@ -21,6 +21,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, platform, useTheme, web} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {
|
||||
@@ -33,7 +34,6 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_WEB} from '#/env'
|
||||
@@ -139,9 +139,7 @@ function DialogInner({
|
||||
_(
|
||||
msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`,
|
||||
),
|
||||
{
|
||||
type: 'success',
|
||||
},
|
||||
'check',
|
||||
)
|
||||
|
||||
// filter out the subscription
|
||||
@@ -171,14 +169,10 @@ function DialogInner({
|
||||
_(
|
||||
msg`You'll start receiving notifications for ${sanitizeHandle(profile.handle, '@')}!`,
|
||||
),
|
||||
{
|
||||
type: 'success',
|
||||
},
|
||||
'check',
|
||||
)
|
||||
} else {
|
||||
Toast.show(_(msg`Changes saved`), {
|
||||
type: 'success',
|
||||
})
|
||||
Toast.show(_(msg`Changes saved`), 'check')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,12 +8,12 @@ import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints, web} from '#/alf'
|
||||
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {logger} from '#/ageAssurance'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
@@ -70,9 +70,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
|
||||
logger.error('AgeAssuranceAppealDialog failed', {safeMessage: err})
|
||||
Toast.show(
|
||||
_(msg`Age assurance inquiry failed to send, please try again.`),
|
||||
{
|
||||
type: 'error',
|
||||
},
|
||||
'xmark',
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {type PropsWithChildren} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
@@ -624,7 +625,7 @@ function MutedWordRow({
|
||||
)
|
||||
}
|
||||
|
||||
function TargetToggle({children}: React.PropsWithChildren<{}>) {
|
||||
function TargetToggle({children}: PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
const ctx = Toggle.useItemContext()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
usePostThreadContext,
|
||||
} from '#/state/queries/usePostThread'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -49,7 +50,6 @@ import {
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_IOS} from '#/env'
|
||||
@@ -240,9 +240,7 @@ export function PostInteractionSettingsDialogControlledInner(
|
||||
_(
|
||||
msg`There was an issue. Please check your internet connection and try again.`,
|
||||
),
|
||||
{
|
||||
type: 'error',
|
||||
},
|
||||
'xmark',
|
||||
)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
useListMembershipRemoveMutation,
|
||||
} from '#/state/queries/list-memberships'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, native, platform, useTheme} from '#/alf'
|
||||
import {AvatarStack} from '#/components/AvatarStack'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -261,8 +260,6 @@ function StarterPackItem({
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const isSelf = subject?.did === currentAccount?.did
|
||||
|
||||
const starterPack = starterPackWithMembership.starterPack
|
||||
const isInPack = !!starterPackWithMembership.listItem
|
||||
@@ -376,17 +373,11 @@ function StarterPackItem({
|
||||
label={isInPack ? _(msg`Remove`) : _(msg`Add`)}
|
||||
color={isInPack ? 'secondary' : 'primary_subtle'}
|
||||
size="tiny"
|
||||
disabled={isPending || isSelf}
|
||||
disabled={isPending}
|
||||
onPress={handleToggleMembership}>
|
||||
{isPending && <ButtonIcon icon={Loader} />}
|
||||
<ButtonText>
|
||||
{isSelf ? (
|
||||
<Trans>Owner</Trans>
|
||||
) : isInPack ? (
|
||||
<Trans>Remove</Trans>
|
||||
) : (
|
||||
<Trans>Add</Trans>
|
||||
)}
|
||||
{isInPack ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '#/state/queries/list'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -24,7 +25,6 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_WEB} from '#/env'
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
useListMembershipAddMutation,
|
||||
useListMembershipRemoveMutation,
|
||||
} from '#/state/queries/list-memberships'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
} from '#/components/dialogs/SearchablePeopleList'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function ListAddRemoveUsersDialog({
|
||||
@@ -113,10 +113,7 @@ function UserResult({
|
||||
Toast.show(_(msg`Added to list`))
|
||||
onChange?.('add', profile)
|
||||
},
|
||||
onError: e =>
|
||||
Toast.show(cleanError(e), {
|
||||
type: 'error',
|
||||
}),
|
||||
onError: e => Toast.show(cleanError(e), 'xmark'),
|
||||
})
|
||||
const {mutate: listMembershipRemove, isPending: isRemovingPending} =
|
||||
useListMembershipRemoveMutation({
|
||||
@@ -124,10 +121,7 @@ function UserResult({
|
||||
Toast.show(_(msg`Removed from list`))
|
||||
onChange?.('remove', profile)
|
||||
},
|
||||
onError: e =>
|
||||
Toast.show(cleanError(e), {
|
||||
type: 'error',
|
||||
}),
|
||||
onError: e => Toast.show(cleanError(e), 'xmark'),
|
||||
})
|
||||
const isMutating = isAddingPending || isRemovingPending
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
|
||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
|
||||
import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {EmojiReactionPicker} from './EmojiReactionPicker'
|
||||
import {hasReachedReactionLimit} from './util'
|
||||
|
||||
@@ -60,11 +60,11 @@ export function ActionsWrapper({
|
||||
.catch(() => Toast.show(_(msg`Failed to remove emoji reaction`)))
|
||||
} else {
|
||||
if (hasReachedReactionLimit(message, currentAccount?.did)) return
|
||||
convo.addReaction(message.id, emoji).catch(() =>
|
||||
Toast.show(_(msg`Failed to add emoji reaction`), {
|
||||
type: 'error',
|
||||
}),
|
||||
)
|
||||
convo
|
||||
.addReaction(message.id, emoji)
|
||||
.catch(() =>
|
||||
Toast.show(_(msg`Failed to add emoji reaction`), 'xmark'),
|
||||
)
|
||||
}
|
||||
},
|
||||
[_, convo, message, currentAccount?.did],
|
||||
|
||||
@@ -5,6 +5,7 @@ import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import type React from 'react'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -13,12 +14,12 @@ import {
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileQuery,
|
||||
} from '#/state/queries/profile'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
|
||||
@@ -135,9 +136,7 @@ function DoneStep({
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not leave chat`), {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(_(msg`Could not leave chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
@@ -163,9 +162,7 @@ function DoneStep({
|
||||
leaveConvo()
|
||||
}
|
||||
if (toastMsg) {
|
||||
Toast.show(toastMsg, {
|
||||
type: 'success',
|
||||
})
|
||||
Toast.show(toastMsg, 'check')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import {useCallback, useEffect} from 'react'
|
||||
import {type ScrollView, View} from 'react-native'
|
||||
import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
type Props = {
|
||||
testID?: string
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
onRemove?: (did: string) => void
|
||||
}
|
||||
|
||||
export function ChatProfileTabs({testID, profiles, onRemove}: Props) {
|
||||
const t = useTheme()
|
||||
const scrollElRef = useAnimatedRef<ScrollView>()
|
||||
const contentSize = useSharedValue(0)
|
||||
const scrollX = useSharedValue(0)
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// Scroll to the end of the list when `profiles` changes.
|
||||
scrollElRef.current?.scrollToEnd({animated: true})
|
||||
})
|
||||
}, [profiles, scrollElRef])
|
||||
|
||||
return (
|
||||
<View testID={testID} accessibilityRole="list" style={[t.atoms.bg]}>
|
||||
<DraggableScrollView
|
||||
ref={scrollElRef}
|
||||
testID={`${testID}-selector`}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onScroll={e => {
|
||||
scrollX.set(Math.round(e.nativeEvent.contentOffset.x))
|
||||
}}>
|
||||
<Animated.View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.flex_grow,
|
||||
a.gap_sm,
|
||||
a.align_center,
|
||||
a.justify_start,
|
||||
]}
|
||||
onLayout={e => {
|
||||
contentSize.set(e.nativeEvent.layout.width)
|
||||
}}>
|
||||
{profiles.map((profile, index) => (
|
||||
<Tab
|
||||
key={profile.did}
|
||||
testID={testID}
|
||||
index={index}
|
||||
profile={profile}
|
||||
total={profiles.length}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</Animated.View>
|
||||
</DraggableScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Tab({
|
||||
testID,
|
||||
index,
|
||||
profile,
|
||||
total,
|
||||
onRemove,
|
||||
}: {
|
||||
testID?: string
|
||||
index: number
|
||||
profile: bsky.profile.AnyProfileView
|
||||
total: number
|
||||
onRemove?: (did: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const moderation = moderateProfile(profile, moderationOpts!)
|
||||
const displayName = sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
|
||||
const onPressItem = useCallback(
|
||||
(did: string) => {
|
||||
onRemove?.(did)
|
||||
},
|
||||
[onRemove],
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
testID={`${testID}-selector-${profile.did}`}
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.border,
|
||||
a.justify_center,
|
||||
a.rounded_lg,
|
||||
a.pl_xs,
|
||||
a.pr_sm,
|
||||
a.py_xs,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
index === 0 ? a.ml_lg : index === total - 1 ? a.mr_lg : null,
|
||||
]}>
|
||||
{moderationOpts ? (
|
||||
<>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
size={24}
|
||||
disabledPreview
|
||||
/>
|
||||
<View style={[a.flex_row, a.align_center, a.max_w_full, a.ml_xs]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.font_normal,
|
||||
a.leading_snug,
|
||||
a.self_start,
|
||||
a.flex_shrink,
|
||||
t.atoms.text,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ProfileCard.AvatarPlaceholder size={24} />
|
||||
<ProfileCard.NamePlaceholder />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
hitSlop={HITSLOP_10}
|
||||
label={l`Remove ${displayName} from group chat`}
|
||||
style={[a.ml_xs]}
|
||||
onPress={() => onPressItem(profile.did)}>
|
||||
{({hovered, pressed, focused}) => (
|
||||
<XIcon
|
||||
size="sm"
|
||||
style={[
|
||||
hovered || pressed || focused
|
||||
? t.atoms.text
|
||||
: t.atoms.text_contrast_high,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {memo, useCallback} from 'react'
|
||||
import {useCallback} from 'react'
|
||||
import * as React from 'react'
|
||||
import {Keyboard, View} from 'react-native'
|
||||
import {type ChatBskyConvoDefs, type ModerationCause} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
unstableCacheProfileView,
|
||||
useProfileBlockMutationQueue,
|
||||
} from '#/state/queries/profile'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {type ViewStyleProp} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
@@ -39,7 +41,6 @@ import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
let ConvoMenu = ({
|
||||
@@ -159,7 +160,7 @@ let ConvoMenu = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
ConvoMenu = memo(ConvoMenu)
|
||||
ConvoMenu = React.memo(ConvoMenu)
|
||||
|
||||
function MenuContent({
|
||||
convo: initialConvo,
|
||||
@@ -205,15 +206,13 @@ function MenuContent({
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not mute chat`), {
|
||||
type: 'error',
|
||||
})
|
||||
Toast.show(_(msg`Could not mute chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||
|
||||
const toggleBlock = useCallback(() => {
|
||||
const toggleBlock = React.useCallback(() => {
|
||||
if (listBlocks.length) {
|
||||
blockedByListControl.open()
|
||||
return
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {memo} from 'react'
|
||||
import {type ReactNode} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -28,7 +29,7 @@ const longDateFormatterWithYear = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => {
|
||||
let DateDivider = ({date: dateStr}: {date: string}): ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user