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