Merge main into native-video-compress
Drops the parallel composer:video:probe* events added in 127d0288 in favor
of main's video:upload:* funnel from #10991. Reshapes the new module's
compressVideo to set passthroughReason on every skip branch so it conforms
to the VideoCompressSkipReason contract, including a new 'below-thresholds'
reason for the shouldCompress passthrough. Probe data wiring into the
funnel comes in a follow-up commit.
This commit is contained in:
+1
-1
@@ -33,7 +33,7 @@ RUN mkdir --parents $NVM_DIR && \
|
||||
RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
nvm install $NODE_VERSION && \
|
||||
nvm use $NODE_VERSION && \
|
||||
npm install --global pnpm@11.7.0 && \
|
||||
npm install --global pnpm@11.9.0 && \
|
||||
pnpm install --frozen-lockfile && \
|
||||
cd bskyembed && pnpm install --frozen-lockfile && cd .. && \
|
||||
pnpm intl:build && \
|
||||
|
||||
@@ -946,14 +946,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/lib/hooks/useNotificationHandler.ts": {
|
||||
"@typescript-eslint/no-floating-promises": {
|
||||
"count": 16
|
||||
},
|
||||
"@typescript-eslint/require-await": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/lib/hooks/useOTAUpdates.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -2183,14 +2175,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/view/com/modals/Modal.tsx": {
|
||||
"@typescript-eslint/no-misused-promises": {
|
||||
"count": 1
|
||||
},
|
||||
"@typescript-eslint/require-await": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/view/com/notifications/NotificationFeed.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
"devEngines": {
|
||||
"packageManager": {
|
||||
"name": "pnpm",
|
||||
"version": "11.7.0",
|
||||
"version": "11.9.0",
|
||||
"onFail": "warn"
|
||||
},
|
||||
"runtime": {
|
||||
@@ -94,7 +94,7 @@
|
||||
"update-actions": "pnpm dlx actions-up --min-age 7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.20.15",
|
||||
"@atproto/api": "0.20.20",
|
||||
"@atproto/syntax": "0.6.1",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
|
||||
Generated
+78
-69
@@ -7,52 +7,52 @@ importers:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
'@pnpm/exe':
|
||||
specifier: 11.7.0
|
||||
version: 11.7.0
|
||||
specifier: 11.9.0
|
||||
version: 11.9.0
|
||||
pnpm:
|
||||
specifier: 11.7.0
|
||||
version: 11.7.0
|
||||
specifier: 11.9.0
|
||||
version: 11.9.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe@11.7.0':
|
||||
resolution: {integrity: sha512-3CujpSSp2PIDE0pwu7mWSdjhdDqaZa7OppVooECWWaNEoA/z66s9FZts1MhDO+2yq1XER4gBHh84DVbFN/r1rA==}
|
||||
'@pnpm/exe@11.9.0':
|
||||
resolution: {integrity: sha512-pPPOpR79qW3nsNhlyDIdfstli4Bi78mk8r22ySxpFRwMbO8KXSjGrVzGmJBsVX39NnJTh7/WADj527nZhG9H9g==}
|
||||
hasBin: true
|
||||
|
||||
'@pnpm/linux-arm64@11.7.0':
|
||||
resolution: {integrity: sha512-ANTX2SlMO+d2y/4bYQhHCwHPX7gSSADJ5+pMUIiDFzIsybnFFaJdZboaFfq9NOxCbETcnDxqZ95Rz3+NHx1JIw==}
|
||||
'@pnpm/linux-arm64@11.9.0':
|
||||
resolution: {integrity: sha512-XYmY2qadHauBA3QaHi2R7fI6kt5Flje0WHz9MVrbH0kVH/XLpfOLnwPeE1+EX6K/nDa2CBvzp35VjYCNGFJa9A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@pnpm/linux-x64@11.7.0':
|
||||
resolution: {integrity: sha512-fr75tqixXoS8cnA81HQIomjOGEPnsOsd3xCDL5pMNY5raOXbKurtgRV+RjATvjxlJxSLIVFKegABlxAiB7q72A==}
|
||||
'@pnpm/linux-x64@11.9.0':
|
||||
resolution: {integrity: sha512-fl7W5imnSmmgXIqMQFZ/rPaVvk9OkKF8/anqHZE3XEDfWcn3BlWGndyOEas/JN7u2BXWYjs63DJZ3rnG6WOhLA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@pnpm/linuxstatic-arm64@11.7.0':
|
||||
resolution: {integrity: sha512-Q++pgzvXkGeqnVRl26/uqmpMGdttQus0rGyL3XIfYGLCi8ZfajYUaCKdZID2MH7+CNOuugWDdFDup3r7BR7Rfg==}
|
||||
'@pnpm/linuxstatic-arm64@11.9.0':
|
||||
resolution: {integrity: sha512-fif8xbnzVEAIlvaU4yIgWKXeXYb4Kj6WMEl/KvM2x1Rp3AKAjBW/53SGzxO4cZP9doAqUzOIpMRGFVbHGeMDRw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/linuxstatic-x64@11.7.0':
|
||||
resolution: {integrity: sha512-z1+exW6ocU/rmOvJnmU3FUBJYaryCUqFoaXN6KZW5BqTj7BPJb7HJcAyXRlirNZMJlEiUY5rXbfStGPQJhGsVA==}
|
||||
'@pnpm/linuxstatic-x64@11.9.0':
|
||||
resolution: {integrity: sha512-9dKu3QdShqOpnWrjW9owARpIJeP0ul8UgIIbBUv8VDGEYibt+g49zQsNaD7SdMD3WeRSjExPQ1zIhplzr5cwvQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/macos-arm64@11.7.0':
|
||||
resolution: {integrity: sha512-gD34/k3JT5oab4BYaqrUor3e4VdwXvkfLNlEI+lvDtX1MHT+2Nauc9p9NsQnpn1zE8blQEflzbF8wAUQ6Dmvkw==}
|
||||
'@pnpm/macos-arm64@11.9.0':
|
||||
resolution: {integrity: sha512-MWzBTgeI5p3odjdVltYvFXaSWAjF2Xk5YaxiP/u2RmW8N6PHsLyIyj37Ds992CrXgFM8fO1RpvsEirozhsD6KA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/win-arm64@11.7.0':
|
||||
resolution: {integrity: sha512-hRTcDmm2j7KoRwbqNo0rUAu9A1kVJN98Eob7H09U0uPJbEMax85JGOmERkT3Lf6HjVJuFNBvfaJ3OTI3HmlVGg==}
|
||||
'@pnpm/win-arm64@11.9.0':
|
||||
resolution: {integrity: sha512-u/QxEcbKJZxC1t3zUYCZiHzu7TaZ/iXc6EGZoQjkeVT0LXmEVR6ypcK3ByjhIqbxQ3HGX75gnpIpR+VjRjubfw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/win-x64@11.7.0':
|
||||
resolution: {integrity: sha512-r/1NuKY7Z+6ZXVIzVrUEMj6TTEBdR63geV4Rlm8HKEhgQTdxyIsJoqV3FJGqoyzbRaScObqAwRfMaK9dskPddQ==}
|
||||
'@pnpm/win-x64@11.9.0':
|
||||
resolution: {integrity: sha512-HqJVHmZG5UKfLi38AjMl2azVmq87TlWRhwSW+f4q9LLaZeAkFyIZ7LY/pN6mh4VlH9yWj90C+tsb1yKiy7OKnw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -116,45 +116,45 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pnpm@11.7.0:
|
||||
resolution: {integrity: sha512-GcyFLBIMcSV2DyRD7mvgyltA+fUFmN4aCaHxd1A+AQ5Xwjx3ZG4B52HeWb+HT7IqM5jDOrlpH8E+uUa28PTWIA==}
|
||||
pnpm@11.9.0:
|
||||
resolution: {integrity: sha512-vWgtXQP+Ul73yf1ngMaITR51asTJyf4AxTh4KCQxDc+Q493E9Tg18G3669UIXkGFXgvLs7YN4qxburieUDbwOw==}
|
||||
engines: {node: '>=22.13'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe@11.7.0':
|
||||
'@pnpm/exe@11.9.0':
|
||||
dependencies:
|
||||
'@reflink/reflink': 0.1.19
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
'@pnpm/linux-arm64': 11.7.0
|
||||
'@pnpm/linux-x64': 11.7.0
|
||||
'@pnpm/linuxstatic-arm64': 11.7.0
|
||||
'@pnpm/linuxstatic-x64': 11.7.0
|
||||
'@pnpm/macos-arm64': 11.7.0
|
||||
'@pnpm/win-arm64': 11.7.0
|
||||
'@pnpm/win-x64': 11.7.0
|
||||
'@pnpm/linux-arm64': 11.9.0
|
||||
'@pnpm/linux-x64': 11.9.0
|
||||
'@pnpm/linuxstatic-arm64': 11.9.0
|
||||
'@pnpm/linuxstatic-x64': 11.9.0
|
||||
'@pnpm/macos-arm64': 11.9.0
|
||||
'@pnpm/win-arm64': 11.9.0
|
||||
'@pnpm/win-x64': 11.9.0
|
||||
|
||||
'@pnpm/linux-arm64@11.7.0':
|
||||
'@pnpm/linux-arm64@11.9.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/linux-x64@11.7.0':
|
||||
'@pnpm/linux-x64@11.9.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/linuxstatic-arm64@11.7.0':
|
||||
'@pnpm/linuxstatic-arm64@11.9.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/linuxstatic-x64@11.7.0':
|
||||
'@pnpm/linuxstatic-x64@11.9.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/macos-arm64@11.7.0':
|
||||
'@pnpm/macos-arm64@11.9.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/win-arm64@11.7.0':
|
||||
'@pnpm/win-arm64@11.9.0':
|
||||
optional: true
|
||||
|
||||
'@pnpm/win-x64@11.7.0':
|
||||
'@pnpm/win-x64@11.9.0':
|
||||
optional: true
|
||||
|
||||
'@reflink/reflink-darwin-arm64@0.1.19':
|
||||
@@ -194,7 +194,7 @@ snapshots:
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
pnpm@11.7.0: {}
|
||||
pnpm@11.9.0: {}
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
@@ -241,8 +241,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@atproto/api':
|
||||
specifier: 0.20.15
|
||||
version: 0.20.15
|
||||
specifier: 0.20.20
|
||||
version: 0.20.20
|
||||
'@atproto/syntax':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
@@ -876,32 +876,36 @@ packages:
|
||||
graphql:
|
||||
optional: true
|
||||
|
||||
'@atproto/api@0.20.15':
|
||||
resolution: {integrity: sha512-b9TuVNY9iWIaRXAeKegNCqRsK9tSpB68DE/j/ytVTxEMK+/m43B0DycJND9tnhRiNIY+i7MhwLqNHPak9YaDJg==}
|
||||
'@atproto/api@0.20.20':
|
||||
resolution: {integrity: sha512-+yqxLCvMu36xRxwc8/x0mhptZBC+/cbfPwdzeOtlBUeVi4ZB/5RWg2L4lWWdhBSiQBZw0AgaPg/2zsx4lDFgjg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/common-web@0.5.0':
|
||||
resolution: {integrity: sha512-ReWnkuZdDU/74/I47gaI26uxQjHmpq4edp41NnZZQ5vIIKGb7Ei6pZHzDTUD9JURo109SKrPx9RMP2IQm0fOKA==}
|
||||
'@atproto/common-web@0.5.1':
|
||||
resolution: {integrity: sha512-nru02gLyzjMQn4ZFgms9ry3kIAKwMaCAvjeFhB9mU6h1ABiSho7aRDbGvnU50CC88TROXuZlgRiEkBjnuiHEtA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/lex-data@0.1.1':
|
||||
resolution: {integrity: sha512-/xza8nU/YhtzhETnHL3QKKofaJ28/0NCzhT7LaYoUkm8EgypWp5ykEtmW52yLhQM2JF6fVa25g1soQmNTGqtSg==}
|
||||
'@atproto/lex-data@0.1.2':
|
||||
resolution: {integrity: sha512-NZ4iZvNaqTM6pz+9VRDyEjtozrrf2EDHySNi3Xa8PCzS8gRMCvsnnsvM7r264v4eSqNIDhBB8fr9hfWCHMspXg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/lex-json@0.1.0':
|
||||
resolution: {integrity: sha512-oWUrRMwFyWpmi/5k1Se3xBTbP06XdxBS5iFuUz9LmqItaPXwrWRD87a9ldPvINQ/A2/mn7J6/qug8sDVlhD+vQ==}
|
||||
'@atproto/lex-json@0.1.1':
|
||||
resolution: {integrity: sha512-FC/NsKHm8TDzWikvf/268T3mMAh6+f31yfCApWC3SvrhWc9c06cSYPp7qzpXcdV0OdB+I5dqHScuTB5OC7HrXg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/lexicon@0.7.1':
|
||||
resolution: {integrity: sha512-voNfNED5KUxn3vpo7N5DMRblBDfWf7kSfdKhJFC1RrLCxg38YbBzzURNVQJ32bp13Oot8kYfyXBWxTgtKLvw8w==}
|
||||
'@atproto/lexicon@0.7.2':
|
||||
resolution: {integrity: sha512-LVZcTr5+9Qh0okZnNmfRiIDPfbL/6rRxf4goiQxPxwDzaeUDhn4M209i1drmiitbCO1qRLJA2hHF54yNA75Cig==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/syntax@0.6.1':
|
||||
resolution: {integrity: sha512-kA4dQDoMPpWCH8N0Q4KoSq024u5MkVfDVa8DdhyLjGA72z/khbOf1jXKPv7NIL2oEc9aj7geKELdvqyf4ogopA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/xrpc@0.8.0':
|
||||
resolution: {integrity: sha512-NJy02bIKrWlE2NQkRV1kT0Cj0ixbuxlF/MejBdo4cPWAa9v3oZexvAcjjb0zaOYeABkaU14iyIhvn2G4e/oLpw==}
|
||||
'@atproto/syntax@0.6.2':
|
||||
resolution: {integrity: sha512-h3njTNFl/jv5kTbDqfamQGOJfGbulqLDuAiLbasKwVdBhFyQanpRLa6vE2WqTwv1XEFWLYNMH0KCVsYwT2+aww==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@atproto/xrpc@0.8.1':
|
||||
resolution: {integrity: sha512-sKopRG3an6LN6NfHnRNIEX1fBx3CRtxbNFWQxyse2f86wMcTuXM+/+olN4lVXgFQgv4AWvKTxRXWyuIF2G4YxQ==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@babel/code-frame@7.10.4':
|
||||
@@ -9472,40 +9476,40 @@ snapshots:
|
||||
|
||||
'@0no-co/graphql.web@1.2.0': {}
|
||||
|
||||
'@atproto/api@0.20.15':
|
||||
'@atproto/api@0.20.20':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.0
|
||||
'@atproto/lexicon': 0.7.1
|
||||
'@atproto/syntax': 0.6.1
|
||||
'@atproto/xrpc': 0.8.0
|
||||
'@atproto/common-web': 0.5.1
|
||||
'@atproto/lexicon': 0.7.2
|
||||
'@atproto/syntax': 0.6.2
|
||||
'@atproto/xrpc': 0.8.1
|
||||
await-lock: 3.0.0
|
||||
multiformats: 13.4.2
|
||||
tlds: 1.261.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/common-web@0.5.0':
|
||||
'@atproto/common-web@0.5.1':
|
||||
dependencies:
|
||||
'@atproto/lex-data': 0.1.1
|
||||
'@atproto/lex-json': 0.1.0
|
||||
'@atproto/syntax': 0.6.1
|
||||
'@atproto/lex-data': 0.1.2
|
||||
'@atproto/lex-json': 0.1.1
|
||||
'@atproto/syntax': 0.6.2
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/lex-data@0.1.1':
|
||||
'@atproto/lex-data@0.1.2':
|
||||
dependencies:
|
||||
multiformats: 13.4.2
|
||||
tslib: 2.8.1
|
||||
uint8arrays: 5.1.1
|
||||
unicode-segmenter: 0.14.5
|
||||
|
||||
'@atproto/lex-json@0.1.0':
|
||||
'@atproto/lex-json@0.1.1':
|
||||
dependencies:
|
||||
'@atproto/lex-data': 0.1.1
|
||||
'@atproto/lex-data': 0.1.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@atproto/lexicon@0.7.1':
|
||||
'@atproto/lexicon@0.7.2':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.5.0
|
||||
'@atproto/syntax': 0.6.1
|
||||
'@atproto/common-web': 0.5.1
|
||||
'@atproto/syntax': 0.6.2
|
||||
multiformats: 13.4.2
|
||||
zod: 3.25.76
|
||||
|
||||
@@ -9514,9 +9518,14 @@ snapshots:
|
||||
iso-datestring-validator: 2.2.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@atproto/xrpc@0.8.0':
|
||||
'@atproto/syntax@0.6.2':
|
||||
dependencies:
|
||||
'@atproto/lexicon': 0.7.1
|
||||
iso-datestring-validator: 2.2.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@atproto/xrpc@0.8.1':
|
||||
dependencies:
|
||||
'@atproto/lexicon': 0.7.2
|
||||
zod: 3.25.76
|
||||
|
||||
'@babel/code-frame@7.10.4':
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import {type Platform} from 'react-native'
|
||||
|
||||
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
||||
import {type VideoCompressSkipReason} from '#/lib/media/video/types'
|
||||
import {type NotificationType} from '#/state/queries/notifications/types'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
||||
@@ -252,23 +253,6 @@ export type Events = {
|
||||
'composer:image:edit': {
|
||||
platform: Platform['OS']
|
||||
}
|
||||
'composer:video:probe': {
|
||||
mimeType: string
|
||||
codec: string
|
||||
width: number
|
||||
height: number
|
||||
duration: number
|
||||
bitrate: number
|
||||
fileSize: number
|
||||
hasAudio: boolean
|
||||
frameRate: number
|
||||
rotation: number
|
||||
isHDR: boolean
|
||||
wouldCompress: boolean
|
||||
}
|
||||
'composer:video:probeFailed': {
|
||||
safeMessage: string
|
||||
}
|
||||
'composerPrompt:press': {}
|
||||
'composerPrompt:camera:press': {}
|
||||
'composerPrompt:gallery:press': {}
|
||||
@@ -1348,4 +1332,99 @@ export type Events = {
|
||||
'invite:followersPromo:press': {}
|
||||
// user dismissed the empty-followers promo banner
|
||||
'invite:followersPromo:dismiss': {}
|
||||
|
||||
// === Video upload funnel (Frontend Spec section D) ===
|
||||
// Every event carries uploadId (client-generated UUID, ties one upload
|
||||
// session end-to-end) + engine (compression engine id, e.g.
|
||||
// native:react-native-compressor@1.13.0). jobId is added once the server
|
||||
// returns it. Sizes / codecs / dimensions / timings only - never content.
|
||||
'video:upload:picked': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
sourceMimeType?: string
|
||||
sourceBytes?: number
|
||||
sourceDurationMs?: number
|
||||
sourceWidth?: number
|
||||
sourceHeight?: number
|
||||
}
|
||||
'video:upload:compressStarted': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
sourceBytes?: number
|
||||
}
|
||||
'video:upload:compressCompleted': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
bytesIn?: number
|
||||
bytesOut: number
|
||||
outputMimeType: string
|
||||
elapsedMs: number
|
||||
}
|
||||
'video:upload:compressSkipped': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
skipReason: VideoCompressSkipReason
|
||||
bytes: number
|
||||
mimeType: string
|
||||
elapsedMs: number
|
||||
}
|
||||
'video:upload:compressFailed': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
errorClass: string
|
||||
elapsedMs: number
|
||||
}
|
||||
'video:upload:uploadStarted': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
bytes: number
|
||||
}
|
||||
'video:upload:uploadCompleted': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
jobId: string
|
||||
bytes: number
|
||||
elapsedMs: number
|
||||
throughputBytesPerSec: number
|
||||
}
|
||||
'video:upload:uploadFailed': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
bytes: number
|
||||
errorClass: string
|
||||
elapsedMs: number
|
||||
}
|
||||
'video:upload:processingStarted': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
jobId: string
|
||||
}
|
||||
'video:upload:processingCompleted': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
jobId: string
|
||||
elapsedMs: number
|
||||
}
|
||||
'video:upload:processingFailed': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
jobId: string
|
||||
errorClass: string
|
||||
elapsedMs: number
|
||||
}
|
||||
'video:upload:published': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
jobId: string
|
||||
// wall-clock from picked to published
|
||||
totalElapsedMs: number
|
||||
}
|
||||
// The event that measures the actual problem: users giving up mid-wait.
|
||||
'video:upload:abandoned': {
|
||||
uploadId: string
|
||||
engine: string
|
||||
phase: 'compress' | 'upload' | 'processing'
|
||||
jobId?: string
|
||||
elapsedInPhaseMs: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import {Pressable} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const positionStyles = {
|
||||
'top-left': {
|
||||
top: a.p_xs.padding,
|
||||
left: a.p_xs.padding,
|
||||
},
|
||||
'top-right': {
|
||||
top: a.p_xs.padding,
|
||||
right: a.p_xs.padding,
|
||||
},
|
||||
'bottom-left': {
|
||||
bottom: a.p_xs.padding,
|
||||
left: a.p_xs.padding,
|
||||
},
|
||||
'bottom-right': {
|
||||
bottom: a.p_xs.padding,
|
||||
right: a.p_xs.padding,
|
||||
},
|
||||
}
|
||||
|
||||
export function AltBadgeWithDialog({
|
||||
text,
|
||||
position,
|
||||
}: {
|
||||
text: string
|
||||
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const large = useLargeAltBadgeEnabled()
|
||||
const control = Prompt.usePromptControl()
|
||||
|
||||
const pos = position ? [a.absolute, positionStyles[position]] : {}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
testID="altBadgeWithDialogButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={l`Show alt text`}
|
||||
accessibilityHint=""
|
||||
hitSlop={HITSLOP_20}
|
||||
onPress={control.open}
|
||||
style={s => [
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
a.z_10,
|
||||
t.atoms.bg_contrast_25,
|
||||
large && {
|
||||
padding: 6,
|
||||
},
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
pos,
|
||||
s.hovered || s.pressed
|
||||
? [
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
]}>
|
||||
<Text
|
||||
accessible={false}
|
||||
style={[a.font_bold, large ? a.text_xs : {fontSize: 8}]}>
|
||||
<Trans>ALT</Trans>
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Prompt.Outer control={control}>
|
||||
<Prompt.Content>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Alt Text</Trans>
|
||||
</Prompt.TitleText>
|
||||
<Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText>
|
||||
</Prompt.Content>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Cancel cta={l`Close`} />
|
||||
</Prompt.Actions>
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -7,9 +7,10 @@ import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {APP_LANGUAGES} from '#/locale/languages'
|
||||
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||
import {resetPostsFeedQueries} from '#/state/queries/post-feed'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useTheme, web} from '#/alf'
|
||||
import * as Select from '#/components/Select'
|
||||
import {Button} from './Button'
|
||||
import {Button, ButtonIcon} from './Button'
|
||||
import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from './icons/Globe'
|
||||
|
||||
export function AppLanguageDropdown() {
|
||||
const t = useTheme()
|
||||
@@ -57,6 +58,7 @@ export function AppLanguageDropdown() {
|
||||
native: [a.gap_xs],
|
||||
}),
|
||||
]}>
|
||||
<ButtonIcon icon={EarthIcon} size="md" />
|
||||
<Select.ValueText
|
||||
placeholder={_(msg`Select an app language`)}
|
||||
style={[t.atoms.text_contrast_medium]}
|
||||
@@ -68,7 +70,7 @@ export function AppLanguageDropdown() {
|
||||
<Select.Content
|
||||
label={_(msg`Select language`)}
|
||||
renderItem={({label, value}) => (
|
||||
<Select.Item value={value} label={label}>
|
||||
<Select.Item value={value} label={label} style={web([a.pointer])}>
|
||||
<Select.ItemIndicator />
|
||||
<Select.ItemText>{label}</Select.ItemText>
|
||||
</Select.Item>
|
||||
|
||||
@@ -66,19 +66,7 @@ export function GifEmbed({
|
||||
a.overflow_hidden,
|
||||
{backgroundColor: t.palette.black},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
/*
|
||||
* Aspect ratio was being clipped weirdly on web -esb
|
||||
*/
|
||||
{
|
||||
top: -2,
|
||||
bottom: -2,
|
||||
left: -2,
|
||||
right: -2,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.absolute, a.inset_0]}>
|
||||
<MediaInsetBorder />
|
||||
<GifPresentationControls
|
||||
onPress={onPress}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
|
||||
@@ -29,6 +24,7 @@ export function GifPresentationControls({
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const largeBadge = useLargeAltBadgeEnabled()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,74 +60,41 @@ export function GifPresentationControls({
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.gifBadgeContainer}>
|
||||
<Text style={[{color: 'white'}, a.font_bold, a.text_xs]}>
|
||||
<Trans>GIF</Trans>
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.flex_row,
|
||||
a.z_10,
|
||||
{
|
||||
bottom: a.p_xs.padding,
|
||||
right: a.p_xs.padding,
|
||||
gap: 3,
|
||||
},
|
||||
largeBadge && {
|
||||
gap: 4,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
accessible={false}
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
a.z_10,
|
||||
t.atoms.bg_contrast_25,
|
||||
largeBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}>
|
||||
<Text style={[a.font_bold, largeBadge ? a.text_xs : {fontSize: 8}]}>
|
||||
<Trans>GIF</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{altText && <AltBadgeWithDialog text={altText} />}
|
||||
</View>
|
||||
{altText && <AltBadge text={altText} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AltBadge({text}: {text: string}) {
|
||||
const control = Prompt.usePromptControl()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
testID="altTextButton"
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Show alt text`)}
|
||||
accessibilityHint=""
|
||||
hitSlop={HITSLOP_20}
|
||||
onPress={control.open}
|
||||
style={styles.altBadgeContainer}>
|
||||
<Text
|
||||
style={[{color: 'white'}, a.font_bold, a.text_xs]}
|
||||
accessible={false}>
|
||||
<Trans>ALT</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Prompt.Outer control={control}>
|
||||
<Prompt.Content>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Alt Text</Trans>
|
||||
</Prompt.TitleText>
|
||||
<Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText>
|
||||
</Prompt.Content>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action
|
||||
onPress={() => control.close()}
|
||||
cta={_(msg`Close`)}
|
||||
color="secondary"
|
||||
/>
|
||||
</Prompt.Actions>
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gifBadgeContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 3,
|
||||
position: 'absolute',
|
||||
left: 6,
|
||||
bottom: 6,
|
||||
zIndex: 2,
|
||||
},
|
||||
altBadgeContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 3,
|
||||
position: 'absolute',
|
||||
right: 6,
|
||||
bottom: 6,
|
||||
zIndex: 2,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
|
||||
@@ -98,19 +99,24 @@ export function VideoEmbedInnerNative({
|
||||
altText={embed.alt}
|
||||
/>
|
||||
) : (
|
||||
<VideoPresentationControls
|
||||
enterFullscreen={() => {
|
||||
videoRef.current?.enterFullscreen(true)
|
||||
}}
|
||||
toggleMuted={() => {
|
||||
videoRef.current?.toggleMuted()
|
||||
}}
|
||||
togglePlayback={() => {
|
||||
videoRef.current?.togglePlayback()
|
||||
}}
|
||||
isPlaying={isPlaying}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
<>
|
||||
<VideoPresentationControls
|
||||
enterFullscreen={() => {
|
||||
videoRef.current?.enterFullscreen(true)
|
||||
}}
|
||||
toggleMuted={() => {
|
||||
videoRef.current?.toggleMuted()
|
||||
}}
|
||||
togglePlayback={() => {
|
||||
videoRef.current?.togglePlayback()
|
||||
}}
|
||||
isPlaying={isPlaying}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
{embed.alt && (
|
||||
<AltBadgeWithDialog text={embed.alt} position="top-right" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<MediaInsetBorder />
|
||||
<KeepAwake enabled={isPlaying} />
|
||||
|
||||
@@ -7,6 +7,8 @@ import type * as HlsTypes from 'hls.js'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import * as BandwidthEstimate from './bandwidth-estimate'
|
||||
import {Controls} from './web-controls/VideoControls'
|
||||
|
||||
@@ -30,6 +32,8 @@ export function VideoEmbedInnerWeb({
|
||||
const [hlsLoading, setHlsLoading] = useState(false)
|
||||
const figId = useId()
|
||||
const {_} = useLingui()
|
||||
const [isFullscreen] = useFullscreen(containerRef)
|
||||
const isGif = embed.presentation === 'gif'
|
||||
|
||||
// send error up to error boundary
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
@@ -77,6 +81,9 @@ export function VideoEmbedInnerWeb({
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
{!isFullscreen && !isGif && embed.alt && (
|
||||
<AltBadgeWithDialog text={embed.alt} position="top-right" />
|
||||
)}
|
||||
<Controls
|
||||
videoRef={videoRef}
|
||||
hlsRef={hlsRef}
|
||||
@@ -88,7 +95,7 @@ export function VideoEmbedInnerWeb({
|
||||
onScreen={onScreen}
|
||||
fullscreenRef={containerRef}
|
||||
hasSubtitleTrack={hasSubtitleTrack}
|
||||
isGif={embed.presentation === 'gif'}
|
||||
isGif={isGif}
|
||||
altText={embed.alt}
|
||||
updateCuePositions={updateCuePositions}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
|
||||
import {
|
||||
isChatPreferenceName,
|
||||
type NotificationSettingsPreferenceName,
|
||||
useChatNotificationSettingsQuery,
|
||||
useNotificationSettingsQuery,
|
||||
} from '#/state/queries/notifications/settings'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {PreferenceControls} from '#/screens/Settings/NotificationSettings/components/PreferenceControls'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
@@ -15,8 +19,8 @@ import {IS_NATIVE} from '#/env'
|
||||
|
||||
type NotificationSettingsDialogProps = {
|
||||
control: Dialog.DialogControlProps
|
||||
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
|
||||
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
|
||||
name: NotificationSettingsPreferenceName
|
||||
syncOthers?: NotificationSettingsPreferenceName[]
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
titleText: React.ReactNode
|
||||
subtitleText: React.ReactNode
|
||||
@@ -55,7 +59,11 @@ function NotificationSettingsDialogInner({
|
||||
}: Omit<NotificationSettingsDialogProps, 'icon'>) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {data: preferences, isError} = useNotificationSettingsQuery()
|
||||
const isChat = isChatPreferenceName(name)
|
||||
const appQuery = useNotificationSettingsQuery({enabled: !isChat})
|
||||
const chatQuery = useChatNotificationSettingsQuery({enabled: isChat})
|
||||
const isError = isChat ? chatQuery.isError : appQuery.isError
|
||||
const preference = isChat ? chatQuery.data?.[name] : appQuery.data?.[name]
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,7 +89,7 @@ function NotificationSettingsDialogInner({
|
||||
<PreferenceControls
|
||||
name={name}
|
||||
syncOthers={syncOthers}
|
||||
preference={preferences?.[name]}
|
||||
preference={preference}
|
||||
allowDisableInApp={allowDisableInApp}
|
||||
/>
|
||||
)}
|
||||
@@ -89,7 +97,7 @@ function NotificationSettingsDialogInner({
|
||||
<Dialog.Close />
|
||||
{IS_NATIVE && (
|
||||
<Button
|
||||
color="secondary"
|
||||
color="primary"
|
||||
size="large"
|
||||
label={l`Close dialog`}
|
||||
onPress={() => control.close()}
|
||||
|
||||
@@ -44,12 +44,13 @@ import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
||||
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
|
||||
import {useSession} from '#/state/session'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, native, platform, useTheme, utils} from '#/alf'
|
||||
import {atoms as a, native, platform, tokens, useTheme, utils} from '#/alf'
|
||||
import {isOnlyEmoji} from '#/alf/typography'
|
||||
import {Button} from '#/components/Button'
|
||||
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
||||
import {useMessageDialogs} from '#/components/dms/MessageOverlays'
|
||||
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
||||
import {useReplyPreviewText} from '#/components/dms/replyPreview'
|
||||
import {ArrowCornerDownRight_Stroke2_Corner3_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
@@ -834,6 +835,7 @@ function ReplyQuote({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const getReplyPreviewText = useReplyPreviewText()
|
||||
|
||||
const senderProfile = useMaybeProfileShadow(
|
||||
relatedProfiles.get(replyTo.sender.did),
|
||||
@@ -857,22 +859,15 @@ function ReplyQuote({
|
||||
let text: string
|
||||
let subtle = false
|
||||
if (isBlocked) {
|
||||
text = l`Blocked message hidden`
|
||||
text = l({
|
||||
message: '(blocked message hidden)',
|
||||
comment: 'A reply summary in chat',
|
||||
})
|
||||
subtle = true
|
||||
} else if (ChatBskyConvoDefs.isMessageView(replyTo)) {
|
||||
text = replyTo.text
|
||||
if (!text.trim()) {
|
||||
subtle = true
|
||||
if (ChatBskyEmbedJoinLink.isView(replyTo.embed)) {
|
||||
text = l`(chat invite link)`
|
||||
} else if (AppBskyEmbedRecord.isView(replyTo.embed)) {
|
||||
text = l`(contains embedded content)`
|
||||
} else {
|
||||
text = l`No text`
|
||||
}
|
||||
}
|
||||
;({text, subtle} = getReplyPreviewText(replyTo))
|
||||
} else {
|
||||
text = l`Deleted message`
|
||||
text = l({message: '(deleted message)', comment: 'A reply summary in chat'})
|
||||
subtle = true
|
||||
}
|
||||
|
||||
@@ -888,6 +883,8 @@ function ReplyQuote({
|
||||
a.mb_xs,
|
||||
a.rounded_md,
|
||||
a.p_sm,
|
||||
// The padding above is a little loose, so we tighten it up here.
|
||||
{paddingTop: tokens.space.sm - 2},
|
||||
a.flex_col,
|
||||
a.align_start,
|
||||
a.border,
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedRecord,
|
||||
type ChatBskyConvoDefs,
|
||||
ChatBskyEmbedJoinLink,
|
||||
ChatBskyGroupDefs,
|
||||
} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {BSKY_APP_HOST, toShortUrl} from '#/lib/strings/url-helpers'
|
||||
|
||||
/**
|
||||
* Describes the embed of a quoted message that has no text of its own, so the
|
||||
* reply preview can show what was shared instead of a generic placeholder. For
|
||||
* a link card we surface the URI directly; otherwise we classify the quoted
|
||||
* record (post/feed/list/etc.) so the caller can render a translated label.
|
||||
*/
|
||||
type ReplyEmbedSummary =
|
||||
| {type: 'external'; uri: string}
|
||||
| {type: 'post'}
|
||||
| {type: 'unknown'}
|
||||
|
||||
function summarizeReplyEmbed(
|
||||
embed: ChatBskyConvoDefs.MessageView['embed'],
|
||||
): ReplyEmbedSummary {
|
||||
if (!AppBskyEmbedRecord.isView(embed)) return {type: 'unknown'}
|
||||
const {record} = embed
|
||||
if (AppBskyEmbedRecord.isViewRecord(record)) {
|
||||
const inner = record.embeds?.[0]
|
||||
if (AppBskyEmbedExternal.isView(inner)) {
|
||||
return {type: 'external', uri: inner.external.uri}
|
||||
}
|
||||
return {type: 'post'}
|
||||
}
|
||||
return {type: 'unknown'}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatter that computes the preview text for a message being quoted
|
||||
* in a reply, shared between the staged-reply composer and the sent reply
|
||||
* bubble so the two stay in sync. When the message has its own text we use it
|
||||
* verbatim; otherwise we summarize the embed.
|
||||
*
|
||||
* `subtle` indicates the text is a placeholder (not real message content), so
|
||||
* callers can render it in a muted/italic style.
|
||||
*/
|
||||
export function useReplyPreviewText(): (
|
||||
message: ChatBskyConvoDefs.MessageView,
|
||||
) => {text: string; subtle: boolean} {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (message: ChatBskyConvoDefs.MessageView) => {
|
||||
const text = message.text
|
||||
if (text.trim()) {
|
||||
return {text, subtle: false}
|
||||
}
|
||||
|
||||
if (ChatBskyEmbedJoinLink.isView(message.embed)) {
|
||||
const {joinLinkPreview} = message.embed
|
||||
if (ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) {
|
||||
return {
|
||||
text: `${BSKY_APP_HOST}/chat/${joinLinkPreview.code}`,
|
||||
subtle: true,
|
||||
}
|
||||
}
|
||||
if (ChatBskyGroupDefs.isDisabledJoinLinkPreviewView(joinLinkPreview)) {
|
||||
return {
|
||||
text: l({
|
||||
message: '(disabled chat invite link)',
|
||||
comment: 'A reply summary in chat',
|
||||
}),
|
||||
subtle: true,
|
||||
}
|
||||
}
|
||||
if (ChatBskyGroupDefs.isInvalidJoinLinkPreviewView(joinLinkPreview)) {
|
||||
return {
|
||||
text: l({
|
||||
message: '(invalid chat invite link)',
|
||||
comment: 'A reply summary in chat',
|
||||
}),
|
||||
subtle: true,
|
||||
}
|
||||
}
|
||||
return {
|
||||
text: l({
|
||||
message: '(chat invite link)',
|
||||
comment: 'A reply summary in chat',
|
||||
}),
|
||||
subtle: true,
|
||||
}
|
||||
}
|
||||
|
||||
const summary = summarizeReplyEmbed(message.embed)
|
||||
switch (summary.type) {
|
||||
case 'external':
|
||||
return {text: toShortUrl(summary.uri), subtle: true}
|
||||
case 'post':
|
||||
return {
|
||||
text: l({
|
||||
message: '(quoted post)',
|
||||
comment: 'A reply summary in chat',
|
||||
}),
|
||||
subtle: true,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
text: l({message: '(no text)', comment: 'A reply summary in chat'}),
|
||||
subtle: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyFeedDefs, type ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
import {Plural} from '@lingui/react/macro'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a} from '#/alf'
|
||||
@@ -60,23 +60,17 @@ export function LabelsOnMe({
|
||||
<ButtonIcon position="left" icon={CircleInfo} />
|
||||
<ButtonText style={[a.leading_snug]}>
|
||||
{type === 'account' ? (
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this account
|
||||
</Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# account label"
|
||||
other="# account labels"
|
||||
/>
|
||||
) : (
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this content
|
||||
</Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# content label"
|
||||
other="# content labels"
|
||||
/>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {useEffect} from 'react'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {CommonActions, useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -108,6 +107,7 @@ let lastHandledNotificationDateDedupe = 0
|
||||
|
||||
export function useNotificationsHandler() {
|
||||
const ax = useAnalytics()
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
const logger = ax.logger.useChild(ax.logger.Context.Notifications)
|
||||
const queryClient = useQueryClient()
|
||||
const {currentAccount, accounts} = useSession()
|
||||
@@ -116,7 +116,7 @@ export function useNotificationsHandler() {
|
||||
const {currentConvoId} = useCurrentConvoId()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const closeAllActiveElements = useCloseAllActiveElements()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
// On Android, we cannot control which sound is used for a notification on Android
|
||||
// 28 or higher. Instead, we have to configure a notification channel ahead of time
|
||||
@@ -129,14 +129,12 @@ export function useNotificationsHandler() {
|
||||
// NOTE: I don't think that it will retroactively move them into the group
|
||||
// if the channels already exist. no big deal imo -sfn
|
||||
const CHAT_GROUP = 'chat'
|
||||
Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, {
|
||||
name: _(msg`Chat`),
|
||||
description: _(
|
||||
msg`You can choose whether chat notifications have sound in the chat settings within the app`,
|
||||
),
|
||||
void Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, {
|
||||
name: l`Chat`,
|
||||
description: l`You can choose whether chat notifications have sound in the chat settings within the app`,
|
||||
})
|
||||
Notifications.setNotificationChannelAsync('chat-messages', {
|
||||
name: _(msg`Chat messages - sound`),
|
||||
void Notifications.setNotificationChannelAsync('chat-messages', {
|
||||
name: l`Chat messages - sound`,
|
||||
groupId: CHAT_GROUP,
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
sound: 'dm.mp3',
|
||||
@@ -144,8 +142,8 @@ export function useNotificationsHandler() {
|
||||
vibrationPattern: [250],
|
||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||
})
|
||||
Notifications.setNotificationChannelAsync('chat-messages-muted', {
|
||||
name: _(msg`Chat messages - silent`),
|
||||
void Notifications.setNotificationChannelAsync('chat-messages-muted', {
|
||||
name: l`Chat messages - silent`,
|
||||
groupId: CHAT_GROUP,
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
sound: null,
|
||||
@@ -154,70 +152,70 @@ export function useNotificationsHandler() {
|
||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||
})
|
||||
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'like' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Likes`),
|
||||
name: l`Likes`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Reposts`),
|
||||
name: l`Reposts`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'reply' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Replies`),
|
||||
name: l`Replies`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'mention' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Mentions`),
|
||||
name: l`Mentions`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'quote' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Quotes`),
|
||||
name: l`Quotes`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'follow' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`New followers`),
|
||||
name: l`New followers`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'like-via-repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Likes of your reposts`),
|
||||
name: l`Likes of your reposts`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'repost-via-repost' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Reposts of your reposts`),
|
||||
name: l`Reposts of your reposts`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
void Notifications.setNotificationChannelAsync(
|
||||
'subscribed-post' satisfies NotificationReason,
|
||||
{
|
||||
name: _(msg`Activity from others`),
|
||||
name: l`Activity from others`,
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
}, [_])
|
||||
}, [l])
|
||||
|
||||
useEffect(() => {
|
||||
const handleNotification = (payload?: NotificationPayload) => {
|
||||
@@ -237,7 +235,7 @@ export function useNotificationsHandler() {
|
||||
|
||||
const account = accounts.find(a => a.did === payload.recipientDid)
|
||||
if (account) {
|
||||
onPressSwitchAccount(account, 'Notification')
|
||||
void onPressSwitchAccount(account, 'Notification')
|
||||
} else {
|
||||
setShowLoggedOut(true)
|
||||
}
|
||||
@@ -305,10 +303,10 @@ export function useNotificationsHandler() {
|
||||
}
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async e => {
|
||||
handleNotification: e => {
|
||||
const payload = getNotificationPayload(e)
|
||||
|
||||
if (!payload) return DEFAULT_HANDLER_OPTIONS
|
||||
if (!payload) return Promise.resolve(DEFAULT_HANDLER_OPTIONS)
|
||||
|
||||
logger.debug('useNotificationsHandler: incoming', {e, payload})
|
||||
|
||||
@@ -323,17 +321,17 @@ export function useNotificationsHandler() {
|
||||
payload.reason === 'chat-removed-from-group' ||
|
||||
payload.reason === 'chat-join-request-rejected' ||
|
||||
payload.convoId !== currentConvoId
|
||||
return {
|
||||
return Promise.resolve({
|
||||
shouldShowList: shouldAlert,
|
||||
shouldShowBanner: shouldAlert,
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
} satisfies Notifications.NotificationBehavior
|
||||
} satisfies Notifications.NotificationBehavior)
|
||||
}
|
||||
|
||||
// Any notification other than a chat message should invalidate the unread page
|
||||
invalidateCachedUnreadPage()
|
||||
return DEFAULT_HANDLER_OPTIONS
|
||||
return Promise.resolve(DEFAULT_HANDLER_OPTIONS)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -363,14 +361,14 @@ export function useNotificationsHandler() {
|
||||
})
|
||||
|
||||
invalidateCachedUnreadPage()
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
void truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
|
||||
if (
|
||||
payload.reason === 'mention' ||
|
||||
payload.reason === 'quote' ||
|
||||
payload.reason === 'reply'
|
||||
) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
void truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
|
||||
logger.debug('Notifications: handleNotification', {
|
||||
@@ -379,7 +377,7 @@ export function useNotificationsHandler() {
|
||||
})
|
||||
|
||||
handleNotification(payload)
|
||||
Notifications.dismissAllNotificationsAsync()
|
||||
void Notifications.dismissAllNotificationsAsync()
|
||||
// Also clear the native `lastResponse` cache. Otherwise a subsequent
|
||||
// `getLastNotificationResponse()` (e.g. on an account-switch remount,
|
||||
// which re-runs `handlePushNotificationEntry`) would replay this
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
VIDEO_MAX_SIZE,
|
||||
} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
compress,
|
||||
probe,
|
||||
type VideoMetadata,
|
||||
} from '../../../../modules/expo-bluesky-video-compress'
|
||||
import {compress, probe} from '../../../../modules/expo-bluesky-video-compress'
|
||||
import {
|
||||
COMPRESSION_MAX_DIMENSION,
|
||||
COMPRESSION_PASSTHROUGH_BITRATE,
|
||||
@@ -23,12 +19,15 @@ export async function compressVideo(
|
||||
opts?: {
|
||||
signal?: AbortSignal
|
||||
onProgress?: (progress: number) => void
|
||||
onProbe?: (metadata: VideoMetadata, wouldCompress: boolean) => void
|
||||
onProbeFailed?: (error: unknown) => void
|
||||
},
|
||||
): Promise<CompressedVideo> {
|
||||
if (file.mimeType === 'image/gif') {
|
||||
return {uri: file.uri, size: file.fileSize ?? -1, mimeType: 'image/gif'}
|
||||
return {
|
||||
uri: file.uri,
|
||||
size: file.fileSize ?? -1,
|
||||
mimeType: 'image/gif',
|
||||
passthroughReason: 'gif',
|
||||
}
|
||||
}
|
||||
|
||||
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
|
||||
@@ -42,22 +41,20 @@ export async function compressVideo(
|
||||
logger.debug('probe failed, falling through to passthrough', {
|
||||
safeMessage: e,
|
||||
})
|
||||
opts?.onProbeFailed?.(e)
|
||||
return {
|
||||
uri: file.uri,
|
||||
size: file.fileSize ?? -1,
|
||||
mimeType: file.mimeType ?? 'video/mp4',
|
||||
passthroughReason: 'compress-error-fallback',
|
||||
}
|
||||
}
|
||||
|
||||
const willCompress = shouldCompress(metadata, isAcceptableFormat)
|
||||
opts?.onProbe?.(metadata, willCompress)
|
||||
|
||||
if (!willCompress) {
|
||||
if (!shouldCompress(metadata, isAcceptableFormat)) {
|
||||
return {
|
||||
uri: file.uri,
|
||||
size: metadata.fileSize,
|
||||
mimeType: file.mimeType ?? 'video/mp4',
|
||||
passthroughReason: 'below-thresholds',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export async function compressVideo(
|
||||
const blob = await response.blob()
|
||||
|
||||
const isGif = blob.type === 'image/gif'
|
||||
const hasCodecs = hasWebCodecs()
|
||||
|
||||
logger.debug('compress: fetched blob', {
|
||||
size: blob.size,
|
||||
@@ -57,22 +58,32 @@ export async function compressVideo(
|
||||
|
||||
// Try MediaBunny compression if WebCodecs is available and file is large enough
|
||||
// Skip GIFs - MediaBunny doesn't support them
|
||||
if (hasWebCodecs() && blob.size >= COMPRESSION_MIN_SIZE_BYTES && !isGif) {
|
||||
let fallbackReason: NonNullable<CompressedVideo['passthroughReason']> | null =
|
||||
null
|
||||
if (isGif) {
|
||||
fallbackReason = 'gif'
|
||||
} else if (!hasCodecs) {
|
||||
fallbackReason = 'no-webcodecs'
|
||||
} else if (blob.size < COMPRESSION_MIN_SIZE_BYTES) {
|
||||
fallbackReason = 'below-byte-threshold'
|
||||
} else {
|
||||
try {
|
||||
return await doCompression(blob, asset.uri, {onProgress, signal})
|
||||
} catch (e) {
|
||||
logger.warn('compress: MediaBunny compression failed, using original', {
|
||||
safeMessage: e,
|
||||
})
|
||||
fallbackReason = 'compress-error-fallback'
|
||||
}
|
||||
} else {
|
||||
logger.debug('compress: skipping compression', {
|
||||
hasWebCodecs: hasWebCodecs(),
|
||||
blobSize: blob.size,
|
||||
minSize: COMPRESSION_MIN_SIZE_BYTES,
|
||||
})
|
||||
}
|
||||
|
||||
logger.debug('compress: skipping compression', {
|
||||
hasWebCodecs: hasCodecs,
|
||||
blobSize: blob.size,
|
||||
minSize: COMPRESSION_MIN_SIZE_BYTES,
|
||||
reason: fallbackReason,
|
||||
})
|
||||
|
||||
// No compression path - just return the blob as-is
|
||||
if (blob.size > VIDEO_MAX_SIZE) {
|
||||
throw new VideoTooLargeError()
|
||||
@@ -83,6 +94,7 @@ export async function compressVideo(
|
||||
size: blob.size,
|
||||
bytes: await blob.arrayBuffer(),
|
||||
mimeType: blob.type || 'video/mp4',
|
||||
passthroughReason: fallbackReason,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {type VideoCompressSkipReason} from '#/lib/media/video/types'
|
||||
import {Sentry} from '#/logger/sentry/lib'
|
||||
import {type Metrics} from '#/analytics/metrics'
|
||||
|
||||
type MetricFn = <E extends keyof Metrics>(event: E, payload: Metrics[E]) => void
|
||||
|
||||
// Identifies the active compression engine. Bumped when the engine swaps.
|
||||
// Versions are intentionally hardcoded so a dependency bump shows up in
|
||||
// analytics as a label change.
|
||||
const COMPRESS_ENGINE =
|
||||
Platform.OS === 'web'
|
||||
? 'web:mediabunny@1.25.3'
|
||||
: 'native:react-native-compressor@1.13.0'
|
||||
|
||||
type Phase = 'compress' | 'upload' | 'processing'
|
||||
|
||||
function errorClass(e: unknown): string {
|
||||
if (e instanceof Error) return e.name || 'Error'
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
export type VideoTelemetry = {
|
||||
readonly uploadId: string
|
||||
readonly engine: string
|
||||
picked: () => void
|
||||
compressStarted: () => void
|
||||
compressSkipped: (video: {
|
||||
size: number
|
||||
mimeType: string
|
||||
skipReason: VideoCompressSkipReason
|
||||
}) => void
|
||||
compressCompleted: (video: {size: number; mimeType: string}) => void
|
||||
compressFailed: (e: unknown) => void
|
||||
uploadStarted: (bytes: number) => void
|
||||
uploadCompleted: (jobId: string) => void
|
||||
uploadFailed: (e: unknown) => void
|
||||
processingStarted: (jobId: string) => void
|
||||
processingCompleted: () => void
|
||||
processingFailed: (e: unknown) => void
|
||||
published: () => void
|
||||
}
|
||||
|
||||
export function createVideoTelemetry({
|
||||
asset,
|
||||
signal,
|
||||
metric,
|
||||
}: {
|
||||
asset: ImagePickerAsset
|
||||
signal: AbortSignal
|
||||
metric: MetricFn
|
||||
}): VideoTelemetry {
|
||||
const uploadId = nanoid()
|
||||
const engine = COMPRESS_ENGINE
|
||||
const startedAt = Date.now()
|
||||
|
||||
let phase: Phase | undefined
|
||||
let phaseStartedAt = startedAt
|
||||
let jobId: string | undefined
|
||||
let uploadBytes: number | undefined
|
||||
let txnEnded = false
|
||||
let abortBound = true
|
||||
|
||||
// Parent span: full selection->ready arc. Inactive so phase spans can be
|
||||
// attached as children regardless of the current async context.
|
||||
const txn = Sentry.startInactiveSpan({
|
||||
name: 'video.upload',
|
||||
op: 'video.upload',
|
||||
attributes: {
|
||||
uploadId,
|
||||
engine,
|
||||
'video.source.mime': asset.mimeType ?? 'unknown',
|
||||
'video.source.bytes': asset.fileSize ?? 0,
|
||||
'video.source.durationMs': asset.duration ?? 0,
|
||||
'video.source.width': asset.width ?? 0,
|
||||
'video.source.height': asset.height ?? 0,
|
||||
},
|
||||
})
|
||||
|
||||
let phaseSpan: ReturnType<typeof Sentry.startInactiveSpan> | undefined
|
||||
|
||||
function endPhaseSpan() {
|
||||
if (!phaseSpan) return
|
||||
phaseSpan.end()
|
||||
phaseSpan = undefined
|
||||
}
|
||||
|
||||
function enterPhase(next: Phase, spanName: string) {
|
||||
endPhaseSpan()
|
||||
phase = next
|
||||
phaseStartedAt = Date.now()
|
||||
phaseSpan = Sentry.withActiveSpan(txn, () =>
|
||||
Sentry.startInactiveSpan({
|
||||
name: spanName,
|
||||
op: spanName,
|
||||
attributes: {uploadId, engine},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function endTxn(outcome: 'ok' | 'error' | 'cancelled') {
|
||||
if (txnEnded) return
|
||||
txnEnded = true
|
||||
endPhaseSpan()
|
||||
txn.setAttribute('outcome', outcome)
|
||||
txn.end()
|
||||
}
|
||||
|
||||
function detachAbort() {
|
||||
if (!abortBound) return
|
||||
abortBound = false
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
}
|
||||
|
||||
function onAbort() {
|
||||
if (phase) {
|
||||
metric('video:upload:abandoned', {
|
||||
uploadId,
|
||||
engine,
|
||||
phase,
|
||||
jobId,
|
||||
elapsedInPhaseMs: Date.now() - phaseStartedAt,
|
||||
})
|
||||
}
|
||||
endTxn('cancelled')
|
||||
abortBound = false
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, {once: true})
|
||||
|
||||
return {
|
||||
uploadId,
|
||||
engine,
|
||||
|
||||
picked() {
|
||||
metric('video:upload:picked', {
|
||||
uploadId,
|
||||
engine,
|
||||
sourceMimeType: asset.mimeType,
|
||||
sourceBytes: asset.fileSize,
|
||||
sourceDurationMs: asset.duration ?? undefined,
|
||||
sourceWidth: asset.width,
|
||||
sourceHeight: asset.height,
|
||||
})
|
||||
},
|
||||
|
||||
compressStarted() {
|
||||
enterPhase('compress', 'video.compress')
|
||||
metric('video:upload:compressStarted', {
|
||||
uploadId,
|
||||
engine,
|
||||
sourceBytes: asset.fileSize,
|
||||
})
|
||||
},
|
||||
|
||||
compressSkipped({size, mimeType, skipReason}) {
|
||||
metric('video:upload:compressSkipped', {
|
||||
uploadId,
|
||||
engine,
|
||||
skipReason,
|
||||
bytes: size,
|
||||
mimeType,
|
||||
elapsedMs: Date.now() - phaseStartedAt,
|
||||
})
|
||||
endPhaseSpan()
|
||||
phase = undefined
|
||||
},
|
||||
|
||||
compressCompleted({size, mimeType}) {
|
||||
metric('video:upload:compressCompleted', {
|
||||
uploadId,
|
||||
engine,
|
||||
bytesIn: asset.fileSize,
|
||||
bytesOut: size,
|
||||
outputMimeType: mimeType,
|
||||
elapsedMs: Date.now() - phaseStartedAt,
|
||||
})
|
||||
endPhaseSpan()
|
||||
phase = undefined
|
||||
},
|
||||
|
||||
compressFailed(e) {
|
||||
metric('video:upload:compressFailed', {
|
||||
uploadId,
|
||||
engine,
|
||||
errorClass: errorClass(e),
|
||||
elapsedMs: Date.now() - phaseStartedAt,
|
||||
})
|
||||
endTxn('error')
|
||||
detachAbort()
|
||||
},
|
||||
|
||||
uploadStarted(bytes) {
|
||||
uploadBytes = bytes
|
||||
enterPhase('upload', 'video.upload.transfer')
|
||||
metric('video:upload:uploadStarted', {uploadId, engine, bytes})
|
||||
},
|
||||
|
||||
uploadCompleted(id) {
|
||||
jobId = id
|
||||
const elapsedMs = Date.now() - phaseStartedAt
|
||||
const bytes = uploadBytes ?? 0
|
||||
metric('video:upload:uploadCompleted', {
|
||||
uploadId,
|
||||
engine,
|
||||
jobId: id,
|
||||
bytes,
|
||||
elapsedMs,
|
||||
throughputBytesPerSec:
|
||||
elapsedMs > 0 ? Math.round((bytes * 1000) / elapsedMs) : 0,
|
||||
})
|
||||
endPhaseSpan()
|
||||
phase = undefined
|
||||
},
|
||||
|
||||
uploadFailed(e) {
|
||||
metric('video:upload:uploadFailed', {
|
||||
uploadId,
|
||||
engine,
|
||||
bytes: uploadBytes ?? 0,
|
||||
errorClass: errorClass(e),
|
||||
elapsedMs: Date.now() - phaseStartedAt,
|
||||
})
|
||||
endTxn('error')
|
||||
detachAbort()
|
||||
},
|
||||
|
||||
processingStarted(id) {
|
||||
jobId = id
|
||||
enterPhase('processing', 'video.processing')
|
||||
metric('video:upload:processingStarted', {uploadId, engine, jobId: id})
|
||||
},
|
||||
|
||||
processingCompleted() {
|
||||
metric('video:upload:processingCompleted', {
|
||||
uploadId,
|
||||
engine,
|
||||
jobId: jobId ?? '',
|
||||
elapsedMs: Date.now() - phaseStartedAt,
|
||||
})
|
||||
// Upload pipeline is done; publish is a separate user action that
|
||||
// fires its own event. Releases the parent span so its duration
|
||||
// measures upload work, not idle composer time.
|
||||
endTxn('ok')
|
||||
detachAbort()
|
||||
},
|
||||
|
||||
processingFailed(e) {
|
||||
metric('video:upload:processingFailed', {
|
||||
uploadId,
|
||||
engine,
|
||||
jobId: jobId ?? '',
|
||||
errorClass: errorClass(e),
|
||||
elapsedMs: Date.now() - phaseStartedAt,
|
||||
})
|
||||
endTxn('error')
|
||||
detachAbort()
|
||||
},
|
||||
|
||||
published() {
|
||||
metric('video:upload:published', {
|
||||
uploadId,
|
||||
engine,
|
||||
jobId: jobId ?? '',
|
||||
totalElapsedMs: Date.now() - startedAt,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,21 @@
|
||||
// Why the compress engine returned the input unchanged. Used both as the
|
||||
// reason on `CompressedVideo.passthroughReason` and as the `skipReason` field
|
||||
// on the `video:upload:compressSkipped` analytics event, so the two stay in
|
||||
// sync.
|
||||
export type VideoCompressSkipReason =
|
||||
| 'gif'
|
||||
| 'below-byte-threshold'
|
||||
| 'below-thresholds'
|
||||
| 'no-webcodecs'
|
||||
| 'compress-error-fallback'
|
||||
|
||||
export type CompressedVideo = {
|
||||
uri: string
|
||||
mimeType: string
|
||||
size: number
|
||||
// web only, can fall back to uri if missing
|
||||
bytes?: ArrayBuffer
|
||||
// Set when the engine returned the input unchanged. Undefined means the
|
||||
// bytes were actually re-encoded.
|
||||
passthroughReason?: VideoCompressSkipReason
|
||||
}
|
||||
|
||||
+118
-112
@@ -1956,10 +1956,10 @@ msgstr ""
|
||||
|
||||
#: src/components/dms/dialogs/NewChatDialog.tsx:155
|
||||
#: src/components/dms/MessageProfileButton.tsx:60
|
||||
#: src/screens/Messages/ChatList.tsx:155
|
||||
#: src/screens/Messages/ChatList.tsx:173
|
||||
#: src/screens/Messages/ChatList.tsx:274
|
||||
#: src/screens/Messages/ChatList.tsx:527
|
||||
#: src/screens/Messages/ChatList.tsx:166
|
||||
#: src/screens/Messages/ChatList.tsx:184
|
||||
#: src/screens/Messages/ChatList.tsx:285
|
||||
#: src/screens/Messages/ChatList.tsx:541
|
||||
#: src/screens/Messages/Conversation.tsx:203
|
||||
#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:99
|
||||
msgid "Before you can message another user, you must first verify your email."
|
||||
@@ -2065,7 +2065,7 @@ msgstr ""
|
||||
msgid "Block user and/or leave this conversation"
|
||||
msgstr "Block user and/or leave this conversation"
|
||||
|
||||
#: src/components/Post/Embed/index.tsx:216
|
||||
#: src/components/Post/Embed/index.tsx:212
|
||||
msgid "Blocked"
|
||||
msgstr ""
|
||||
|
||||
@@ -2335,7 +2335,7 @@ msgstr "Camera access needed"
|
||||
#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:299
|
||||
#: src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx:175
|
||||
#: src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx:181
|
||||
#: src/components/Menu/index.tsx:367
|
||||
#: src/components/Menu/index.tsx:373
|
||||
#: src/components/moderation/BlockDialog.tsx:202
|
||||
#: src/components/PostControls/RepostButton.tsx:210
|
||||
#: src/components/Prompt.tsx:152
|
||||
@@ -2531,6 +2531,12 @@ msgstr ""
|
||||
msgid "Chat not found."
|
||||
msgstr "Chat not found."
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:567
|
||||
#: src/screens/Messages/ChatList.tsx:602
|
||||
#: src/screens/Messages/ChatList.tsx:640
|
||||
msgid "Chat options"
|
||||
msgstr "Chat options"
|
||||
|
||||
#: src/components/moderation/BlockDialog.tsx:293
|
||||
msgid "Chat owners cannot leave a group chat."
|
||||
msgstr "Chat owners cannot leave a group chat."
|
||||
@@ -2550,10 +2556,10 @@ msgstr ""
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:88
|
||||
#: src/Navigation.tsx:506
|
||||
#: src/screens/Messages/ChatList.tsx:84
|
||||
#: src/screens/Messages/ChatList.tsx:88
|
||||
#: src/screens/Messages/ChatList.tsx:552
|
||||
#: src/screens/Messages/ChatList.tsx:583
|
||||
#: src/screens/Messages/ChatList.tsx:95
|
||||
#: src/screens/Messages/ChatList.tsx:99
|
||||
#: src/screens/Messages/ChatList.tsx:652
|
||||
#: src/screens/Messages/ChatList.tsx:662
|
||||
#: src/screens/Messages/Settings.tsx:39
|
||||
msgid "Chat settings"
|
||||
msgstr ""
|
||||
@@ -2580,9 +2586,9 @@ msgctxt "toast"
|
||||
msgid "Chat unmuted"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:78
|
||||
#: src/screens/Messages/ChatList.tsx:539
|
||||
#: src/screens/Messages/ChatList.tsx:576
|
||||
#: src/screens/Messages/ChatList.tsx:89
|
||||
#: src/screens/Messages/ChatList.tsx:553
|
||||
#: src/screens/Messages/ChatList.tsx:593
|
||||
msgid "Chats"
|
||||
msgstr ""
|
||||
|
||||
@@ -2656,7 +2662,7 @@ msgstr ""
|
||||
msgid "Choose your password"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:192
|
||||
#: src/screens/Signup/index.tsx:193
|
||||
msgid "Choose your username"
|
||||
msgstr ""
|
||||
|
||||
@@ -2857,7 +2863,7 @@ msgstr "Close the incoming requests banner"
|
||||
#: src/components/intents/GroupChatJoinDialog.tsx:240
|
||||
#: src/components/intents/GroupChatJoinDialog.tsx:276
|
||||
#: src/components/intents/GroupChatJoinDialog.tsx:277
|
||||
#: src/components/Menu/index.tsx:361
|
||||
#: src/components/Menu/index.tsx:367
|
||||
msgid "Close this dialog"
|
||||
msgstr ""
|
||||
|
||||
@@ -2881,7 +2887,7 @@ msgstr ""
|
||||
msgid "Collapses list of users for a given notification"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:66
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:68
|
||||
msgid "Color"
|
||||
msgstr "Color"
|
||||
|
||||
@@ -2912,7 +2918,7 @@ msgstr ""
|
||||
msgid "Complete onboarding and start using your account"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:194
|
||||
#: src/screens/Signup/index.tsx:195
|
||||
msgid "Complete the challenge"
|
||||
msgstr ""
|
||||
|
||||
@@ -2989,8 +2995,8 @@ msgid "Contact our support team"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:156
|
||||
#: src/screens/Signup/index.tsx:233
|
||||
#: src/screens/Signup/index.tsx:236
|
||||
#: src/screens/Signup/index.tsx:240
|
||||
#: src/screens/Signup/index.tsx:243
|
||||
msgid "Contact support"
|
||||
msgstr ""
|
||||
|
||||
@@ -3130,7 +3136,7 @@ msgid "Copied build version to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/ChatInvite/Root.tsx:99
|
||||
#: src/components/dms/MessageContextMenu.tsx:83
|
||||
#: src/components/dms/MessageContextMenu.tsx:97
|
||||
#: src/components/PostControls/DiscoverDebug.tsx:36
|
||||
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:273
|
||||
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:77
|
||||
@@ -3222,8 +3228,8 @@ msgstr ""
|
||||
msgid "Copy link to starter pack"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:183
|
||||
#: src/components/dms/MessageContextMenu.tsx:187
|
||||
#: src/components/dms/MessageContextMenu.tsx:198
|
||||
#: src/components/dms/MessageContextMenu.tsx:202
|
||||
msgid "Copy message text"
|
||||
msgstr ""
|
||||
|
||||
@@ -3398,7 +3404,7 @@ msgstr ""
|
||||
#: src/components/WelcomeModal.tsx:158
|
||||
#: src/components/WelcomeModal.tsx:166
|
||||
#: src/screens/Messages/JoinRequest.tsx:310
|
||||
#: src/screens/Signup/index.tsx:135
|
||||
#: src/screens/Signup/index.tsx:136
|
||||
#: src/view/com/auth/SplashScreen.tsx:107
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:116
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:345
|
||||
@@ -3544,7 +3550,7 @@ msgstr ""
|
||||
msgid "Dawn"
|
||||
msgstr "Dawn"
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:29
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:30
|
||||
msgid "Day"
|
||||
msgstr "Day"
|
||||
|
||||
@@ -3627,7 +3633,7 @@ msgstr ""
|
||||
msgid "Delete conversation"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:197
|
||||
#: src/components/dms/MessageContextMenu.tsx:212
|
||||
msgid "Delete for me"
|
||||
msgstr ""
|
||||
|
||||
@@ -3640,7 +3646,7 @@ msgstr ""
|
||||
msgid "Delete message"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:194
|
||||
#: src/components/dms/MessageContextMenu.tsx:209
|
||||
msgid "Delete message for me"
|
||||
msgstr ""
|
||||
|
||||
@@ -3671,7 +3677,7 @@ msgstr ""
|
||||
msgid "Delete this post?"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Post/Embed/index.tsx:209
|
||||
#: src/components/Post/Embed/index.tsx:205
|
||||
msgid "Deleted"
|
||||
msgstr ""
|
||||
|
||||
@@ -4040,7 +4046,7 @@ msgstr ""
|
||||
msgid "Duration:"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:30
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:31
|
||||
msgid "Dusk"
|
||||
msgstr "Dusk"
|
||||
|
||||
@@ -4171,12 +4177,12 @@ msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/EditProfileDialog.tsx:265
|
||||
#: src/screens/Profile/Header/EditProfileDialog.tsx:271
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:296
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:299
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:350
|
||||
msgid "Edit profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:299
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:302
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:352
|
||||
msgid "Edit Profile"
|
||||
msgstr ""
|
||||
@@ -4598,7 +4604,7 @@ msgid "Failed to accept join request"
|
||||
msgstr "Failed to accept join request"
|
||||
|
||||
#: src/components/dms/ActionsWrapper.web.tsx:92
|
||||
#: src/components/dms/MessageContextMenu.tsx:126
|
||||
#: src/components/dms/MessageContextMenu.tsx:140
|
||||
msgid "Failed to add emoji reaction"
|
||||
msgstr ""
|
||||
|
||||
@@ -4693,10 +4699,6 @@ msgstr ""
|
||||
msgid "Failed to hide suggestion, please check your internet connection"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/JoinRequests.tsx:223
|
||||
msgid "Failed to ignore join request"
|
||||
msgstr "Failed to ignore join request"
|
||||
|
||||
#: src/components/intents/GroupChatJoinDialog.tsx:147
|
||||
msgid "Failed to join the group chat. Please try again."
|
||||
msgstr "Failed to join the group chat. Please try again."
|
||||
@@ -4712,7 +4714,7 @@ msgctxt "toast"
|
||||
msgid "Failed to leave group chat"
|
||||
msgstr "Failed to leave group chat"
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:404
|
||||
#: src/screens/Messages/ChatList.tsx:418
|
||||
#: src/screens/Messages/Inbox.tsx:209
|
||||
msgid "Failed to load conversations"
|
||||
msgstr ""
|
||||
@@ -4762,6 +4764,7 @@ msgstr ""
|
||||
msgid "Failed to lock group chat"
|
||||
msgstr "Failed to lock group chat"
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:634
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:441
|
||||
msgid "Failed to mark all chats as read"
|
||||
msgstr "Failed to mark all chats as read"
|
||||
@@ -4784,6 +4787,10 @@ msgstr ""
|
||||
msgid "Failed to reconnect Germ DM. Error: {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/JoinRequests.tsx:223
|
||||
msgid "Failed to reject join request"
|
||||
msgstr "Failed to reject join request"
|
||||
|
||||
#: src/screens/Settings/FindContactsSettings.tsx:509
|
||||
msgid "Failed to remove data due to a network error, please check your internet connection."
|
||||
msgstr ""
|
||||
@@ -4794,7 +4801,7 @@ msgid "Failed to remove data. {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/ActionsWrapper.web.tsx:77
|
||||
#: src/components/dms/MessageContextMenu.tsx:111
|
||||
#: src/components/dms/MessageContextMenu.tsx:125
|
||||
#: src/components/dms/ReactionsDialog.tsx:179
|
||||
msgid "Failed to remove emoji reaction"
|
||||
msgstr ""
|
||||
@@ -5779,7 +5786,7 @@ msgstr ""
|
||||
msgid "Have we got your location wrong? <0>Tap here to update your location with GPS.</0>"
|
||||
msgstr "Have we got your location wrong? <0>Tap here to update your location with GPS.</0>"
|
||||
|
||||
#: src/screens/Signup/index.tsx:231
|
||||
#: src/screens/Signup/index.tsx:238
|
||||
msgid "Having trouble?"
|
||||
msgstr ""
|
||||
|
||||
@@ -6224,7 +6231,7 @@ msgstr "In-app, push, everyone"
|
||||
msgid "In-app, push, people you follow"
|
||||
msgstr "In-app, push, people you follow"
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:421
|
||||
#: src/screens/Messages/ChatList.tsx:435
|
||||
msgid "Inbox empty"
|
||||
msgstr "Inbox empty"
|
||||
|
||||
@@ -6403,8 +6410,8 @@ msgid "Invites, but personal"
|
||||
msgstr ""
|
||||
|
||||
#: src/ageAssurance/components/NoAccessScreen.tsx:394
|
||||
msgid "Is your location not accurate? <0>Tap here to update your location with GPS.</0> "
|
||||
msgstr "Is your location not accurate? <0>Tap here to update your location with GPS.</0> "
|
||||
msgid "Is your location not accurate? <0>Tap here to update your location with GPS.</0>"
|
||||
msgstr "Is your location not accurate? <0>Tap here to update your location with GPS.</0>"
|
||||
|
||||
#: src/components/contacts/components/InviteInfo.tsx:47
|
||||
msgid "It looks like some of your contacts have not tried to find you here yet. You can personally invite them by customizing a draft message we will provide."
|
||||
@@ -6474,11 +6481,6 @@ msgstr ""
|
||||
msgid "Keep me posted"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/BlockDialog.tsx:365
|
||||
#: src/components/moderation/BlockDialog.tsx:372
|
||||
msgid "Kick member"
|
||||
msgstr "Kick member"
|
||||
|
||||
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:200
|
||||
msgid "KWS website"
|
||||
msgstr ""
|
||||
@@ -6949,9 +6951,7 @@ msgstr ""
|
||||
msgid "Load new posts"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:245
|
||||
#: src/screens/Messages/components/MessageInput.tsx:258
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:228
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:254
|
||||
msgctxt "placeholder"
|
||||
msgid "Loading chat…"
|
||||
msgstr "Loading chat…"
|
||||
@@ -7065,6 +7065,8 @@ msgstr ""
|
||||
msgid "Mark all as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:644
|
||||
#: src/screens/Messages/ChatList.tsx:648
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:459
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:463
|
||||
msgid "Mark all chats as read"
|
||||
@@ -7084,6 +7086,7 @@ msgstr ""
|
||||
msgid "Marked all as read"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:631
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:438
|
||||
msgid "Marked all chats as read"
|
||||
msgstr "Marked all chats as read"
|
||||
@@ -7139,19 +7142,17 @@ msgstr ""
|
||||
msgid "Mentions"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Menu/index.tsx:113
|
||||
#: src/components/Menu/index.tsx:119
|
||||
msgid "Menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageInput.tsx:202
|
||||
#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:203
|
||||
msgctxt "action"
|
||||
msgid "Message"
|
||||
msgstr "Message"
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:246
|
||||
#: src/screens/Messages/components/MessageInput.tsx:259
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:229
|
||||
#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:203
|
||||
msgctxt "action"
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:255
|
||||
msgctxt "description"
|
||||
msgid "Message"
|
||||
msgstr "Message"
|
||||
|
||||
@@ -7179,7 +7180,7 @@ msgstr "Message failed to send."
|
||||
|
||||
#. placeholder {0}: sender?.handle ?? 'unknown'
|
||||
#. placeholder {1}: message.text
|
||||
#: src/components/dms/MessageContextMenu.tsx:152
|
||||
#: src/components/dms/MessageContextMenu.tsx:166
|
||||
msgid "Message from @{0}: {1}"
|
||||
msgstr ""
|
||||
|
||||
@@ -7188,21 +7189,15 @@ msgstr ""
|
||||
msgid "Message from server: {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:242
|
||||
#: src/screens/Messages/components/MessageInput.tsx:200
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:251
|
||||
msgid "Message input field"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageInput.tsx:96
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:65
|
||||
msgid "Message is too long"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:107
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:116
|
||||
msgid "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})"
|
||||
msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})"
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:151
|
||||
#: src/components/dms/MessageContextMenu.tsx:165
|
||||
msgid "Message options"
|
||||
msgstr ""
|
||||
|
||||
@@ -7510,7 +7505,6 @@ msgstr ""
|
||||
msgid "Nevermind, create a handle for me"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dialogs/nuxs/GroupChatsAnnouncement.tsx:171
|
||||
#: src/screens/Search/modules/ExploreTrendingTopics.tsx:184
|
||||
#: src/view/com/profile/ProfileMenu.tsx:398
|
||||
msgid "New"
|
||||
@@ -7522,6 +7516,11 @@ msgctxt "action"
|
||||
msgid "New"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dialogs/nuxs/GroupChatsAnnouncement.tsx:171
|
||||
msgctxt "nux-description"
|
||||
msgid "New"
|
||||
msgstr "New"
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:569
|
||||
msgid "New {postsCount, plural, one {post} other {posts}} from {firstAuthorLink}"
|
||||
msgstr ""
|
||||
@@ -7532,11 +7531,11 @@ msgstr ""
|
||||
|
||||
#: src/components/dms/dialogs/NewChatDialog.tsx:169
|
||||
#: src/components/dms/dialogs/NewChatDialog.tsx:184
|
||||
#: src/screens/Messages/ChatList.tsx:218
|
||||
#: src/screens/Messages/ChatList.tsx:219
|
||||
#: src/screens/Messages/ChatList.tsx:439
|
||||
#: src/screens/Messages/ChatList.tsx:440
|
||||
#: src/screens/Messages/ChatList.tsx:561
|
||||
#: src/screens/Messages/ChatList.tsx:229
|
||||
#: src/screens/Messages/ChatList.tsx:230
|
||||
#: src/screens/Messages/ChatList.tsx:453
|
||||
#: src/screens/Messages/ChatList.tsx:454
|
||||
#: src/screens/Messages/ChatList.tsx:578
|
||||
msgid "New chat"
|
||||
msgstr ""
|
||||
|
||||
@@ -7676,7 +7675,7 @@ msgstr ""
|
||||
msgid "Next image"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:31
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:33
|
||||
msgid "Night"
|
||||
msgstr "Night"
|
||||
|
||||
@@ -8087,8 +8086,8 @@ msgid "Only admins can accept join requests."
|
||||
msgstr "Only admins can accept join requests."
|
||||
|
||||
#: src/screens/Messages/JoinRequests.tsx:233
|
||||
msgid "Only admins can ignore join requests."
|
||||
msgstr "Only admins can ignore join requests."
|
||||
msgid "Only admins can reject join requests."
|
||||
msgstr "Only admins can reject join requests."
|
||||
|
||||
#: src/components/intents/GroupChatJoinDialog.tsx:155
|
||||
msgid "Only followers can join this group chat."
|
||||
@@ -8165,8 +8164,7 @@ msgstr ""
|
||||
msgid "Open drawer menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:204
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:174
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:213
|
||||
#: src/view/com/composer/Composer.tsx:2158
|
||||
msgid "Open emoji picker"
|
||||
msgstr ""
|
||||
@@ -8275,7 +8273,7 @@ msgstr ""
|
||||
msgid "Opens a dialog to choose who can interact with this post"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:38
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:40
|
||||
msgid "Opens a list of color themes for the QR card"
|
||||
msgstr "Opens a list of color themes for the QR card"
|
||||
|
||||
@@ -8605,7 +8603,7 @@ msgstr ""
|
||||
msgid "Photography"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:37
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:39
|
||||
msgid "Pick a color theme"
|
||||
msgstr "Pick a color theme"
|
||||
|
||||
@@ -9334,7 +9332,7 @@ msgstr ""
|
||||
msgid "Reject join request"
|
||||
msgstr "Reject join request"
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:408
|
||||
#: src/screens/Messages/ChatList.tsx:422
|
||||
#: src/screens/Messages/Inbox.tsx:213
|
||||
msgid "Reload conversations"
|
||||
msgstr ""
|
||||
@@ -9461,6 +9459,11 @@ msgstr ""
|
||||
msgid "Remove live status"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/BlockDialog.tsx:365
|
||||
#: src/components/moderation/BlockDialog.tsx:372
|
||||
msgid "Remove member"
|
||||
msgstr "Remove member"
|
||||
|
||||
#: src/components/dialogs/MutedWords.tsx:613
|
||||
msgid "Remove mute word from your list"
|
||||
msgstr ""
|
||||
@@ -9502,11 +9505,11 @@ msgstr ""
|
||||
msgid "Remove your verification for this account?"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Post/Embed/index.tsx:244
|
||||
#: src/components/Post/Embed/index.tsx:240
|
||||
msgid "Removed by author"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Post/Embed/index.tsx:242
|
||||
#: src/components/Post/Embed/index.tsx:238
|
||||
msgid "Removed by you"
|
||||
msgstr ""
|
||||
|
||||
@@ -9604,8 +9607,8 @@ msgstr ""
|
||||
msgid "Replies to this post are disabled."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:167
|
||||
#: src/components/dms/MessageContextMenu.tsx:170
|
||||
#: src/components/dms/MessageContextMenu.tsx:182
|
||||
#: src/components/dms/MessageContextMenu.tsx:185
|
||||
msgid "Reply"
|
||||
msgstr "Reply"
|
||||
|
||||
@@ -9647,7 +9650,7 @@ msgstr ""
|
||||
msgid "Reply was successfully hidden"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:205
|
||||
#: src/components/dms/MessageContextMenu.tsx:220
|
||||
#: src/features/liveNow/components/LiveStatusDialog.tsx:267
|
||||
#: src/screens/Messages/ConversationSettings/index.tsx:583
|
||||
msgid "Report"
|
||||
@@ -9680,7 +9683,7 @@ msgstr ""
|
||||
msgid "Report list"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:202
|
||||
#: src/components/dms/MessageContextMenu.tsx:217
|
||||
msgid "Report message"
|
||||
msgstr ""
|
||||
|
||||
@@ -9808,8 +9811,8 @@ msgid "Request code"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/JoinRequests.tsx:215
|
||||
msgid "Request ignored."
|
||||
msgstr "Request ignored."
|
||||
msgid "Request rejected."
|
||||
msgstr "Request rejected."
|
||||
|
||||
#: src/components/dms/ChatInvite/Root.tsx:117
|
||||
#: src/components/intents/GroupChatJoinDialog.tsx:295
|
||||
@@ -9929,7 +9932,7 @@ msgstr ""
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:377
|
||||
#: src/screens/Login/LoginForm.tsx:329
|
||||
#: src/screens/Login/LoginForm.tsx:335
|
||||
#: src/screens/Messages/ChatList.tsx:413
|
||||
#: src/screens/Messages/ChatList.tsx:427
|
||||
#: src/screens/Messages/components/MessageListError.tsx:24
|
||||
#: src/screens/Messages/Inbox.tsx:218
|
||||
#: src/screens/Messages/JoinRequests.tsx:361
|
||||
@@ -10084,8 +10087,8 @@ msgstr ""
|
||||
msgid "Say hello!"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/ChatList.tsx:209
|
||||
#: src/screens/Messages/ChatList.tsx:430
|
||||
#: src/screens/Messages/ChatList.tsx:220
|
||||
#: src/screens/Messages/ChatList.tsx:444
|
||||
msgid "Say hi to someone"
|
||||
msgstr "Say hi to someone"
|
||||
|
||||
@@ -10486,8 +10489,7 @@ msgstr "Send error report"
|
||||
msgid "Send feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:316
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:251
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:324
|
||||
msgid "Send message"
|
||||
msgstr ""
|
||||
|
||||
@@ -10521,7 +10523,7 @@ msgid "Send via direct message"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: i18n.date(new Date(message.sentAt), { timeStyle: 'short', })
|
||||
#: src/components/dms/MessageContextMenu.tsx:161
|
||||
#: src/components/dms/MessageContextMenu.tsx:175
|
||||
msgid "Sent at {0}"
|
||||
msgstr "Sent at {0}"
|
||||
|
||||
@@ -11218,7 +11220,7 @@ msgstr ""
|
||||
#. placeholder {1}: state.serviceDescription && !state.serviceDescription.phoneVerificationRequired ? '2' : '3'
|
||||
#. placeholder {1}: state.totalSteps
|
||||
#: src/screens/Onboarding/Layout.tsx:226
|
||||
#: src/screens/Signup/index.tsx:180
|
||||
#: src/screens/Signup/index.tsx:181
|
||||
msgid "Step {0} of {1}"
|
||||
msgstr ""
|
||||
|
||||
@@ -11290,11 +11292,11 @@ msgstr ""
|
||||
msgid "Subscribe to account activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:347
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:350
|
||||
msgid "Subscribe to Labeler"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:313
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:316
|
||||
msgid "Subscribe to this labeler"
|
||||
msgstr ""
|
||||
|
||||
@@ -11340,6 +11342,10 @@ msgstr ""
|
||||
msgid "Suggestive"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:29
|
||||
msgid "Sunlight"
|
||||
msgstr "Sunlight"
|
||||
|
||||
#: src/screens/Settings/AppIconSettings/useAppIconSets.ts:72
|
||||
msgctxt "Name of app icon variant"
|
||||
msgid "Sunrise"
|
||||
@@ -12299,8 +12305,8 @@ msgstr ""
|
||||
msgid "Topic"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageContextMenu.tsx:176
|
||||
#: src/components/dms/MessageContextMenu.tsx:179
|
||||
#: src/components/dms/MessageContextMenu.tsx:191
|
||||
#: src/components/dms/MessageContextMenu.tsx:194
|
||||
#: src/components/Post/Translated/index.tsx:150
|
||||
#: src/components/Post/Translated/index.tsx:157
|
||||
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:557
|
||||
@@ -12388,14 +12394,14 @@ msgstr "Try it"
|
||||
msgid "TV"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/inviteFriends/components/ThemePicker.tsx:32
|
||||
msgid "Twilight"
|
||||
msgstr "Twilight"
|
||||
|
||||
#: src/screens/Settings/PrivacyAndSecuritySettings.tsx:65
|
||||
msgid "Two-factor authentication (2FA)"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageInput.tsx:201
|
||||
msgid "Type your message here"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:445
|
||||
msgid "Type:"
|
||||
msgstr ""
|
||||
@@ -12418,7 +12424,7 @@ msgstr ""
|
||||
#: src/screens/Login/index.tsx:96
|
||||
#: src/screens/Login/LoginForm.tsx:167
|
||||
#: src/screens/Login/SetNewPasswordForm.tsx:83
|
||||
#: src/screens/Signup/index.tsx:87
|
||||
#: src/screens/Signup/index.tsx:88
|
||||
msgid "Unable to contact your service. Please check your Internet connection."
|
||||
msgstr ""
|
||||
|
||||
@@ -12677,7 +12683,7 @@ msgstr ""
|
||||
msgid "Unsnooze email reminder"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:345
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:348
|
||||
msgid "Unsubscribe"
|
||||
msgstr ""
|
||||
|
||||
@@ -12686,7 +12692,7 @@ msgstr ""
|
||||
msgid "Unsubscribe from list"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:312
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:315
|
||||
msgid "Unsubscribe from this labeler"
|
||||
msgstr ""
|
||||
|
||||
@@ -13455,7 +13461,7 @@ msgstr "We’re having network issues, try again"
|
||||
msgid "We’re introducing a new layer of verification on Bluesky — an easy-to-see checkmark."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:136
|
||||
#: src/screens/Signup/index.tsx:137
|
||||
msgid "We’re so excited to have you join us!"
|
||||
msgstr "We’re so excited to have you join us!"
|
||||
|
||||
@@ -13559,7 +13565,7 @@ msgid "Who can verify?"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Home/NoFeedsPinned.tsx:80
|
||||
#: src/screens/Messages/ChatList.tsx:393
|
||||
#: src/screens/Messages/ChatList.tsx:407
|
||||
#: src/screens/Messages/Inbox.tsx:198
|
||||
msgid "Whoops!"
|
||||
msgstr ""
|
||||
@@ -13844,7 +13850,7 @@ msgid "You can update this later from your settings."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/ActionsWrapper.web.tsx:81
|
||||
#: src/components/dms/MessageContextMenu.tsx:115
|
||||
#: src/components/dms/MessageContextMenu.tsx:129
|
||||
msgid "You cannot add more than {EMOJI_REACTION_LIMIT, plural, one {# emoji reaction} other {# emoji reactions}}"
|
||||
msgstr "You cannot add more than {EMOJI_REACTION_LIMIT, plural, one {# emoji reaction} other {# emoji reactions}}"
|
||||
|
||||
@@ -14112,11 +14118,11 @@ msgstr "You won’t be able to rejoin unless you’re invited."
|
||||
msgid "You: {message}"
|
||||
msgstr "You: {message}"
|
||||
|
||||
#: src/screens/Signup/index.tsx:152
|
||||
#: src/screens/Signup/index.tsx:153
|
||||
msgid "You'll follow the suggested users and feeds once you finish creating your account!"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:157
|
||||
#: src/screens/Signup/index.tsx:158
|
||||
msgid "You'll follow the suggested users once you finish creating your account!"
|
||||
msgstr ""
|
||||
|
||||
@@ -14204,7 +14210,7 @@ msgstr ""
|
||||
msgid "You've run out of videos to watch. Maybe it's a good time to take a break?"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Signup/index.tsx:190
|
||||
#: src/screens/Signup/index.tsx:191
|
||||
msgid "Your account"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
||||
import {useMessagesEventBus} from '#/state/messages/events'
|
||||
import {useUnreadCountsQuery} from '#/state/queries/messages/get-unread-counts'
|
||||
import {useListConvoRequests} from '#/state/queries/messages/list-conversation-requests'
|
||||
import {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
@@ -84,16 +85,8 @@ export function MessagesInboxScreenInner({}: Props) {
|
||||
return items
|
||||
}, [data])
|
||||
|
||||
const hasUnreadConvos = useMemo(() => {
|
||||
return conversations.some(
|
||||
item =>
|
||||
item.type === 'incoming' &&
|
||||
item.view.members.every(
|
||||
member => member.handle !== 'missing.invalid',
|
||||
) &&
|
||||
item.view.unreadCount > 0,
|
||||
)
|
||||
}, [conversations])
|
||||
const {data: unreadCounts} = useUnreadCountsQuery()
|
||||
const hasUnreadConvos = (unreadCounts?.unreadRequestConvos ?? 0) > 0
|
||||
|
||||
return (
|
||||
<Layout.Screen testID="messagesInboxScreen">
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
|
||||
import {
|
||||
type NotificationSettingsPreference,
|
||||
useChatNotificationSettingsQuery,
|
||||
} from '#/state/queries/notifications/settings'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {ExportCarDialog} from '#/screens/Settings/components/ExportCarDialog'
|
||||
import {ChatNotificationDialogs} from '#/screens/Settings/NotificationSettings/components/ChatNotificationDialogs'
|
||||
import {SettingPreview} from '#/screens/Settings/NotificationSettings/components/SettingPreview'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
@@ -18,7 +24,10 @@ import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell'
|
||||
import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||
import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
|
||||
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import * as Skele from '#/components/Skeleton'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
@@ -52,8 +61,12 @@ export function MessagesSettingsScreenInner({}: Props) {
|
||||
const {data: profile} = useProfileQuery({
|
||||
did: currentAccount!.did,
|
||||
})
|
||||
const {data: chatNotificationSettings, isError: chatSettingsError} =
|
||||
useChatNotificationSettingsQuery()
|
||||
const {preferences, setPref} = useBackgroundNotificationPreferences()
|
||||
|
||||
const chatDialogControl = Dialog.useDialogControl()
|
||||
const chatRequestDialogControl = Dialog.useDialogControl()
|
||||
const exportCarControl = Dialog.useDialogControl()
|
||||
|
||||
const isGroupChatEnabled = !ax.features.enabled(ax.features.GroupChatsDisable)
|
||||
@@ -237,6 +250,54 @@ export function MessagesSettingsScreenInner({}: Props) {
|
||||
<Divider style={{marginVertical: 10}} />
|
||||
</>
|
||||
) : null}
|
||||
<View style={[a.px_xl, a.gap_lg]}>
|
||||
<Text style={[a.pb_xs, a.text_md, a.font_semi_bold, t.atoms.text]}>
|
||||
<Trans>Notifications</Trans>
|
||||
</Text>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={l`Settings for notifications for new messages`}
|
||||
accessibilityHint={undefined}
|
||||
style={[a.flex_row, a.align_start, a.justify_between, a.gap_sm]}
|
||||
onPress={() => {
|
||||
chatDialogControl.open()
|
||||
}}>
|
||||
<MessageIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
|
||||
<View style={[a.flex_1, a.flex_grow]}>
|
||||
<Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
|
||||
<Trans>New messages</Trans>
|
||||
</Text>
|
||||
<NotificationPreferenceSubtitle
|
||||
preference={chatNotificationSettings?.chat}
|
||||
isLoading={!chatNotificationSettings}
|
||||
isError={chatSettingsError}
|
||||
/>
|
||||
</View>
|
||||
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={l`Settings for notifications for new message requests`}
|
||||
accessibilityHint={undefined}
|
||||
style={[a.flex_row, a.align_start, a.justify_between, a.gap_sm]}
|
||||
onPress={() => {
|
||||
chatRequestDialogControl.open()
|
||||
}}>
|
||||
<EnvelopeIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
|
||||
<View style={[a.flex_1, a.flex_grow]}>
|
||||
<Text style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
|
||||
<Trans>New message requests</Trans>
|
||||
</Text>
|
||||
<NotificationPreferenceSubtitle
|
||||
preference={chatNotificationSettings?.chatRequest}
|
||||
isLoading={!chatNotificationSettings}
|
||||
isError={chatSettingsError}
|
||||
/>
|
||||
</View>
|
||||
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
|
||||
</Pressable>
|
||||
</View>
|
||||
<Divider style={{marginVertical: 10}} />
|
||||
{IS_NATIVE && (
|
||||
<>
|
||||
<View style={[a.px_xl]}>
|
||||
@@ -263,12 +324,12 @@ export function MessagesSettingsScreenInner({}: Props) {
|
||||
</>
|
||||
)}
|
||||
<View style={[a.px_xl]}>
|
||||
<Toggle.Item
|
||||
label={l`Export my chat data`}
|
||||
name="playSoundChat"
|
||||
value={preferences.playSoundChat}
|
||||
style={[a.flex_row, a.align_center, a.justify_between]}
|
||||
onChange={() => {
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={l`Export my chat data`}
|
||||
accessibilityHint={undefined}
|
||||
style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}
|
||||
onPress={() => {
|
||||
exportCarControl.open()
|
||||
}}>
|
||||
<CarIcon style={[a.mr_2xs, t.atoms.text]} size="lg" />
|
||||
@@ -277,12 +338,46 @@ export function MessagesSettingsScreenInner({}: Props) {
|
||||
<Trans>Export my chat data</Trans>
|
||||
</Text>
|
||||
<ChevronRightIcon style={[a.ml_2xs, t.atoms.text]} size="lg" />
|
||||
</Toggle.Item>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Divider style={{marginVertical: 10}} />
|
||||
</View>
|
||||
</Layout.Content>
|
||||
<ChatNotificationDialogs
|
||||
chatControl={chatDialogControl}
|
||||
chatRequestControl={chatRequestDialogControl}
|
||||
/>
|
||||
<ExportCarDialog control={exportCarControl} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationPreferenceSubtitle({
|
||||
preference,
|
||||
isLoading,
|
||||
isError,
|
||||
}: {
|
||||
preference?: NotificationSettingsPreference
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||
<Trans>Failed to load notification settings.</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Skele.Text style={[a.text_sm, {width: 120}]} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||
<SettingPreview preference={preference} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {UNREAD_REQUEST_CAP} from '#/state/queries/messages/get-unread-counts'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Inbox_Stroke2_Corner2_Rounded as InboxIcon} from '#/components/icons/Inbox'
|
||||
import {Link} from '#/components/Link'
|
||||
|
||||
// The server caps unreadRequestConvos at 11, where 11 means "any more than 10".
|
||||
const REQUEST_COUNT_CAP = 11
|
||||
|
||||
export function InboxRequests({
|
||||
count,
|
||||
variant,
|
||||
@@ -21,7 +19,7 @@ export function InboxRequests({
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const unread = count > 0
|
||||
const overflow = count >= REQUEST_COUNT_CAP
|
||||
const overflow = count >= UNREAD_REQUEST_CAP
|
||||
|
||||
const label = !unread
|
||||
? l({
|
||||
@@ -30,8 +28,8 @@ export function InboxRequests({
|
||||
})
|
||||
: overflow
|
||||
? l({
|
||||
message: `10+ requests`,
|
||||
comment: 'Displayed when the number of requests is greater than 10',
|
||||
message: `${UNREAD_REQUEST_CAP - 1}+ requests`,
|
||||
comment: 'Displayed when the number of requests exceeds the cap',
|
||||
})
|
||||
: plural(count, {
|
||||
one: '# request',
|
||||
@@ -55,9 +53,9 @@ export function InboxRequests({
|
||||
<ButtonText style={[a.text_md, a.font_bold]}>
|
||||
{overflow
|
||||
? l({
|
||||
message: `10+`,
|
||||
message: `${UNREAD_REQUEST_CAP - 1}+`,
|
||||
comment:
|
||||
'Displayed when the number of requests is greater than 10',
|
||||
'Displayed when the number of requests exceeds the cap – for example, 99+ requests',
|
||||
})
|
||||
: count}
|
||||
</ButtonText>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {LayoutAnimation, View} from 'react-native'
|
||||
import {AppBskyEmbedRecord, ChatBskyEmbedJoinLink} from '@atproto/api'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
@@ -8,6 +8,7 @@ import {useConvoActive} from '#/state/messages/convo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
||||
import {useReplyPreviewText} from '#/components/dms/replyPreview'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
@@ -16,15 +17,27 @@ import {Text} from '#/components/Typography'
|
||||
* being replied to, with a button to cancel the reply.
|
||||
*/
|
||||
export function MessageInputReply() {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const convo = useConvoActive()
|
||||
const {replyTo, clearReply} = useMessageReplies()
|
||||
const {replyTo} = useMessageReplies()
|
||||
|
||||
if (!replyTo) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <MessageInputReplyInner replyTo={replyTo} />
|
||||
}
|
||||
|
||||
function MessageInputReplyInner({
|
||||
replyTo,
|
||||
}: {
|
||||
replyTo: ChatBskyConvoDefs.MessageView
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const convo = useConvoActive()
|
||||
const {clearReply} = useMessageReplies()
|
||||
const getReplyPreviewText = useReplyPreviewText()
|
||||
const {text, subtle} = getReplyPreviewText(replyTo)
|
||||
|
||||
const onRemove = () => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
clearReply()
|
||||
@@ -35,19 +48,6 @@ export function MessageInputReply() {
|
||||
? createSanitizedDisplayName(senderProfile, false)
|
||||
: null
|
||||
|
||||
let text = replyTo.text
|
||||
let subtle = false
|
||||
if (!text.trim()) {
|
||||
subtle = true
|
||||
if (ChatBskyEmbedJoinLink.isView(replyTo.embed)) {
|
||||
text = l`(chat invite link)`
|
||||
} else if (AppBskyEmbedRecord.isView(replyTo.embed)) {
|
||||
text = l`(contains embedded content)`
|
||||
} else {
|
||||
text = l`No text`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -82,7 +82,7 @@ export function MessageInputReply() {
|
||||
<Button
|
||||
label={l`Cancel reply`}
|
||||
onPress={onRemove}
|
||||
style={[a.px_2xs]}
|
||||
style={[a.px_2xs, {transform: [{translateX: 2}]}]}
|
||||
hitSlop={HITSLOP_20}>
|
||||
<XIcon size="xs" style={t.atoms.text_contrast_high} />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
import {NotificationSettingsDialog} from '#/components/dialogs/NotificationSettingsDialog'
|
||||
import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
|
||||
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
||||
|
||||
export function ChatNotificationDialogs({
|
||||
chatControl,
|
||||
chatRequestControl,
|
||||
}: {
|
||||
chatControl: Dialog.DialogControlProps
|
||||
chatRequestControl: Dialog.DialogControlProps
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<NotificationSettingsDialog
|
||||
control={chatControl}
|
||||
name="chat"
|
||||
icon={MessageIcon}
|
||||
titleText={<Trans>New messages</Trans>}
|
||||
subtitleText={
|
||||
<Trans>Get notifications when people send you messages.</Trans>
|
||||
}
|
||||
allowDisableInApp={false}
|
||||
/>
|
||||
<NotificationSettingsDialog
|
||||
control={chatRequestControl}
|
||||
name="chatRequest"
|
||||
icon={EnvelopeIcon}
|
||||
titleText={<Trans>New message requests</Trans>}
|
||||
subtitleText={
|
||||
<Trans>
|
||||
Get notifications when people send you message requests.
|
||||
</Trans>
|
||||
}
|
||||
allowDisableInApp={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings'
|
||||
import {
|
||||
type NotificationSettingsPreference,
|
||||
type NotificationSettingsPreferenceName,
|
||||
useNotificationSettingsUpdateMutation,
|
||||
} from '#/state/queries/notifications/settings'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Loader} from '#/components/Loader'
|
||||
@@ -17,16 +20,13 @@ export function PreferenceControls({
|
||||
preference,
|
||||
allowDisableInApp = true,
|
||||
}: {
|
||||
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
|
||||
name: NotificationSettingsPreferenceName
|
||||
/**
|
||||
* Keep other prefs in sync with `name`. For use in the "everything else" category
|
||||
* which groups starterpack joins + verified + unverified notifications into a single toggle.
|
||||
*/
|
||||
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
|
||||
preference?:
|
||||
| AppBskyNotificationDefs.Preference
|
||||
| AppBskyNotificationDefs.FilterablePreference
|
||||
| AppBskyNotificationDefs.ChatPreference
|
||||
syncOthers?: NotificationSettingsPreferenceName[]
|
||||
preference?: NotificationSettingsPreference
|
||||
allowDisableInApp?: boolean
|
||||
}) {
|
||||
if (!preference)
|
||||
@@ -52,12 +52,9 @@ export function Inner({
|
||||
preference,
|
||||
allowDisableInApp,
|
||||
}: {
|
||||
name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>
|
||||
syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[]
|
||||
preference:
|
||||
| AppBskyNotificationDefs.Preference
|
||||
| AppBskyNotificationDefs.FilterablePreference
|
||||
| AppBskyNotificationDefs.ChatPreference
|
||||
name: NotificationSettingsPreferenceName
|
||||
syncOthers?: NotificationSettingsPreferenceName[]
|
||||
preference: NotificationSettingsPreference
|
||||
allowDisableInApp: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
@@ -95,7 +92,7 @@ export function Inner({
|
||||
}
|
||||
|
||||
const onChangeFilter = ([change]: string[]) => {
|
||||
if (change !== 'all' && change !== 'follows' && change !== 'accepted')
|
||||
if (change !== 'all' && change !== 'follows')
|
||||
throw new Error('Invalid filter')
|
||||
|
||||
const newPreference = {
|
||||
@@ -135,7 +132,7 @@ export function Inner({
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Platform />
|
||||
</Toggle.Item>
|
||||
{allowDisableInApp && (
|
||||
{allowDisableInApp && 'list' in preference && (
|
||||
<Toggle.Item
|
||||
label={l`Receive in-app notifications`}
|
||||
name="list"
|
||||
@@ -176,31 +173,17 @@ export function Inner({
|
||||
/>
|
||||
)}
|
||||
</Toggle.Item>
|
||||
{name === 'chat' ? (
|
||||
<Toggle.Item
|
||||
highlightRow
|
||||
label={l`Accepted conversations`}
|
||||
name="accepted">
|
||||
{({selected}) => (
|
||||
<Toggle.RadioWithLabel
|
||||
label={l`Accepted conversations`}
|
||||
selected={selected}
|
||||
/>
|
||||
)}
|
||||
</Toggle.Item>
|
||||
) : (
|
||||
<Toggle.Item
|
||||
highlightRow
|
||||
label={l`People I follow`}
|
||||
name="follows">
|
||||
{({selected}) => (
|
||||
<Toggle.RadioWithLabel
|
||||
label={l`People I follow`}
|
||||
selected={selected}
|
||||
/>
|
||||
)}
|
||||
</Toggle.Item>
|
||||
)}
|
||||
<Toggle.Item
|
||||
highlightRow
|
||||
label={l`People I follow`}
|
||||
name="follows">
|
||||
{({selected}) => (
|
||||
<Toggle.RadioWithLabel
|
||||
label={l`People I follow`}
|
||||
selected={selected}
|
||||
/>
|
||||
)}
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {type NotificationSettingsPreference} from '#/state/queries/notifications/settings'
|
||||
|
||||
export function SettingPreview({
|
||||
preference,
|
||||
}: {
|
||||
preference?: NotificationSettingsPreference
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
if (!preference) {
|
||||
return null
|
||||
}
|
||||
|
||||
if ('include' in preference) {
|
||||
const list = 'list' in preference && preference.list
|
||||
|
||||
if (preference.include === 'all') {
|
||||
if (list && preference.push) return l`In-app, push, everyone`
|
||||
if (list) return l`In-app, everyone`
|
||||
if (preference.push) return l`Push, everyone`
|
||||
} else if (preference.include === 'follows') {
|
||||
if (list && preference.push) return l`In-app, push, people you follow`
|
||||
if (list) return l`In-app, people you follow`
|
||||
if (preference.push) return l`Push, people you follow`
|
||||
}
|
||||
} else {
|
||||
if (preference.list && preference.push) return l`In-app, push`
|
||||
if (preference.list) return l`In-app`
|
||||
if (preference.push) return l`Push`
|
||||
}
|
||||
|
||||
return l`Off`
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useEffect} from 'react'
|
||||
import {Linking, View} from 'react-native'
|
||||
import * as Notification from 'expo-notifications'
|
||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -10,7 +9,10 @@ import {
|
||||
type AllNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
|
||||
import {
|
||||
useChatNotificationSettingsQuery,
|
||||
useNotificationSettingsQuery,
|
||||
} from '#/state/queries/notifications/settings'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -18,11 +20,13 @@ import {NotificationSettingsDialog} from '#/components/dialogs/NotificationSetti
|
||||
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
|
||||
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
|
||||
import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
|
||||
import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
|
||||
import {
|
||||
Heart2_Stroke2_Corner0_Rounded as HeartIcon,
|
||||
LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon,
|
||||
} from '#/components/icons/Heart2'
|
||||
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
||||
import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person'
|
||||
import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote'
|
||||
import {
|
||||
@@ -33,7 +37,9 @@ import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/S
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
||||
import * as SettingsList from '../components/SettingsList'
|
||||
import {ChatNotificationDialogs} from './components/ChatNotificationDialogs'
|
||||
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
|
||||
import {SettingPreview} from './components/SettingPreview'
|
||||
|
||||
const RQKEY = ['notification-permissions']
|
||||
|
||||
@@ -42,6 +48,8 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
const {t: l} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
const {data: settings, isError} = useNotificationSettingsQuery()
|
||||
const {data: chatSettings, isError: chatError} =
|
||||
useChatNotificationSettingsQuery()
|
||||
|
||||
const likeDialogControl = Dialog.useDialogControl()
|
||||
const followDialogControl = Dialog.useDialogControl()
|
||||
@@ -52,6 +60,8 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
const activityDialogControl = Dialog.useDialogControl()
|
||||
const likeRepostDialogControl = Dialog.useDialogControl()
|
||||
const repostRepostDialogControl = Dialog.useDialogControl()
|
||||
const chatDialogControl = Dialog.useDialogControl()
|
||||
const chatRequestDialogControl = Dialog.useDialogControl()
|
||||
const miscDialogControl = Dialog.useDialogControl()
|
||||
|
||||
const {data: permissions, refetch} = useQuery({
|
||||
@@ -234,6 +244,40 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.PressableItem
|
||||
label={l`Settings for notifications for new messages`}
|
||||
onPress={chatDialogControl.open}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={MessageIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>New messages</Trans>}
|
||||
subtitleText={
|
||||
chatError ? (
|
||||
<Trans>Failed to load notification settings.</Trans>
|
||||
) : (
|
||||
<SettingPreview preference={chatSettings?.chat} />
|
||||
)
|
||||
}
|
||||
showSkeleton={!chatSettings && !chatError}
|
||||
/>
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.PressableItem
|
||||
label={l`Settings for notifications for new message requests`}
|
||||
onPress={chatRequestDialogControl.open}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={EnvelopeIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>New message requests</Trans>}
|
||||
subtitleText={
|
||||
chatError ? (
|
||||
<Trans>Failed to load notification settings.</Trans>
|
||||
) : (
|
||||
<SettingPreview preference={chatSettings?.chatRequest} />
|
||||
)
|
||||
}
|
||||
showSkeleton={!chatSettings && !chatError}
|
||||
/>
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.PressableItem
|
||||
label={l`Settings for notifications for everything else`}
|
||||
onPress={miscDialogControl.open}
|
||||
@@ -333,6 +377,10 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
<Trans>Get notifications when people repost your reposts.</Trans>
|
||||
}
|
||||
/>
|
||||
<ChatNotificationDialogs
|
||||
chatControl={chatDialogControl}
|
||||
chatRequestControl={chatRequestDialogControl}
|
||||
/>
|
||||
<NotificationSettingsDialog
|
||||
control={miscDialogControl}
|
||||
name="starterpackJoined"
|
||||
@@ -350,46 +398,3 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingPreview({
|
||||
preference,
|
||||
}: {
|
||||
preference?:
|
||||
| AppBskyNotificationDefs.Preference
|
||||
| AppBskyNotificationDefs.FilterablePreference
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
if (!preference) {
|
||||
return null
|
||||
} else {
|
||||
if ('include' in preference) {
|
||||
if (preference.include === 'all') {
|
||||
if (preference.list && preference.push) {
|
||||
return l`In-app, push, everyone`
|
||||
} else if (preference.list) {
|
||||
return l`In-app, everyone`
|
||||
} else if (preference.push) {
|
||||
return l`Push, everyone`
|
||||
}
|
||||
} else if (preference.include === 'follows') {
|
||||
if (preference.list && preference.push) {
|
||||
return l`In-app, push, people you follow`
|
||||
} else if (preference.list) {
|
||||
return l`In-app, people you follow`
|
||||
} else if (preference.push) {
|
||||
return l`Push, people you follow`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (preference.list && preference.push) {
|
||||
return l`In-app, push`
|
||||
} else if (preference.list) {
|
||||
return l`In-app`
|
||||
} else if (preference.push) {
|
||||
return l`Push`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return l`Off`
|
||||
}
|
||||
|
||||
@@ -10,13 +10,12 @@ export const RQKEY = (includeGroupChats: boolean) =>
|
||||
[RQKEY_ROOT, includeGroupChats] as const
|
||||
export const RQKEY_PARTIAL = [RQKEY_ROOT] as const
|
||||
|
||||
// the server sentinel-caps the badge counts: unreadAcceptedConvos maxes at 31
|
||||
// (meaning "more than 30") and unreadRequestConvos at 11 (meaning "more than
|
||||
// 10"). at the cap the value is no longer an exact count, so consumers must not
|
||||
// treat it as one - both the optimistic decrement and the badge display ceiling
|
||||
// key off these.
|
||||
export const UNREAD_ACCEPTED_CAP = 31
|
||||
export const UNREAD_REQUEST_CAP = 11
|
||||
// the server sentinel-caps the badge counts: unreadAcceptedConvos and
|
||||
// unreadRequestConvos max out at 100 (meaning "more than 99"). at the cap the
|
||||
// value is no longer an exact count, so consumers must not treat it as one -
|
||||
// both the optimistic decrement and the badge display ceiling key off these.
|
||||
export const UNREAD_ACCEPTED_CAP = 100
|
||||
export const UNREAD_REQUEST_CAP = 100
|
||||
|
||||
export function useUnreadCountsQuery() {
|
||||
const agent = useAgent()
|
||||
|
||||
@@ -856,18 +856,16 @@ export function useUnreadMessageCount(): {
|
||||
const request = data?.unreadRequestConvos ?? 0
|
||||
|
||||
if (accepted > 0) {
|
||||
const total = accepted + Math.min(request, 1)
|
||||
return {
|
||||
count: total,
|
||||
count: accepted,
|
||||
// accepted is sentinel-capped at UNREAD_ACCEPTED_CAP (meaning "more than
|
||||
// cap - 1"). show the "+" overflow label only when accepted is actually
|
||||
// capped - the +1 request nudge must not trip it at exactly cap - 1
|
||||
// accepted convos. otherwise clamp the number to cap - 1 so the nudge
|
||||
// never surfaces the sentinel value (31) itself
|
||||
// capped, otherwise clamp the number to cap - 1 so we never surface the
|
||||
// sentinel value (100) itself
|
||||
numUnread:
|
||||
accepted >= UNREAD_ACCEPTED_CAP
|
||||
? `${UNREAD_ACCEPTED_CAP - 1}+`
|
||||
: String(Math.min(total, UNREAD_ACCEPTED_CAP - 1)),
|
||||
: String(Math.min(accepted, UNREAD_ACCEPTED_CAP - 1)),
|
||||
// only needed when numUnread is undefined
|
||||
hasNew: false,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {
|
||||
type AppBskyNotificationDefs,
|
||||
type ChatBskyNotificationDefs,
|
||||
} from '@atproto/api'
|
||||
import {t} from '@lingui/core/macro'
|
||||
import {
|
||||
type QueryClient,
|
||||
@@ -7,12 +10,59 @@ import {
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/components/Toast'
|
||||
|
||||
const RQKEY_ROOT = 'notification-settings'
|
||||
const RQKEY = [RQKEY_ROOT]
|
||||
const RQKEY_APP = [RQKEY_ROOT, 'app']
|
||||
const RQKEY_CHAT = [RQKEY_ROOT, 'chat']
|
||||
|
||||
// App notification preferences live on the appview. Chat preferences live on a
|
||||
// separate chat service proxy that can be up or down independently, so they are
|
||||
// fetched and cached separately. This combined type names every preference for
|
||||
// the generic settings dialog, but it is never the shape of a query response.
|
||||
export type NotificationSettingsPreferences = Omit<
|
||||
AppBskyNotificationDefs.Preferences,
|
||||
'chat'
|
||||
> &
|
||||
Partial<Pick<ChatBskyNotificationDefs.Preferences, 'chat' | 'chatRequest'>>
|
||||
|
||||
export type AppNotificationSettingsPreferences = Omit<
|
||||
AppBskyNotificationDefs.Preferences,
|
||||
'chat'
|
||||
>
|
||||
|
||||
export type ChatNotificationSettingsPreferences = Pick<
|
||||
ChatBskyNotificationDefs.Preferences,
|
||||
'chat' | 'chatRequest'
|
||||
>
|
||||
|
||||
export type NotificationSettingsPreferenceName = Exclude<
|
||||
keyof NotificationSettingsPreferences,
|
||||
'$type'
|
||||
>
|
||||
|
||||
export type NotificationSettingsPreference =
|
||||
| AppBskyNotificationDefs.Preference
|
||||
| AppBskyNotificationDefs.FilterablePreference
|
||||
| ChatBskyNotificationDefs.ChatPreference
|
||||
|
||||
export function isChatPreferenceName(
|
||||
name: NotificationSettingsPreferenceName,
|
||||
): name is 'chat' | 'chatRequest' {
|
||||
return name === 'chat' || name === 'chatRequest'
|
||||
}
|
||||
|
||||
type NotificationSettingsUpdate = Partial<NotificationSettingsPreferences>
|
||||
|
||||
type AppNotificationSettingsUpdate = Partial<
|
||||
Omit<AppBskyNotificationDefs.Preferences, '$type' | 'chat'>
|
||||
>
|
||||
|
||||
type ChatNotificationSettingsUpdate =
|
||||
Partial<ChatNotificationSettingsPreferences>
|
||||
|
||||
export function useNotificationSettingsQuery({
|
||||
enabled,
|
||||
@@ -20,10 +70,27 @@ export function useNotificationSettingsQuery({
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: RQKEY,
|
||||
queryFn: async () => {
|
||||
const response = await agent.app.bsky.notification.getPreferences()
|
||||
return response.data.preferences
|
||||
queryKey: RQKEY_APP,
|
||||
queryFn: async (): Promise<AppNotificationSettingsPreferences> => {
|
||||
const res = await agent.app.bsky.notification.getPreferences()
|
||||
return appPreferencesWithoutChat(res.data.preferences)
|
||||
},
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
export function useChatNotificationSettingsQuery({
|
||||
enabled,
|
||||
}: {enabled?: boolean} = {}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: RQKEY_CHAT,
|
||||
queryFn: async (): Promise<ChatNotificationSettingsPreferences> => {
|
||||
const res = await agent.chat.bsky.notification.getPreferences(undefined, {
|
||||
headers: DM_SERVICE_HEADERS,
|
||||
})
|
||||
return chatPreferencesForSettings(res.data.preferences)
|
||||
},
|
||||
enabled,
|
||||
})
|
||||
@@ -33,19 +100,27 @@ export function useNotificationSettingsUpdateMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
update: Partial<AppBskyNotificationDefs.Preferences>,
|
||||
) => {
|
||||
const response =
|
||||
await agent.app.bsky.notification.putPreferencesV2(update)
|
||||
return response.data.preferences
|
||||
mutationFn: async (update: NotificationSettingsUpdate) => {
|
||||
const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update)
|
||||
await Promise.all([
|
||||
hasUpdates(appUpdate)
|
||||
? agent.app.bsky.notification.putPreferencesV2(appUpdate)
|
||||
: undefined,
|
||||
hasUpdates(chatUpdate)
|
||||
? agent.chat.bsky.notification.putPreferences(chatUpdate, {
|
||||
headers: DM_SERVICE_HEADERS,
|
||||
encoding: 'application/json',
|
||||
})
|
||||
: undefined,
|
||||
])
|
||||
},
|
||||
onMutate: update => {
|
||||
optimisticUpdateNotificationSettings(queryClient, update)
|
||||
},
|
||||
onError: e => {
|
||||
logger.error('Could not update notification settings', {message: e})
|
||||
void queryClient.invalidateQueries({queryKey: RQKEY})
|
||||
void queryClient.invalidateQueries({queryKey: RQKEY_APP})
|
||||
void queryClient.invalidateQueries({queryKey: RQKEY_CHAT})
|
||||
Toast.show(t`Could not update notification settings`, {
|
||||
type: 'error',
|
||||
})
|
||||
@@ -55,13 +130,62 @@ export function useNotificationSettingsUpdateMutation() {
|
||||
|
||||
function optimisticUpdateNotificationSettings(
|
||||
queryClient: QueryClient,
|
||||
update: Partial<AppBskyNotificationDefs.Preferences>,
|
||||
update: NotificationSettingsUpdate,
|
||||
) {
|
||||
queryClient.setQueryData(
|
||||
RQKEY,
|
||||
(old?: AppBskyNotificationDefs.Preferences) => {
|
||||
if (!old) return old
|
||||
return {...old, ...update}
|
||||
},
|
||||
)
|
||||
const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update)
|
||||
|
||||
if (hasUpdates(appUpdate)) {
|
||||
queryClient.setQueryData(
|
||||
RQKEY_APP,
|
||||
(old?: AppNotificationSettingsPreferences) => {
|
||||
if (!old) return old
|
||||
return {...old, ...appUpdate}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (hasUpdates(chatUpdate)) {
|
||||
queryClient.setQueryData(
|
||||
RQKEY_CHAT,
|
||||
(old?: ChatNotificationSettingsPreferences) => {
|
||||
if (!old) return old
|
||||
return {...old, ...chatUpdate}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function appPreferencesWithoutChat(
|
||||
preferences: AppBskyNotificationDefs.Preferences,
|
||||
): Omit<AppBskyNotificationDefs.Preferences, 'chat'> {
|
||||
const {chat: _ignoredChat, ...appPreferences} = preferences
|
||||
return appPreferences
|
||||
}
|
||||
|
||||
function chatPreferencesForSettings(
|
||||
preferences: ChatBskyNotificationDefs.Preferences,
|
||||
): Pick<ChatBskyNotificationDefs.Preferences, 'chat' | 'chatRequest'> {
|
||||
return {
|
||||
chat: preferences.chat,
|
||||
chatRequest: preferences.chatRequest,
|
||||
}
|
||||
}
|
||||
|
||||
function splitNotificationSettingsUpdate(update: NotificationSettingsUpdate): {
|
||||
appUpdate: AppNotificationSettingsUpdate
|
||||
chatUpdate: ChatNotificationSettingsUpdate
|
||||
} {
|
||||
const {chat, chatRequest, $type: _type, ...appUpdate} = update
|
||||
|
||||
return {
|
||||
appUpdate: appUpdate,
|
||||
chatUpdate: {
|
||||
...(chat !== undefined ? {chat} : {}),
|
||||
...(chatRequest !== undefined ? {chatRequest} : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function hasUpdates(update: object) {
|
||||
return Object.keys(update).length > 0
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
} from '#/lib/constants'
|
||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {createVideoTelemetry} from '#/lib/media/video/telemetry'
|
||||
import {mimeToExt} from '#/lib/media/video/util'
|
||||
import {useCallOnce} from '#/lib/once'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
@@ -387,6 +388,12 @@ export const ComposePost = ({
|
||||
const selectVideo = useCallback(
|
||||
(postId: string, asset: ImagePickerAsset) => {
|
||||
const abortController = new AbortController()
|
||||
const telemetry = createVideoTelemetry({
|
||||
asset,
|
||||
signal: abortController.signal,
|
||||
metric: ax.metric,
|
||||
})
|
||||
telemetry.picked()
|
||||
composerDispatch({
|
||||
type: 'update_post',
|
||||
postId: postId,
|
||||
@@ -394,6 +401,7 @@ export const ComposePost = ({
|
||||
type: 'embed_add_video',
|
||||
asset,
|
||||
abortController,
|
||||
telemetry,
|
||||
},
|
||||
})
|
||||
void processVideo(
|
||||
@@ -412,7 +420,7 @@ export const ComposePost = ({
|
||||
currentDid,
|
||||
abortController.signal,
|
||||
i18n,
|
||||
ax.metric,
|
||||
telemetry,
|
||||
)
|
||||
},
|
||||
[i18n, agent, currentDid, composerDispatch, ax.metric],
|
||||
@@ -495,6 +503,12 @@ export const ComposePost = ({
|
||||
|
||||
// Start video processing using existing flow
|
||||
const abortController = new AbortController()
|
||||
const telemetry = createVideoTelemetry({
|
||||
asset,
|
||||
signal: abortController.signal,
|
||||
metric: ax.metric,
|
||||
})
|
||||
telemetry.picked()
|
||||
composerDispatch({
|
||||
type: 'update_post',
|
||||
postId,
|
||||
@@ -502,6 +516,7 @@ export const ComposePost = ({
|
||||
type: 'embed_add_video',
|
||||
asset,
|
||||
abortController,
|
||||
telemetry,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -560,7 +575,7 @@ export const ComposePost = ({
|
||||
currentDid,
|
||||
abortController.signal,
|
||||
i18n,
|
||||
ax.metric,
|
||||
telemetry,
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('Failed to restore video from draft', {
|
||||
@@ -981,6 +996,15 @@ export const ComposePost = ({
|
||||
})
|
||||
).uris[0]
|
||||
|
||||
// Fire published event for every video that made it into the post.
|
||||
// The status guard upstream ensures each video.telemetry is present and
|
||||
// processing has completed by this point.
|
||||
for (const post of filteredThread.posts) {
|
||||
if (post.embed.media?.type === 'video') {
|
||||
post.embed.media.video.telemetry?.published()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Wait for app view to have received the post(s). If this fails, it's
|
||||
* ok, because the post _was_ actually published above.
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {type VideoTelemetry} from '#/lib/media/video/telemetry'
|
||||
import {type SelfLabel} from '#/lib/moderation'
|
||||
import {insertMentionAt} from '#/lib/strings/mention-manip'
|
||||
import {shortenLinks} from '#/lib/strings/rich-text-manip'
|
||||
@@ -88,6 +89,7 @@ export type PostAction =
|
||||
type: 'embed_add_video'
|
||||
asset: ImagePickerAsset
|
||||
abortController: AbortController
|
||||
telemetry: VideoTelemetry
|
||||
}
|
||||
| {type: 'embed_remove_video'}
|
||||
| {type: 'embed_update_video'; videoAction: VideoAction}
|
||||
@@ -458,7 +460,11 @@ function postReducer(state: PostDraft, action: PostAction): PostDraft {
|
||||
if (!prevMedia) {
|
||||
nextMedia = {
|
||||
type: 'video',
|
||||
video: createVideoState(action.asset, action.abortController),
|
||||
video: createVideoState(
|
||||
action.asset,
|
||||
action.abortController,
|
||||
action.telemetry,
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
UploadLimitError,
|
||||
VideoTooLargeError,
|
||||
} from '#/lib/media/video/errors'
|
||||
import {type VideoTelemetry} from '#/lib/media/video/telemetry'
|
||||
import {type CompressedVideo} from '#/lib/media/video/types'
|
||||
import {uploadVideo} from '#/lib/media/video/upload'
|
||||
import {createVideoAgent} from '#/lib/media/video/util'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {type AnalyticsContextType} from '#/analytics'
|
||||
|
||||
type CaptionsTrack = {lang: string; file: File}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const NO_VIDEO = Object.freeze({
|
||||
video: undefined,
|
||||
jobId: undefined,
|
||||
pendingPublish: undefined,
|
||||
telemetry: undefined,
|
||||
altText: '',
|
||||
captions: [],
|
||||
})
|
||||
@@ -80,6 +81,7 @@ type ErrorState = {
|
||||
jobId: string | null
|
||||
error: string
|
||||
pendingPublish?: undefined
|
||||
telemetry: VideoTelemetry
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
@@ -92,6 +94,7 @@ type CompressingState = {
|
||||
video?: undefined
|
||||
jobId?: undefined
|
||||
pendingPublish?: undefined
|
||||
telemetry: VideoTelemetry
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
@@ -104,6 +107,7 @@ type UploadingState = {
|
||||
video: CompressedVideo
|
||||
jobId?: undefined
|
||||
pendingPublish?: undefined
|
||||
telemetry: VideoTelemetry
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
@@ -117,6 +121,7 @@ type ProcessingState = {
|
||||
jobId: string
|
||||
jobStatus: AppBskyVideoDefs.JobStatus | null
|
||||
pendingPublish?: undefined
|
||||
telemetry: VideoTelemetry
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
@@ -129,6 +134,7 @@ type DoneState = {
|
||||
video: CompressedVideo
|
||||
jobId?: undefined
|
||||
pendingPublish: {blobRef: BlobRef}
|
||||
telemetry: VideoTelemetry
|
||||
altText: string
|
||||
captions: CaptionsTrack[]
|
||||
}
|
||||
@@ -143,12 +149,14 @@ export type VideoState =
|
||||
export function createVideoState(
|
||||
asset: ImagePickerAsset,
|
||||
abortController: AbortController,
|
||||
telemetry: VideoTelemetry,
|
||||
): CompressingState {
|
||||
return {
|
||||
status: 'compressing',
|
||||
progress: 0,
|
||||
abortController,
|
||||
asset,
|
||||
telemetry,
|
||||
altText: '',
|
||||
captions: [],
|
||||
}
|
||||
@@ -171,6 +179,7 @@ export function videoReducer(
|
||||
asset: state.asset ?? null,
|
||||
video: state.video ?? null,
|
||||
jobId: state.jobId ?? null,
|
||||
telemetry: state.telemetry,
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
@@ -199,6 +208,7 @@ export function videoReducer(
|
||||
abortController: state.abortController,
|
||||
asset: state.asset,
|
||||
video: action.video,
|
||||
telemetry: state.telemetry,
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
@@ -214,6 +224,7 @@ export function videoReducer(
|
||||
video: state.video,
|
||||
jobId: action.jobId,
|
||||
jobStatus: null,
|
||||
telemetry: state.telemetry,
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
@@ -240,6 +251,7 @@ export function videoReducer(
|
||||
pendingPublish: {
|
||||
blobRef: action.blobRef,
|
||||
},
|
||||
telemetry: state.telemetry,
|
||||
altText: state.altText,
|
||||
captions: state.captions,
|
||||
}
|
||||
@@ -266,40 +278,21 @@ export async function processVideo(
|
||||
did: string,
|
||||
signal: AbortSignal,
|
||||
i18n: I18n,
|
||||
metric: AnalyticsContextType['metric'],
|
||||
telemetry: VideoTelemetry,
|
||||
) {
|
||||
let video: CompressedVideo | undefined
|
||||
try {
|
||||
telemetry.compressStarted()
|
||||
video = await compressVideo(asset, {
|
||||
onProgress: num => {
|
||||
dispatch({type: 'update_progress', progress: trunc2dp(num), signal})
|
||||
},
|
||||
signal,
|
||||
onProbe: (metadata, wouldCompress) => {
|
||||
metric('composer:video:probe', {
|
||||
mimeType: metadata.mimeType,
|
||||
codec: metadata.codec,
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
duration: metadata.duration,
|
||||
bitrate: metadata.bitrate,
|
||||
fileSize: metadata.fileSize,
|
||||
hasAudio: metadata.hasAudio,
|
||||
frameRate: metadata.frameRate,
|
||||
rotation: metadata.rotation,
|
||||
isHDR: metadata.isHDR,
|
||||
wouldCompress,
|
||||
})
|
||||
},
|
||||
onProbeFailed: e => {
|
||||
metric('composer:video:probeFailed', {
|
||||
safeMessage: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
const message = getCompressErrorMessage(e, i18n)
|
||||
if (message !== null) {
|
||||
telemetry.compressFailed(e)
|
||||
dispatch({
|
||||
type: 'to_error',
|
||||
error: message,
|
||||
@@ -308,6 +301,15 @@ export async function processVideo(
|
||||
}
|
||||
return
|
||||
}
|
||||
if (video.passthroughReason) {
|
||||
telemetry.compressSkipped({
|
||||
size: video.size,
|
||||
mimeType: video.mimeType,
|
||||
skipReason: video.passthroughReason,
|
||||
})
|
||||
} else {
|
||||
telemetry.compressCompleted({size: video.size, mimeType: video.mimeType})
|
||||
}
|
||||
dispatch({
|
||||
type: 'compressing_to_uploading',
|
||||
video,
|
||||
@@ -316,6 +318,7 @@ export async function processVideo(
|
||||
|
||||
let uploadResponse: AppBskyVideoDefs.JobStatus | undefined
|
||||
try {
|
||||
telemetry.uploadStarted(video.size)
|
||||
uploadResponse = await uploadVideo({
|
||||
video,
|
||||
agent,
|
||||
@@ -329,6 +332,7 @@ export async function processVideo(
|
||||
} catch (e) {
|
||||
const message = getUploadErrorMessage(e, i18n)
|
||||
if (message !== null) {
|
||||
telemetry.uploadFailed(e)
|
||||
dispatch({
|
||||
type: 'to_error',
|
||||
error: message,
|
||||
@@ -339,6 +343,8 @@ export async function processVideo(
|
||||
}
|
||||
|
||||
const jobId = uploadResponse.jobId
|
||||
telemetry.uploadCompleted(jobId)
|
||||
telemetry.processingStarted(jobId)
|
||||
dispatch({
|
||||
type: 'uploading_to_processing',
|
||||
jobId,
|
||||
@@ -377,6 +383,7 @@ export async function processVideo(
|
||||
}
|
||||
|
||||
logger.error('Error processing video', {safeMessage: e})
|
||||
telemetry.processingFailed(e)
|
||||
dispatch({
|
||||
type: 'to_error',
|
||||
error: i18n._(msg`Video failed to process`),
|
||||
@@ -386,6 +393,7 @@ export async function processVideo(
|
||||
}
|
||||
|
||||
if (blob) {
|
||||
telemetry.processingCompleted()
|
||||
dispatch({
|
||||
type: 'to_done',
|
||||
blobRef: blob,
|
||||
|
||||
@@ -242,12 +242,18 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
||||
accessibilityLabel={l`Chat`}
|
||||
accessibilityHint={
|
||||
!aa.flags.chatDisabled && numUnreadMessages.count > 0
|
||||
? l({
|
||||
message: plural(numUnreadMessages.numUnread ?? 0, {
|
||||
one: '# unread item',
|
||||
other: '# unread items',
|
||||
}),
|
||||
})
|
||||
? numUnreadMessages.numUnread?.includes('+')
|
||||
? l({
|
||||
message: `${numUnreadMessages.numUnread} unread items`,
|
||||
comment:
|
||||
'Accessibility hint for the bottom bar chat icon when the number of unread messages exceeds the cap, with the + symbol already included – for example, 99+ unread items',
|
||||
})
|
||||
: l({
|
||||
message: plural(numUnreadMessages.numUnread ?? 0, {
|
||||
one: '# unread item',
|
||||
other: '# unread items',
|
||||
}),
|
||||
})
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -27,6 +27,10 @@ export function DesktopSearch() {
|
||||
if (query.length) setActive(true)
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
setActive(false)
|
||||
}
|
||||
|
||||
const onChangeText = (text: string) => {
|
||||
setQuery(text)
|
||||
if (!active) {
|
||||
@@ -64,6 +68,7 @@ export function DesktopSearch() {
|
||||
hotkey
|
||||
value={query}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onClearText}
|
||||
onSubmitEditing={onSubmit}
|
||||
|
||||
Reference in New Issue
Block a user