Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd0323c113 | |||
| cd820709b6 | |||
| c894dda0bf | |||
| c634cd9071 | |||
| 3c92714e4e | |||
| 25baf1f661 | |||
| dcbcd1bb80 | |||
| 92ee6260c1 | |||
| 9c7b330f92 | |||
| bc95c0e50c | |||
| aeafb14fb4 | |||
| 4c75b568df | |||
| 9702b210cc | |||
| d53fdb0dda | |||
| 9be3b0794c | |||
| 77db98de65 | |||
| 51babe0164 | |||
| 04a2a227fc | |||
| 42510c0515 | |||
| f36776a91e | |||
| eb18b6a49a | |||
| 3eaa2d9ddf | |||
| 066fd8cbf9 | |||
| 06ebd2964a | |||
| 3304cd0424 | |||
| 32ba17a4f1 | |||
| f83335f110 | |||
| 4da86e5864 | |||
| dd86402763 | |||
| 13d21692bf | |||
| e672f43ec8 | |||
| afd3d2829f | |||
| 619fa0d0bb | |||
| 9cf457acf8 | |||
| f1f9ca9606 |
@@ -52,10 +52,6 @@ jobs:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: "Use upgraded MMKV for Fabric"
|
||||
run: |
|
||||
sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' filename.txt
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
|
||||
@@ -275,10 +275,6 @@ jobs:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: "Use upgraded MMKV for Fabric"
|
||||
run: |
|
||||
sed -i 's/"react-native-mmkv": "\^2\.12\.2"/"react-native-mmkv": "^3.3.0"/' filename.txt
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
|
||||
+3
-3
@@ -29,8 +29,8 @@ module.exports = function (_config) {
|
||||
const UPDATES_CHANNEL = IS_TESTFLIGHT
|
||||
? 'testflight'
|
||||
: IS_PRODUCTION
|
||||
? 'production'
|
||||
: undefined
|
||||
? 'production'
|
||||
: undefined
|
||||
const UPDATES_ENABLED = !!UPDATES_CHANNEL
|
||||
|
||||
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||
@@ -219,7 +219,7 @@ module.exports = function (_config) {
|
||||
compileSdkVersion: 35,
|
||||
targetSdkVersion: 35,
|
||||
buildToolsVersion: '35.0.0',
|
||||
newArchEnabled: true,
|
||||
newArchEnabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: ['variant', [
|
||||
'&:is(.dark *):not(:is(.dark .light *))',
|
||||
]],
|
||||
darkMode: ['variant', ['&:is(.dark *):not(:is(.dark .light *))']],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import assert from 'assert'
|
||||
import {
|
||||
Kysely,
|
||||
KyselyPlugin,
|
||||
type KyselyPlugin,
|
||||
Migrator,
|
||||
PluginTransformQueryArgs,
|
||||
PluginTransformResultArgs,
|
||||
type PluginTransformQueryArgs,
|
||||
type PluginTransformResultArgs,
|
||||
PostgresDialect,
|
||||
QueryResult,
|
||||
RootOperationNode,
|
||||
UnknownRow,
|
||||
type QueryResult,
|
||||
type RootOperationNode,
|
||||
type UnknownRow,
|
||||
} from 'kysely'
|
||||
import {default as Pg} from 'pg'
|
||||
|
||||
import {dbLogger as log} from '../logger.js'
|
||||
import {default as migrations} from './migrations/index.js'
|
||||
import {DbMigrationProvider} from './migrations/provider.js'
|
||||
import {DbSchema} from './schema.js'
|
||||
import {type DbSchema} from './schema.js'
|
||||
|
||||
export class Database {
|
||||
migrator: Migrator
|
||||
destroyed = false
|
||||
|
||||
constructor(public db: Kysely<DbSchema>, public cfg: PgConfig) {
|
||||
constructor(
|
||||
public db: Kysely<DbSchema>,
|
||||
public cfg: PgConfig,
|
||||
) {
|
||||
this.migrator = new Migrator({
|
||||
db,
|
||||
migrationTableSchema: cfg.schema,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import events from 'node:events'
|
||||
import http from 'node:http'
|
||||
import type http from 'node:http'
|
||||
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import {createHttpTerminator, HttpTerminator} from 'http-terminator'
|
||||
import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
|
||||
|
||||
import {Config} from './config.js'
|
||||
import {type Config} from './config.js'
|
||||
import {AppContext} from './context.js'
|
||||
import {default as routes, errorHandler} from './routes/index.js'
|
||||
|
||||
@@ -17,7 +17,10 @@ export class LinkService {
|
||||
public server?: http.Server
|
||||
private terminator?: HttpTerminator
|
||||
|
||||
constructor(public app: express.Application, public ctx: AppContext) {}
|
||||
constructor(
|
||||
public app: express.Application,
|
||||
public ctx: AppContext,
|
||||
) {}
|
||||
|
||||
static async create(cfg: Config): Promise<LinkService> {
|
||||
let app = express()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import events from 'node:events'
|
||||
import http from 'node:http'
|
||||
import type http from 'node:http'
|
||||
|
||||
import express from 'express'
|
||||
import {createHttpTerminator, HttpTerminator} from 'http-terminator'
|
||||
import {createHttpTerminator, type HttpTerminator} from 'http-terminator'
|
||||
|
||||
import {Config} from './config.js'
|
||||
import {type Config} from './config.js'
|
||||
import {AppContext} from './context.js'
|
||||
import {default as routes, errorHandler} from './routes/index.js'
|
||||
|
||||
@@ -15,7 +15,10 @@ export class CardService {
|
||||
public server?: http.Server
|
||||
private terminator?: HttpTerminator
|
||||
|
||||
constructor(public app: express.Application, public ctx: AppContext) {}
|
||||
constructor(
|
||||
public app: express.Application,
|
||||
public ctx: AppContext,
|
||||
) {}
|
||||
|
||||
static async create(cfg: Config): Promise<CardService> {
|
||||
let app = express()
|
||||
|
||||
@@ -105,16 +105,14 @@ const text = t`Hello World`;
|
||||
```
|
||||
|
||||
We can then run `yarn intl:extract` to update the catalog in `src/locale/locales/{locale}/messages.po`. This will add the new string to the catalog.
|
||||
We can then run `yarn intl:compile` to update the translation files in `src/locale/locales/{locale}/messages.js`. This will add the new string to the translation files.
|
||||
The configuration for translations is defined in `lingui.config.js`
|
||||
|
||||
So the workflow is as follows:
|
||||
1. Wrap messages in Trans macro
|
||||
2. Run `yarn intl:extract` command to generate message catalogs
|
||||
3. Translate message catalogs (send them to translators usually)
|
||||
4. Run `yarn intl:compile` to create runtime catalogs
|
||||
5. Load runtime catalog
|
||||
6. Enjoy translated app!
|
||||
4. Load runtime catalog
|
||||
5. Enjoy translated app!
|
||||
|
||||
### Common pitfalls
|
||||
|
||||
|
||||
@@ -64,4 +64,10 @@ cfg.transformer.getTransformOptions = async () => ({
|
||||
},
|
||||
})
|
||||
|
||||
// po support
|
||||
cfg.transformer.babelTransformerPath = require.resolve(
|
||||
'@lingui/metro-transformer/expo',
|
||||
)
|
||||
cfg.resolver.sourceExts = [...cfg.resolver.sourceExts, 'po', 'pot']
|
||||
|
||||
module.exports = cfg
|
||||
|
||||
+15
@@ -15,6 +15,8 @@ class BackgroundNotificationHandler(
|
||||
|
||||
if (remoteMessage.data["reason"] == "chat-message") {
|
||||
mutateWithChatMessage(remoteMessage)
|
||||
} else {
|
||||
mutateWithOtherReason(remoteMessage)
|
||||
}
|
||||
|
||||
notifInterface.showMessage(remoteMessage)
|
||||
@@ -39,4 +41,17 @@ class BackgroundNotificationHandler(
|
||||
// TODO - Remove this once we have more backend capability
|
||||
remoteMessage.data["badge"] = null
|
||||
}
|
||||
|
||||
private fun mutateWithOtherReason(remoteMessage: RemoteMessage) {
|
||||
// If oreo or higher
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// If one of "like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost"
|
||||
// assign to it's eponymous channel. otherwise do nothing, let expo handle it
|
||||
when (remoteMessage.data["reason"]) {
|
||||
"like", "repost", "follow", "mention", "reply", "quote", "like-via-repost", "repost-via-repost" -> {
|
||||
remoteMessage.data["channelId"] = remoteMessage.data["reason"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const EmojiPicker = ({onEmojiSelected}: EmojiPickerViewProps) => {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: scheme === 'dark' ? '#000' : '#fff',
|
||||
} as const),
|
||||
}) as const,
|
||||
[scheme],
|
||||
)
|
||||
|
||||
|
||||
+8
-7
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "is-ci || husky install",
|
||||
"postinstall": "patch-package && yarn intl:compile",
|
||||
"postinstall": "patch-package",
|
||||
"prebuild": "expo prebuild --clean",
|
||||
"android": "expo run:android",
|
||||
"android:prod": "expo run:android --variant release",
|
||||
@@ -53,10 +53,9 @@
|
||||
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
|
||||
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
|
||||
"perf:measure": "NODE_ENV=test flashlight measure",
|
||||
"intl:build": "yarn intl:extract:all && yarn intl:compile",
|
||||
"intl:build": "yarn intl:extract:all",
|
||||
"intl:extract": "lingui extract --clean --locale en",
|
||||
"intl:extract:all": "lingui extract --clean",
|
||||
"intl:compile": "lingui compile",
|
||||
"intl:pull": "crowdin download translations --verbose -b main",
|
||||
"intl:push": "crowdin push translations --verbose -b main",
|
||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
||||
@@ -69,7 +68,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.15.15",
|
||||
"@atproto/api": "^0.15.16",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -190,7 +189,7 @@
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.17.1",
|
||||
"react-native-keyboard-controller": "^1.17.5",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "^6.7.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
@@ -218,13 +217,15 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.142",
|
||||
"@atproto/dev-env": "^0.3.144",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@expo/config-plugins": "~10.0.2",
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/loader": "^5.3.2",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@lingui/metro-transformer": "^5.3.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/eslint-config": "^0.79.3",
|
||||
@@ -265,7 +266,7 @@
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
"metro-react-native-babel-preset": "^0.77.0",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier": "^3.6.0",
|
||||
"react-native-dotenv": "^3.4.11",
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
|
||||
@@ -9,10 +9,9 @@ const templateFile = path.join(
|
||||
'scripts.html',
|
||||
)
|
||||
|
||||
const {entrypoints} = require(path.join(
|
||||
projectRoot,
|
||||
'web-build/asset-manifest.json',
|
||||
))
|
||||
const {entrypoints} = require(
|
||||
path.join(projectRoot, 'web-build/asset-manifest.json'),
|
||||
)
|
||||
|
||||
console.log(`Found ${entrypoints.length} entrypoints`)
|
||||
console.log(`Writing ${templateFile}`)
|
||||
|
||||
+2
-2
@@ -447,7 +447,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
name="LikesOnRepostsNotificationSettings"
|
||||
getComponent={() => LikesOnRepostsNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Likes on your reposts notifications`),
|
||||
title: title(msg`Likes of your reposts notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
@@ -455,7 +455,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
name="RepostsOnRepostsNotificationSettings"
|
||||
getComponent={() => RepostsOnRepostsNotificationSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Reposts on your reposts notifications`),
|
||||
title: title(msg`Reposts of your reposts notifications`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -190,7 +190,7 @@ export function Root({children}: {children: React.ReactNode}) {
|
||||
if (item) playHaptic('Light')
|
||||
setHoveredMenuItem(item)
|
||||
},
|
||||
} satisfies ContextType),
|
||||
}) satisfies ContextType,
|
||||
[
|
||||
measurement,
|
||||
setMeasurement,
|
||||
@@ -710,8 +710,8 @@ export function Item({
|
||||
const xOffset = position
|
||||
? position.x
|
||||
: align === 'left'
|
||||
? measurement.x
|
||||
: measurement.x + measurement.width - layout.width
|
||||
? measurement.x
|
||||
: measurement.x + measurement.width - layout.width
|
||||
|
||||
registerHoverable(
|
||||
id,
|
||||
|
||||
+57
-22
@@ -9,7 +9,7 @@ import {
|
||||
import {BSKY_DOWNLOAD_URL} from '#/lib/constants'
|
||||
import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {type AllNavigatorParams} from '#/lib/routes/types'
|
||||
import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {
|
||||
convertBskyAppUrlIfNeeded,
|
||||
@@ -24,6 +24,7 @@ import {Button, type ButtonProps} from '#/components/Button'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Text, type TextProps} from '#/components/Typography'
|
||||
import {router} from '#/routes'
|
||||
import {useGlobalDialogsControlContext} from './dialogs/Context'
|
||||
|
||||
/**
|
||||
* Only available within a `Link`, since that inherits from `Button`.
|
||||
@@ -98,10 +99,10 @@ export function useLink({
|
||||
return typeof to === 'string'
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to))
|
||||
: to.screen
|
||||
? router.matchName(to.screen)?.build(to.params)
|
||||
: to.href
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href))
|
||||
: undefined
|
||||
? router.matchName(to.screen)?.build(to.params)
|
||||
: to.href
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href))
|
||||
: undefined
|
||||
}, [to])
|
||||
|
||||
if (!href) {
|
||||
@@ -111,7 +112,8 @@ export function useLink({
|
||||
}
|
||||
|
||||
const isExternal = isExternalUrl(href)
|
||||
const {openModal, closeModal} = useModalControls()
|
||||
const {closeModal} = useModalControls()
|
||||
const {linkWarningDialogControl} = useGlobalDialogsControlContext()
|
||||
const openLink = useOpenLink()
|
||||
|
||||
const onPress = React.useCallback(
|
||||
@@ -132,10 +134,9 @@ export function useLink({
|
||||
}
|
||||
|
||||
if (requiresWarning) {
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: displayText,
|
||||
href: href,
|
||||
linkWarningDialogControl.open({
|
||||
displayText,
|
||||
href,
|
||||
})
|
||||
} else {
|
||||
if (isExternal) {
|
||||
@@ -154,15 +155,44 @@ export function useLink({
|
||||
} else {
|
||||
closeModal() // close any active modals
|
||||
|
||||
const [screen, params] = router.matchPath(href) as [
|
||||
screen: keyof AllNavigatorParams,
|
||||
params?: RouteParams,
|
||||
]
|
||||
|
||||
// does not apply to web's flat navigator
|
||||
if (isNative && screen !== 'NotFound') {
|
||||
const state = navigation.getState()
|
||||
// if screen is not in the current navigator, it means it's
|
||||
// most likely a tab screen
|
||||
if (!state.routeNames.includes(screen)) {
|
||||
const parent = navigation.getParent()
|
||||
if (
|
||||
parent &&
|
||||
parent.getState().routeNames.includes(`${screen}Tab`)
|
||||
) {
|
||||
// yep, it's a tab screen. i.e. SearchTab
|
||||
// thus we need to navigate to the child screen
|
||||
// via the parent navigator
|
||||
// see https://reactnavigation.org/docs/upgrading-from-6.x/#changes-to-the-navigate-action
|
||||
// TODO: can we support the other kinds of actions? push/replace -sfn
|
||||
|
||||
// @ts-expect-error include does not narrow the type unfortunately
|
||||
parent.navigate(`${screen}Tab`, {screen, params})
|
||||
return
|
||||
} else {
|
||||
// will probably fail, but let's try anyway
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'push') {
|
||||
navigation.dispatch(StackActions.push(...router.matchPath(href)))
|
||||
navigation.dispatch(StackActions.push(screen, params))
|
||||
} else if (action === 'replace') {
|
||||
navigation.dispatch(
|
||||
StackActions.replace(...router.matchPath(href)),
|
||||
)
|
||||
navigation.dispatch(StackActions.replace(screen, params))
|
||||
} else if (action === 'navigate') {
|
||||
// @ts-ignore
|
||||
navigation.navigate(...router.matchPath(href))
|
||||
// @ts-expect-error not typed
|
||||
navigation.navigate(screen, params)
|
||||
} else {
|
||||
throw Error('Unsupported navigator action.')
|
||||
}
|
||||
@@ -176,13 +206,13 @@ export function useLink({
|
||||
displayText,
|
||||
isExternal,
|
||||
href,
|
||||
openModal,
|
||||
openLink,
|
||||
closeModal,
|
||||
action,
|
||||
navigation,
|
||||
overridePresentation,
|
||||
shouldProxy,
|
||||
linkWarningDialogControl,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -195,16 +225,21 @@ export function useLink({
|
||||
)
|
||||
|
||||
if (requiresWarning) {
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: displayText,
|
||||
href: href,
|
||||
linkWarningDialogControl.open({
|
||||
displayText,
|
||||
href,
|
||||
share: true,
|
||||
})
|
||||
} else {
|
||||
shareUrl(href)
|
||||
}
|
||||
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
|
||||
}, [
|
||||
disableMismatchWarning,
|
||||
displayText,
|
||||
href,
|
||||
isExternal,
|
||||
linkWarningDialogControl,
|
||||
])
|
||||
|
||||
const onLongPress = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
|
||||
@@ -77,9 +77,9 @@ export function ImageEmbed({
|
||||
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
||||
? 'none'
|
||||
: rest.viewContext ===
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
? 'square'
|
||||
: 'constrained'
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
? 'square'
|
||||
: 'constrained'
|
||||
}
|
||||
image={image}
|
||||
onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])}
|
||||
|
||||
@@ -317,8 +317,8 @@ export function Controls({
|
||||
!focused
|
||||
? msg`Unmute video`
|
||||
: playing
|
||||
? msg`Pause video`
|
||||
: msg`Play video`,
|
||||
? msg`Pause video`
|
||||
: msg`Play video`,
|
||||
)}
|
||||
accessibilityHint=""
|
||||
style={[
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {type RefObject, useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {isSafari} from '#/lib/browser'
|
||||
import {logger} from '#/logger'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
|
||||
export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||
@@ -79,7 +80,12 @@ export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||
await ref.current.play()
|
||||
} catch (e: any) {
|
||||
if (
|
||||
!e.message?.includes(`The request is not allowed by the user agent`)
|
||||
!e.message?.includes(
|
||||
`The request is not allowed by the user agent`,
|
||||
) &&
|
||||
!e.message?.includes(
|
||||
`The play() request was interrupted by a call to pause()`,
|
||||
)
|
||||
) {
|
||||
throw e
|
||||
}
|
||||
@@ -176,8 +182,15 @@ export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||
} else {
|
||||
const promise = ref.current.play()
|
||||
if (promise !== undefined) {
|
||||
promise.catch(err => {
|
||||
console.error('Error playing video:', err)
|
||||
promise.catch((err: any) => {
|
||||
if (
|
||||
// ignore this common error. it's fine
|
||||
!err.message?.includes(
|
||||
`The play() request was interrupted by a call to pause()`,
|
||||
)
|
||||
) {
|
||||
logger.error('Error playing video:', {message: err})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,8 +600,8 @@ let PostMenuItems = ({
|
||||
isDetachPending
|
||||
? Loader
|
||||
: quoteEmbed.isDetached
|
||||
? Eye
|
||||
: EyeSlash
|
||||
? Eye
|
||||
: EyeSlash
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ProfileHoverCard(props: ProfileHoverCardProps) {
|
||||
return props.children
|
||||
} else {
|
||||
return (
|
||||
<View onPointerMove={onPointerMove} style={[a.flex_shrink]}>
|
||||
<View onPointerMove={onPointerMove} style={[a.flex_shrink, props.style]}>
|
||||
<ProfileHoverCardInner {...props} />
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type React from 'react'
|
||||
|
||||
export type ProfileHoverCardProps = {
|
||||
children: React.ReactElement
|
||||
import {type ViewStyleProp} from '#/alf'
|
||||
|
||||
export type ProfileHoverCardProps = ViewStyleProp & {
|
||||
children: React.ReactNode
|
||||
did: string
|
||||
disable?: boolean
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ export function Trigger({children, label}: TriggerProps) {
|
||||
borderColor: focused
|
||||
? t.palette.primary_500
|
||||
: hovered
|
||||
? t.palette.contrast_100
|
||||
: t.palette.contrast_25,
|
||||
? t.palette.contrast_100
|
||||
: t.palette.contrast_25,
|
||||
},
|
||||
])}>
|
||||
{children}
|
||||
@@ -244,6 +244,7 @@ export function Item({ref, value, style, children}: ItemProps) {
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={flatten([
|
||||
t.atoms.text,
|
||||
a.relative,
|
||||
a.flex,
|
||||
{minHeight: 25, paddingLeft: 30, paddingRight: 35},
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import React from 'react'
|
||||
import {Keyboard, Platform, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
Keyboard,
|
||||
Platform,
|
||||
type StyleProp,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyGraphDefs,
|
||||
type AppBskyGraphDefs,
|
||||
AtUri,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
@@ -13,7 +19,7 @@ import {HITSLOP_10} from '#/lib/constants'
|
||||
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {
|
||||
ThreadgateAllowUISetting,
|
||||
type ThreadgateAllowUISetting,
|
||||
threadgateViewToAllowUISetting,
|
||||
} from '#/state/queries/threadgate'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -70,8 +76,8 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
||||
const description = anyoneCanReply
|
||||
? _(msg`Everybody can reply`)
|
||||
: noOneCanReply
|
||||
? _(msg`Replies disabled`)
|
||||
: _(msg`Some people can reply`)
|
||||
? _(msg`Replies disabled`)
|
||||
: _(msg`Some people can reply`)
|
||||
|
||||
const onPressOpen = () => {
|
||||
if (isNative && Keyboard.isVisible()) {
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {atoms as a, useBreakpoints, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ChangeEmailDialog({
|
||||
control,
|
||||
verifyEmailControl,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
verifyEmailControl: Dialog.DialogControlProps
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
<Inner verifyEmailControl={verifyEmailControl} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
export function Inner({
|
||||
verifyEmailControl,
|
||||
}: {
|
||||
verifyEmailControl: Dialog.DialogControlProps
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const agent = useAgent()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<
|
||||
'StepOne' | 'StepTwo' | 'StepThree'
|
||||
>('StepOne')
|
||||
const [email, setEmail] = useState('')
|
||||
const [confirmationCode, setConfirmationCode] = useState('')
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const currentEmail = currentAccount?.email || '(no email)'
|
||||
const uiStrings = {
|
||||
StepOne: {
|
||||
title: _(msg`Change Your Email`),
|
||||
message: '',
|
||||
},
|
||||
StepTwo: {
|
||||
title: _(msg`Security Step Required`),
|
||||
message: _(
|
||||
msg`An email has been sent to your previous address, ${currentEmail}. It includes a confirmation code which you can enter below.`,
|
||||
),
|
||||
},
|
||||
StepThree: {
|
||||
title: _(msg`Email Updated!`),
|
||||
message: _(
|
||||
msg`Your email address has been updated but it is not yet verified. As a next step, please verify your new email.`,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const onRequestChange = async () => {
|
||||
if (email === currentAccount?.email) {
|
||||
setError(
|
||||
_(
|
||||
msg`The email address you entered is the same as your current email address.`,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
const res = await agent.com.atproto.server.requestEmailUpdate()
|
||||
if (res.data.tokenRequired) {
|
||||
setCurrentStep('StepTwo')
|
||||
} else {
|
||||
await agent.com.atproto.server.updateEmail({email: email.trim()})
|
||||
await agent.resumeSession(agent.session!)
|
||||
setCurrentStep('StepThree')
|
||||
}
|
||||
} catch (e) {
|
||||
setError(cleanError(String(e)))
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirm = async () => {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await agent.com.atproto.server.updateEmail({
|
||||
email: email.trim(),
|
||||
token: confirmationCode.trim(),
|
||||
})
|
||||
await agent.resumeSession(agent.session!)
|
||||
setCurrentStep('StepThree')
|
||||
} catch (e) {
|
||||
setError(cleanError(String(e)))
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onVerify = async () => {
|
||||
control.close(() => {
|
||||
verifyEmailControl.open()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Verify email dialog`)}
|
||||
style={web({maxWidth: 450})}>
|
||||
<Dialog.Close />
|
||||
<View style={[a.gap_xl]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.font_heavy, a.text_2xl]}>
|
||||
{uiStrings[currentStep].title}
|
||||
</Text>
|
||||
{error ? (
|
||||
<View style={[a.rounded_sm, a.overflow_hidden]}>
|
||||
<ErrorMessage message={error} />
|
||||
</View>
|
||||
) : null}
|
||||
{currentStep === 'StepOne' ? (
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<Trans>Enter your new email address below.</Trans>
|
||||
</TextField.LabelText>
|
||||
<TextField.Root>
|
||||
<TextField.Input
|
||||
label={_(msg`New email address`)}
|
||||
placeholder={_(msg`alice@example.com`)}
|
||||
defaultValue={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoComplete="email"
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
{uiStrings[currentStep].message}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{currentStep === 'StepTwo' ? (
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<Trans>Confirmation code</Trans>
|
||||
</TextField.LabelText>
|
||||
<TextField.Root>
|
||||
<TextField.Input
|
||||
label={_(msg`Confirmation code`)}
|
||||
placeholder="XXXXX-XXXXX"
|
||||
onChangeText={setConfirmationCode}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}>
|
||||
{currentStep === 'StepOne' ? (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Request change`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={onRequestChange}>
|
||||
<ButtonText>
|
||||
<Trans>Request change</Trans>
|
||||
</ButtonText>
|
||||
{isProcessing ? (
|
||||
<Loader size="sm" style={[{color: 'white'}]} />
|
||||
) : null}
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`I have a code`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={() => setCurrentStep('StepTwo')}>
|
||||
<ButtonText>
|
||||
<Trans>I have a code</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
) : currentStep === 'StepTwo' ? (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Confirm`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={onConfirm}>
|
||||
<ButtonText>
|
||||
<Trans>Confirm</Trans>
|
||||
</ButtonText>
|
||||
{isProcessing ? (
|
||||
<Loader size="sm" style={[{color: 'white'}]} />
|
||||
) : null}
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Resend email`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={() => {
|
||||
setConfirmationCode('')
|
||||
setCurrentStep('StepOne')
|
||||
}}>
|
||||
<ButtonText>
|
||||
<Trans>Resend email</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
) : currentStep === 'StepThree' ? (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Verify email`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
onPress={onVerify}>
|
||||
<ButtonText>
|
||||
<Trans>Verify email</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="large"
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,11 @@ type ControlsContext = {
|
||||
signinDialogControl: Control
|
||||
inAppBrowserConsentControl: StatefulControl<string>
|
||||
emailDialogControl: StatefulControl<Screen>
|
||||
linkWarningDialogControl: StatefulControl<{
|
||||
href: string
|
||||
displayText: string
|
||||
share?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
const ControlsContext = createContext<ControlsContext | null>(null)
|
||||
@@ -36,6 +41,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const signinDialogControl = Dialog.useDialogControl()
|
||||
const inAppBrowserConsentControl = useStatefulDialogControl<string>()
|
||||
const emailDialogControl = useStatefulDialogControl<Screen>()
|
||||
const linkWarningDialogControl = useStatefulDialogControl<{
|
||||
href: string
|
||||
displayText: string
|
||||
share?: boolean
|
||||
}>()
|
||||
|
||||
const ctx = useMemo<ControlsContext>(
|
||||
() => ({
|
||||
@@ -43,12 +53,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
signinDialogControl,
|
||||
inAppBrowserConsentControl,
|
||||
emailDialogControl,
|
||||
linkWarningDialogControl,
|
||||
}),
|
||||
[
|
||||
mutedWordsDialogControl,
|
||||
signinDialogControl,
|
||||
inAppBrowserConsentControl,
|
||||
emailDialogControl,
|
||||
linkWarningDialogControl,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -185,8 +185,8 @@ export function Disable() {
|
||||
state.emailStatus === 'pending'
|
||||
? Loader
|
||||
: state.emailStatus === 'success'
|
||||
? Check
|
||||
: Envelope
|
||||
? Check
|
||||
: Envelope
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -116,8 +116,8 @@ export function Enable() {
|
||||
state.status === 'pending'
|
||||
? Loader
|
||||
: state.status === 'success'
|
||||
? Check
|
||||
: ShieldIcon
|
||||
? Check
|
||||
: ShieldIcon
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {isPossiblyAUrl, splitApexDomain} from '#/lib/strings/url-helpers'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useGlobalDialogsControlContext} from './Context'
|
||||
|
||||
export function LinkWarningDialog() {
|
||||
const {linkWarningDialogControl} = useGlobalDialogsControlContext()
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={linkWarningDialogControl.control}
|
||||
nativeOptions={{preventExpansion: true}}
|
||||
webOptions={{alignCenter: true}}
|
||||
onClose={linkWarningDialogControl.clear}>
|
||||
<Dialog.Handle />
|
||||
<InAppBrowserConsentInner link={linkWarningDialogControl.value} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function InAppBrowserConsentInner({
|
||||
link,
|
||||
}: {
|
||||
link?: {href: string; displayText: string; share?: boolean}
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const openLink = useOpenLink()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const potentiallyMisleading = useMemo(
|
||||
() => link && isPossiblyAUrl(link.displayText),
|
||||
[link],
|
||||
)
|
||||
|
||||
const onPressVisit = useCallback(() => {
|
||||
control.close(() => {
|
||||
if (!link) return
|
||||
if (link.share) {
|
||||
shareUrl(link.href)
|
||||
} else {
|
||||
openLink(link.href, undefined, true)
|
||||
}
|
||||
})
|
||||
}, [control, link, openLink])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
control.close()
|
||||
}, [control])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
style={web({maxWidth: 450})}
|
||||
label={
|
||||
potentiallyMisleading
|
||||
? _(msg`Potentially misleading link warning`)
|
||||
: _(msg`Leaving Bluesky`)
|
||||
}>
|
||||
<View style={[a.gap_2xl]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.font_heavy, a.text_2xl]}>
|
||||
{potentiallyMisleading ? (
|
||||
<Trans>Potentially misleading link</Trans>
|
||||
) : (
|
||||
<Trans>Leaving Bluesky</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={[t.atoms.text_contrast_high, a.text_md, a.leading_snug]}>
|
||||
<Trans>This link is taking you to the following website:</Trans>
|
||||
</Text>
|
||||
{link && <LinkBox href={link.href} />}
|
||||
{potentiallyMisleading && (
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_high, a.text_md, a.leading_snug]}>
|
||||
<Trans>Make sure this is where you intend to go!</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.gap_sm,
|
||||
gtMobile && [a.flex_row_reverse, a.justify_start],
|
||||
]}>
|
||||
<Button
|
||||
label={link?.share ? _(msg`Share link`) : _(msg`Visit site`)}
|
||||
accessibilityHint={_(msg`Opens link ${link?.href ?? ''}`)}
|
||||
onPress={onPressVisit}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color={potentiallyMisleading ? 'secondary_inverted' : 'primary'}>
|
||||
<ButtonText>
|
||||
{link?.share ? (
|
||||
<Trans>Share link</Trans>
|
||||
) : (
|
||||
<Trans>Visit site</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Go back`)}
|
||||
onPress={onCancel}
|
||||
size="large"
|
||||
variant="ghost"
|
||||
color="secondary">
|
||||
<ButtonText>
|
||||
<Trans>Go back</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
function LinkBox({href}: {href: string}) {
|
||||
const t = useTheme()
|
||||
const [scheme, hostname, rest] = useMemo(() => {
|
||||
try {
|
||||
const urlp = new URL(href)
|
||||
const [subdomain, apexdomain] = splitApexDomain(urlp.hostname)
|
||||
return [
|
||||
urlp.protocol + '//' + subdomain,
|
||||
apexdomain,
|
||||
urlp.pathname.replace(/\/$/, '') + urlp.search + urlp.hash,
|
||||
]
|
||||
} catch {
|
||||
return ['', href, '']
|
||||
}
|
||||
}, [href])
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_medium,
|
||||
a.px_md,
|
||||
{paddingVertical: 10},
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
]}>
|
||||
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
{scheme}
|
||||
<Text style={[a.text_md, a.leading_snug, t.atoms.text, a.font_bold]}>
|
||||
{hostname}
|
||||
</Text>
|
||||
{rest}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPostgate, AtUri} from '@atproto/api'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPostgate,
|
||||
AtUri,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
@@ -22,7 +26,7 @@ import {
|
||||
import {
|
||||
createThreadgateViewQueryKey,
|
||||
getThreadgateView,
|
||||
ThreadgateAllowUISetting,
|
||||
type ThreadgateAllowUISetting,
|
||||
threadgateViewToAllowUISetting,
|
||||
useSetThreadgateAllowMutation,
|
||||
useThreadgateViewQuery,
|
||||
@@ -558,7 +562,8 @@ export function usePrefetchPostInteractionSettings({
|
||||
await Promise.all([
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: createPostgateQueryKey(postUri),
|
||||
queryFn: () => getPostgateRecord({agent, postUri}),
|
||||
queryFn: () =>
|
||||
getPostgateRecord({agent, postUri}).then(res => res ?? null),
|
||||
staleTime: STALE.SECONDS.THIRTY,
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
|
||||
@@ -390,8 +390,8 @@ function DefaultProfileCard({
|
||||
!enabled
|
||||
? {opacity: 0.5}
|
||||
: pressed || focused || hovered
|
||||
? t.atoms.bg_contrast_25
|
||||
: t.atoms.bg,
|
||||
? t.atoms.bg_contrast_25
|
||||
: t.atoms.bg,
|
||||
]}>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ChangeEmailDialog} from './ChangeEmailDialog'
|
||||
|
||||
export function VerifyEmailDialog({
|
||||
control,
|
||||
onCloseWithoutVerifying,
|
||||
onCloseAfterVerifying,
|
||||
reasonText,
|
||||
changeEmailControl,
|
||||
reminder,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
onCloseWithoutVerifying?: () => void
|
||||
onCloseAfterVerifying?: () => void
|
||||
reasonText?: string
|
||||
/**
|
||||
* if a changeEmailControl for a ChangeEmailDialog is not provided,
|
||||
* this component will create one for you. Using this prop
|
||||
* helps reduce duplication, since these dialogs are often used together.
|
||||
*/
|
||||
changeEmailControl?: Dialog.DialogControlProps
|
||||
reminder?: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const fallbackChangeEmailControl = Dialog.useDialogControl()
|
||||
|
||||
const [didVerify, setDidVerify] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
onClose={async () => {
|
||||
if (!didVerify) {
|
||||
onCloseWithoutVerifying?.()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await agent.resumeSession(agent.session!)
|
||||
onCloseAfterVerifying?.()
|
||||
} catch (e: unknown) {
|
||||
logger.error(String(e))
|
||||
return
|
||||
}
|
||||
}}>
|
||||
<Dialog.Handle />
|
||||
<Inner
|
||||
setDidVerify={setDidVerify}
|
||||
reasonText={reasonText}
|
||||
changeEmailControl={changeEmailControl ?? fallbackChangeEmailControl}
|
||||
reminder={reminder}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
{!changeEmailControl && (
|
||||
<ChangeEmailDialog
|
||||
control={fallbackChangeEmailControl}
|
||||
verifyEmailControl={control}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Inner({
|
||||
setDidVerify,
|
||||
reasonText,
|
||||
changeEmailControl,
|
||||
reminder,
|
||||
}: {
|
||||
setDidVerify: (value: boolean) => void
|
||||
reasonText?: string
|
||||
changeEmailControl: Dialog.DialogControlProps
|
||||
reminder?: boolean
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const agent = useAgent()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const t = useTheme()
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<
|
||||
'Reminder' | 'StepOne' | 'StepTwo' | 'StepThree'
|
||||
>(reminder ? 'Reminder' : 'StepOne')
|
||||
const [confirmationCode, setConfirmationCode] = useState('')
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const uiStrings = {
|
||||
Reminder: {
|
||||
title: _(msg`Please Verify Your Email`),
|
||||
message: _(
|
||||
msg`Your email has not yet been verified. This is an important security step which we recommend.`,
|
||||
),
|
||||
},
|
||||
StepOne: {
|
||||
title: _(msg`Verify Your Email`),
|
||||
message: '',
|
||||
},
|
||||
StepTwo: {
|
||||
title: _(msg`Enter Code`),
|
||||
message: _(
|
||||
msg`An email has been sent! Please enter the confirmation code included in the email below.`,
|
||||
),
|
||||
},
|
||||
StepThree: {
|
||||
title: _(msg`Success!`),
|
||||
message: _(msg`Thank you! Your email has been successfully verified.`),
|
||||
},
|
||||
}
|
||||
|
||||
const onSendEmail = async () => {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await agent.com.atproto.server.requestEmailConfirmation()
|
||||
setCurrentStep('StepTwo')
|
||||
} catch (e: unknown) {
|
||||
setError(cleanError(e))
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onVerifyEmail = async () => {
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await agent.com.atproto.server.confirmEmail({
|
||||
email: (currentAccount?.email || '').trim(),
|
||||
token: confirmationCode.trim(),
|
||||
})
|
||||
} catch (e: unknown) {
|
||||
setError(cleanError(String(e)))
|
||||
setIsProcessing(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsProcessing(false)
|
||||
setDidVerify(true)
|
||||
setCurrentStep('StepThree')
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Verify email dialog`)}
|
||||
style={web({maxWidth: 450})}>
|
||||
<View style={[a.gap_xl]}>
|
||||
{currentStep === 'Reminder' && (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{height: 150},
|
||||
t.atoms.bg_contrast_100,
|
||||
]}>
|
||||
<EnvelopeIcon width={64} fill="white" />
|
||||
</View>
|
||||
)}
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.font_heavy, a.text_2xl]}>
|
||||
{uiStrings[currentStep].title}
|
||||
</Text>
|
||||
{error ? (
|
||||
<View style={[a.rounded_sm, a.overflow_hidden]}>
|
||||
<ErrorMessage message={error} />
|
||||
</View>
|
||||
) : null}
|
||||
{currentStep === 'StepOne' ? (
|
||||
<View>
|
||||
{reasonText ? (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_md, a.leading_snug]}>{reasonText}</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
Don't have access to{' '}
|
||||
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
|
||||
{currentAccount?.email}
|
||||
</Text>
|
||||
?{' '}
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(msg`Change email address`)}
|
||||
style={[a.text_md, a.leading_snug]}
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
control.close(() => {
|
||||
changeEmailControl.open()
|
||||
})
|
||||
return false
|
||||
}}>
|
||||
<Trans>Change your email address</Trans>
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
You'll receive an email at{' '}
|
||||
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
|
||||
{currentAccount?.email}
|
||||
</Text>{' '}
|
||||
to verify it's you.
|
||||
</Trans>{' '}
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(msg`Change email address`)}
|
||||
style={[a.text_md, a.leading_snug]}
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
control.close(() => {
|
||||
changeEmailControl.open()
|
||||
})
|
||||
return false
|
||||
}}>
|
||||
<Trans>Need to change it?</Trans>
|
||||
</InlineLinkText>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
{uiStrings[currentStep].message}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{currentStep === 'StepTwo' ? (
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<Trans>Confirmation Code</Trans>
|
||||
</TextField.LabelText>
|
||||
<TextField.Root>
|
||||
<TextField.Input
|
||||
label={_(msg`Confirmation code`)}
|
||||
placeholder="XXXXX-XXXXX"
|
||||
onChangeText={setConfirmationCode}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}>
|
||||
{currentStep === 'Reminder' ? (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Get started`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
onPress={() => setCurrentStep('StepOne')}>
|
||||
<ButtonText>
|
||||
<Trans>Get started</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Maybe later`)}
|
||||
accessibilityHint={_(msg`Snoozes the reminder`)}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Maybe later</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
) : currentStep === 'StepOne' ? (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Send confirmation email`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={onSendEmail}>
|
||||
<ButtonText>
|
||||
<Trans>Send confirmation</Trans>
|
||||
</ButtonText>
|
||||
{isProcessing ? (
|
||||
<Loader size="sm" style={[{color: 'white'}]} />
|
||||
) : null}
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`I have a code`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={() => setCurrentStep('StepTwo')}>
|
||||
<ButtonText>
|
||||
<Trans>I have a code</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
) : currentStep === 'StepTwo' ? (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Confirm`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={onVerifyEmail}>
|
||||
<ButtonText>
|
||||
<Trans>Confirm</Trans>
|
||||
</ButtonText>
|
||||
{isProcessing ? (
|
||||
<Loader size="sm" style={[{color: 'white'}]} />
|
||||
) : null}
|
||||
</Button>
|
||||
<Button
|
||||
label={_(msg`Resend email`)}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="large"
|
||||
disabled={isProcessing}
|
||||
onPress={() => {
|
||||
setConfirmationCode('')
|
||||
setCurrentStep('StepOne')
|
||||
}}>
|
||||
<ButtonText>
|
||||
<Trans>Resend email</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
) : currentStep === 'StepThree' ? (
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Close</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
@@ -98,8 +98,8 @@ export function EmojiReactionPicker({
|
||||
: t.palette.primary_500,
|
||||
}
|
||||
: alreadyReacted
|
||||
? {backgroundColor: t.palette.primary_200}
|
||||
: bgColor,
|
||||
? {backgroundColor: t.palette.primary_200}
|
||||
: bgColor,
|
||||
{height: 40, width: 40},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
|
||||
@@ -128,8 +128,7 @@ export let MessageContextMenu = ({
|
||||
label={_(msg`Message options`)}
|
||||
contentLabel={_(
|
||||
msg`Message from @${
|
||||
sender?.handle ?? // should always be defined
|
||||
'unknown'
|
||||
sender?.handle ?? 'unknown' // should always be defined
|
||||
}: ${message.text}`,
|
||||
)}>
|
||||
{children}
|
||||
|
||||
@@ -3,28 +3,27 @@ import Svg, {Circle, Path} from 'react-native-svg'
|
||||
|
||||
import {type Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
|
||||
export const VerifiedCheck = React.forwardRef<Svg, Props>(function LogoImpl(
|
||||
props,
|
||||
ref,
|
||||
) {
|
||||
const {fill, size, style, ...rest} = useCommonSVGProps(props)
|
||||
export const VerifiedCheck = React.forwardRef<Svg, Props>(
|
||||
function LogoImpl(props, ref) {
|
||||
const {fill, size, style, ...rest} = useCommonSVGProps(props)
|
||||
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
style={[style]}>
|
||||
<Circle cx="12" cy="12" r="11.5" fill={fill} />
|
||||
<Path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
style={[style]}>
|
||||
<Circle cx="12" cy="12" r="11.5" fill={fill} />
|
||||
<Path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3,33 +3,32 @@ import Svg, {Path} from 'react-native-svg'
|
||||
|
||||
import {type Props, useCommonSVGProps} from '#/components/icons/common'
|
||||
|
||||
export const VerifierCheck = React.forwardRef<Svg, Props>(function LogoImpl(
|
||||
props,
|
||||
ref,
|
||||
) {
|
||||
const {fill, size, style, ...rest} = useCommonSVGProps(props)
|
||||
export const VerifierCheck = React.forwardRef<Svg, Props>(
|
||||
function LogoImpl(props, ref) {
|
||||
const {fill, size, style, ...rest} = useCommonSVGProps(props)
|
||||
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
style={[style]}>
|
||||
<Path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z"
|
||||
/>
|
||||
<Path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
style={[style]}>
|
||||
<Path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z"
|
||||
/>
|
||||
<Path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type ModerationUI} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -148,8 +148,8 @@ function ContentHiderActive({
|
||||
modui.noOverride
|
||||
? _(msg`Learn more about the moderation applied to this content`)
|
||||
: override
|
||||
? _(msg`Hides the content`)
|
||||
: _(msg`Shows the content`)
|
||||
? _(msg`Hides the content`)
|
||||
: _(msg`Shows the content`)
|
||||
}>
|
||||
{state => (
|
||||
<View
|
||||
|
||||
@@ -512,13 +512,13 @@ function StepTitle({
|
||||
backgroundColor: active
|
||||
? t.palette.primary_500
|
||||
: completed
|
||||
? t.palette.primary_100
|
||||
: t.atoms.bg_contrast_25.backgroundColor,
|
||||
? t.palette.primary_100
|
||||
: t.atoms.bg_contrast_25.backgroundColor,
|
||||
borderColor: active
|
||||
? t.palette.primary_500
|
||||
: completed
|
||||
? t.palette.primary_400
|
||||
: t.atoms.border_contrast_low.borderColor,
|
||||
? t.palette.primary_400
|
||||
: t.atoms.border_contrast_low.borderColor,
|
||||
},
|
||||
]}>
|
||||
{completed ? (
|
||||
@@ -533,8 +533,8 @@ function StepTitle({
|
||||
color: active
|
||||
? 'white'
|
||||
: completed
|
||||
? t.palette.primary_700
|
||||
: t.atoms.text_contrast_medium.color,
|
||||
? t.palette.primary_700
|
||||
: t.atoms.text_contrast_medium.color,
|
||||
fontVariant: ['tabular-nums'],
|
||||
width: 24,
|
||||
height: 24,
|
||||
|
||||
@@ -130,8 +130,8 @@ export function Badge({
|
||||
verifiedByHidden
|
||||
? t.atoms.bg_contrast_100.backgroundColor
|
||||
: state.profile.isVerified
|
||||
? t.palette.primary_500
|
||||
: t.atoms.bg_contrast_100.backgroundColor
|
||||
? t.palette.primary_500
|
||||
: t.atoms.bg_contrast_100.backgroundColor
|
||||
}
|
||||
verifier={state.profile.role === 'verifier'}
|
||||
/>
|
||||
|
||||
@@ -64,13 +64,13 @@ function Inner({
|
||||
? _(msg`You are verified`)
|
||||
: _(msg`Your verifications`)
|
||||
: state.profile.isVerified
|
||||
? _(msg`${userName} is verified`)
|
||||
: _(
|
||||
msg({
|
||||
message: `${userName}'s verifications`,
|
||||
comment: `Possessive, meaning "the verifications of {userName}"`,
|
||||
}),
|
||||
)
|
||||
? _(msg`${userName} is verified`)
|
||||
: _(
|
||||
msg({
|
||||
message: `${userName}'s verifications`,
|
||||
comment: `Possessive, meaning "the verifications of {userName}"`,
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
|
||||
@@ -14,6 +14,7 @@ export type DebouncedNavigationProp = Pick<
|
||||
| 'dispatch'
|
||||
| 'goBack'
|
||||
| 'getState'
|
||||
| 'getParent'
|
||||
>
|
||||
|
||||
export function useNavigationDeduped() {
|
||||
@@ -46,6 +47,9 @@ export function useNavigationDeduped() {
|
||||
getState: () => {
|
||||
return navigation.getState()
|
||||
},
|
||||
getParent: (...args: Parameters<typeof navigation.getParent>) => {
|
||||
return navigation.getParent(...args)
|
||||
},
|
||||
}),
|
||||
[dedupe, navigation],
|
||||
)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react'
|
||||
import {useEffect} from 'react'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {type AppBskyNotificationListNotifications} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {CommonActions, useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -25,6 +28,10 @@ export type NotificationReason =
|
||||
| 'quote'
|
||||
| 'chat-message'
|
||||
| 'starterpack-joined'
|
||||
| 'like-via-repost'
|
||||
| 'repost-via-repost'
|
||||
| 'verified'
|
||||
| 'unverified'
|
||||
|
||||
/**
|
||||
* Manually overridden type, but retains the possibility of
|
||||
@@ -66,34 +73,103 @@ export function useNotificationsHandler() {
|
||||
const {currentConvoId} = useCurrentConvoId()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const closeAllActiveElements = useCloseAllActiveElements()
|
||||
const {_} = useLingui()
|
||||
|
||||
// On Android, we cannot control which sound is used for a notification on Android
|
||||
// 28 or higher. Instead, we have to configure a notification channel ahead of time
|
||||
// which has the sounds we want in the configuration for that channel. These two
|
||||
// channels allow for the mute/unmute functionality we want for the background
|
||||
// handler.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isAndroid) return
|
||||
// assign both chat notifications to a 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
|
||||
const CHAT_GROUP = 'chat'
|
||||
Notifications.setNotificationChannelGroupAsync(CHAT_GROUP, {
|
||||
name: _(msg`Chat`),
|
||||
description: _(
|
||||
msg`You can choose whether chat notifications have sound in the chat settings within the app`,
|
||||
),
|
||||
})
|
||||
Notifications.setNotificationChannelAsync('chat-messages', {
|
||||
name: 'Chat',
|
||||
name: _(msg`Chat messages - sound`),
|
||||
groupId: CHAT_GROUP,
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
sound: 'dm.mp3',
|
||||
showBadge: true,
|
||||
vibrationPattern: [250],
|
||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||
})
|
||||
|
||||
Notifications.setNotificationChannelAsync('chat-messages-muted', {
|
||||
name: 'Chat - Muted',
|
||||
name: _(msg`Chat messages - silent`),
|
||||
groupId: CHAT_GROUP,
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
sound: null,
|
||||
showBadge: true,
|
||||
vibrationPattern: [250],
|
||||
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE,
|
||||
})
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'like' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`Likes`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'repost' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`Reposts`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'reply' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`Replies`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'mention' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`Mentions`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'quote' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`Quotes`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'follow' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`New followers`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'like-via-repost' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`Likes of your reposts`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(
|
||||
'repost-via-repost' satisfies AppBskyNotificationListNotifications.Notification['reason'],
|
||||
{
|
||||
name: _(msg`Reposts of your reposts`),
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
},
|
||||
)
|
||||
}, [_])
|
||||
|
||||
useEffect(() => {
|
||||
const handleNotification = (payload?: NotificationPayload) => {
|
||||
if (!payload) return
|
||||
|
||||
@@ -151,6 +227,10 @@ export function useNotificationsHandler() {
|
||||
case 'quote':
|
||||
case 'reply':
|
||||
case 'starterpack-joined':
|
||||
case 'like-via-repost':
|
||||
case 'repost-via-repost':
|
||||
case 'verified':
|
||||
case 'unverified':
|
||||
resetToTab('NotificationsTab')
|
||||
break
|
||||
// TODO implement these after we have an idea of how to handle each individual case
|
||||
@@ -242,7 +322,19 @@ export function useNotificationsHandler() {
|
||||
const payload = e.notification.request.trigger
|
||||
.payload as NotificationPayload
|
||||
|
||||
if (!payload) return
|
||||
if (!payload) {
|
||||
logger.error('useNotificationsHandler: received no payload', {
|
||||
identifier: e.notification.request.identifier,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!payload.reason) {
|
||||
logger.error('useNotificationsHandler: received unknown payload', {
|
||||
payload,
|
||||
identifier: e.notification.request.identifier,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
'User pressed a notification, opening notifications tab',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isRelativeUrl,
|
||||
toNiceDomain,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useInAppBrowser} from '#/state/preferences/in-app-browser'
|
||||
import {useTheme} from '#/alf'
|
||||
@@ -64,6 +65,10 @@ export function useOpenLink() {
|
||||
toolbarColor: t.atoms.bg.backgroundColor,
|
||||
controlsColor: t.palette.primary_500,
|
||||
createTask: false,
|
||||
}).catch(err => {
|
||||
if (__DEV__)
|
||||
logger.error('Could not open web browser', {message: err})
|
||||
Linking.openURL(url)
|
||||
}),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
AppBskyLabelerDefs,
|
||||
type AppBskyLabelerDefs,
|
||||
BskyAgent,
|
||||
ComAtprotoLabelDefs,
|
||||
InterpretedLabelValueDefinition,
|
||||
type ComAtprotoLabelDefs,
|
||||
type InterpretedLabelValueDefinition,
|
||||
LABELS,
|
||||
ModerationCause,
|
||||
ModerationOpts,
|
||||
ModerationUI,
|
||||
type ModerationCause,
|
||||
type ModerationOpts,
|
||||
type ModerationUI,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {AppModerationCause} from '#/components/Pills'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
|
||||
export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
|
||||
export const OTHER_SELF_LABELS = ['graphic-media']
|
||||
@@ -29,8 +29,8 @@ export function getModerationCauseKey(
|
||||
cause.source.type === 'labeler'
|
||||
? cause.source.did
|
||||
: cause.source.type === 'list'
|
||||
? cause.source.list.uri
|
||||
: 'user'
|
||||
? cause.source.list.uri
|
||||
: 'user'
|
||||
if (cause.type === 'label') {
|
||||
return `label:${cause.label.val}:${source}`
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
BSKY_LABELER_DID,
|
||||
ModerationCause,
|
||||
ModerationCauseSource,
|
||||
type ModerationCause,
|
||||
type ModerationCauseSource,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -12,10 +12,10 @@ import {useLabelDefinitions} from '#/state/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {AppModerationCause} from '#/components/Pills'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {useGlobalLabelStrings} from './useGlobalLabelStrings'
|
||||
import {getDefinition, getLabelStrings} from './useLabelInfo'
|
||||
|
||||
@@ -153,8 +153,8 @@ export function useModerationCauseDescription(
|
||||
def.identifier === '!no-unauthenticated'
|
||||
? EyeSlash
|
||||
: def.severity === 'alert'
|
||||
? Warning
|
||||
: CircleInfo,
|
||||
? Warning
|
||||
: CircleInfo,
|
||||
name: strings.name,
|
||||
description: strings.description,
|
||||
source,
|
||||
|
||||
@@ -7,6 +7,7 @@ export type Gate =
|
||||
| 'old_postonboarding'
|
||||
| 'onboarding_add_video_feed'
|
||||
| 'post_threads_v2_unspecced'
|
||||
| 'reengagement_features'
|
||||
| 'remove_show_latest_button'
|
||||
| 'test_gate_1'
|
||||
| 'test_gate_2'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import {AppState, AppStateStatus} from 'react-native'
|
||||
import {AppState, type AppStateStatus} from 'react-native'
|
||||
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
||||
|
||||
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
import {logger} from '#/logger'
|
||||
import {MetricEvents} from '#/logger/metrics'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {useSession} from '../../state/session'
|
||||
import {timeout} from '../async/timeout'
|
||||
import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
|
||||
import {Gate} from './gates'
|
||||
import {type Gate} from './gates'
|
||||
|
||||
const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV'
|
||||
|
||||
@@ -51,8 +51,8 @@ function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'development'
|
||||
: IS_TESTFLIGHT
|
||||
? 'staging'
|
||||
: 'production',
|
||||
? 'staging'
|
||||
: 'production',
|
||||
},
|
||||
// Don't block on waiting for network. The fetched config will kick in on next load.
|
||||
// This ensures the UI is always consistent and doesn't update mid-session.
|
||||
|
||||
@@ -11,8 +11,8 @@ const IFRAME_HOST = isWeb
|
||||
? 'http://localhost:8100'
|
||||
: 'https://bsky.app'
|
||||
: __DEV__ && !process.env.JEST_WORKER_ID
|
||||
? 'http://localhost:8100'
|
||||
: 'https://bsky.app'
|
||||
? 'http://localhost:8100'
|
||||
: 'https://bsky.app'
|
||||
|
||||
export const embedPlayerSources = [
|
||||
'youtube',
|
||||
|
||||
@@ -193,6 +193,11 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
|
||||
return startUriToStarterPackUri(urlp.pathname)
|
||||
}
|
||||
|
||||
// special-case search links
|
||||
if (urlp.pathname === '/search') {
|
||||
return `/search?q=${urlp.searchParams.get('q')}`
|
||||
}
|
||||
|
||||
return urlp.pathname
|
||||
} catch (e) {
|
||||
console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e)
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module '*.po' {
|
||||
import {type Messages} from '@lingui/core'
|
||||
export const messages: Messages
|
||||
}
|
||||
+41
-41
@@ -11,47 +11,47 @@ import {i18n} from '@lingui/core'
|
||||
|
||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {AppLanguage} from '#/locale/languages'
|
||||
import {messages as messagesAn} from '#/locale/locales/an/messages'
|
||||
import {messages as messagesAst} from '#/locale/locales/ast/messages'
|
||||
import {messages as messagesCa} from '#/locale/locales/ca/messages'
|
||||
import {messages as messagesCy} from '#/locale/locales/cy/messages'
|
||||
import {messages as messagesDa} from '#/locale/locales/da/messages'
|
||||
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||
import {messages as messagesEl} from '#/locale/locales/el/messages'
|
||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages'
|
||||
import {messages as messagesEo} from '#/locale/locales/eo/messages'
|
||||
import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||
import {messages as messagesEu} from '#/locale/locales/eu/messages'
|
||||
import {messages as messagesFi} from '#/locale/locales/fi/messages'
|
||||
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
||||
import {messages as messagesFy} from '#/locale/locales/fy/messages'
|
||||
import {messages as messagesGa} from '#/locale/locales/ga/messages'
|
||||
import {messages as messagesGd} from '#/locale/locales/gd/messages'
|
||||
import {messages as messagesGl} from '#/locale/locales/gl/messages'
|
||||
import {messages as messagesHi} from '#/locale/locales/hi/messages'
|
||||
import {messages as messagesHu} from '#/locale/locales/hu/messages'
|
||||
import {messages as messagesIa} from '#/locale/locales/ia/messages'
|
||||
import {messages as messagesId} from '#/locale/locales/id/messages'
|
||||
import {messages as messagesIt} from '#/locale/locales/it/messages'
|
||||
import {messages as messagesJa} from '#/locale/locales/ja/messages'
|
||||
import {messages as messagesKm} from '#/locale/locales/km/messages'
|
||||
import {messages as messagesKo} from '#/locale/locales/ko/messages'
|
||||
import {messages as messagesNe} from '#/locale/locales/ne/messages'
|
||||
import {messages as messagesNl} from '#/locale/locales/nl/messages'
|
||||
import {messages as messagesPl} from '#/locale/locales/pl/messages'
|
||||
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
|
||||
import {messages as messagesPt_PT} from '#/locale/locales/pt-PT/messages'
|
||||
import {messages as messagesRo} from '#/locale/locales/ro/messages'
|
||||
import {messages as messagesRu} from '#/locale/locales/ru/messages'
|
||||
import {messages as messagesSv} from '#/locale/locales/sv/messages'
|
||||
import {messages as messagesTh} from '#/locale/locales/th/messages'
|
||||
import {messages as messagesTr} from '#/locale/locales/tr/messages'
|
||||
import {messages as messagesUk} from '#/locale/locales/uk/messages'
|
||||
import {messages as messagesVi} from '#/locale/locales/vi/messages'
|
||||
import {messages as messagesZh_CN} from '#/locale/locales/zh-CN/messages'
|
||||
import {messages as messagesZh_HK} from '#/locale/locales/zh-HK/messages'
|
||||
import {messages as messagesZh_TW} from '#/locale/locales/zh-TW/messages'
|
||||
import {messages as messagesAn} from '#/locale/locales/an/messages.po'
|
||||
import {messages as messagesAst} from '#/locale/locales/ast/messages.po'
|
||||
import {messages as messagesCa} from '#/locale/locales/ca/messages.po'
|
||||
import {messages as messagesCy} from '#/locale/locales/cy/messages.po'
|
||||
import {messages as messagesDa} from '#/locale/locales/da/messages.po'
|
||||
import {messages as messagesDe} from '#/locale/locales/de/messages.po'
|
||||
import {messages as messagesEl} from '#/locale/locales/el/messages.po'
|
||||
import {messages as messagesEn} from '#/locale/locales/en/messages.po'
|
||||
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages.po'
|
||||
import {messages as messagesEo} from '#/locale/locales/eo/messages.po'
|
||||
import {messages as messagesEs} from '#/locale/locales/es/messages.po'
|
||||
import {messages as messagesEu} from '#/locale/locales/eu/messages.po'
|
||||
import {messages as messagesFi} from '#/locale/locales/fi/messages.po'
|
||||
import {messages as messagesFr} from '#/locale/locales/fr/messages.po'
|
||||
import {messages as messagesFy} from '#/locale/locales/fy/messages.po'
|
||||
import {messages as messagesGa} from '#/locale/locales/ga/messages.po'
|
||||
import {messages as messagesGd} from '#/locale/locales/gd/messages.po'
|
||||
import {messages as messagesGl} from '#/locale/locales/gl/messages.po'
|
||||
import {messages as messagesHi} from '#/locale/locales/hi/messages.po'
|
||||
import {messages as messagesHu} from '#/locale/locales/hu/messages.po'
|
||||
import {messages as messagesIa} from '#/locale/locales/ia/messages.po'
|
||||
import {messages as messagesId} from '#/locale/locales/id/messages.po'
|
||||
import {messages as messagesIt} from '#/locale/locales/it/messages.po'
|
||||
import {messages as messagesJa} from '#/locale/locales/ja/messages.po'
|
||||
import {messages as messagesKm} from '#/locale/locales/km/messages.po'
|
||||
import {messages as messagesKo} from '#/locale/locales/ko/messages.po'
|
||||
import {messages as messagesNe} from '#/locale/locales/ne/messages.po'
|
||||
import {messages as messagesNl} from '#/locale/locales/nl/messages.po'
|
||||
import {messages as messagesPl} from '#/locale/locales/pl/messages.po'
|
||||
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages.po'
|
||||
import {messages as messagesPt_PT} from '#/locale/locales/pt-PT/messages.po'
|
||||
import {messages as messagesRo} from '#/locale/locales/ro/messages.po'
|
||||
import {messages as messagesRu} from '#/locale/locales/ru/messages.po'
|
||||
import {messages as messagesSv} from '#/locale/locales/sv/messages.po'
|
||||
import {messages as messagesTh} from '#/locale/locales/th/messages.po'
|
||||
import {messages as messagesTr} from '#/locale/locales/tr/messages.po'
|
||||
import {messages as messagesUk} from '#/locale/locales/uk/messages.po'
|
||||
import {messages as messagesVi} from '#/locale/locales/vi/messages.po'
|
||||
import {messages as messagesZh_CN} from '#/locale/locales/zh-CN/messages.po'
|
||||
import {messages as messagesZh_HK} from '#/locale/locales/zh-HK/messages.po'
|
||||
import {messages as messagesZh_TW} from '#/locale/locales/zh-TW/messages.po'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
|
||||
/**
|
||||
|
||||
+44
-41
@@ -7,173 +7,176 @@ import {useLanguagePrefs} from '#/state/preferences'
|
||||
|
||||
/**
|
||||
* We do a dynamic import of just the catalog that we need
|
||||
*
|
||||
* IMPORTANT: Imports should be prefixed with '@lingui/loader!'
|
||||
* for the Lingui webpack loader to work
|
||||
*/
|
||||
export async function dynamicActivate(locale: AppLanguage) {
|
||||
let mod: any
|
||||
|
||||
switch (locale) {
|
||||
case AppLanguage.an: {
|
||||
mod = await import(`./locales/an/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/an/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ast: {
|
||||
mod = await import(`./locales/ast/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ast/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ca: {
|
||||
mod = await import(`./locales/ca/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ca/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.cy: {
|
||||
mod = await import(`./locales/cy/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/cy/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
mod = await import(`./locales/da/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/da/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.de: {
|
||||
mod = await import(`./locales/de/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/de/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.el: {
|
||||
mod = await import(`./locales/el/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/el/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.en_GB: {
|
||||
mod = await import(`./locales/en-GB/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/en-GB/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.eo: {
|
||||
mod = await import(`./locales/eo/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/eo/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.es: {
|
||||
mod = await import(`./locales/es/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/es/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.eu: {
|
||||
mod = await import(`./locales/eu/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/eu/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.fi: {
|
||||
mod = await import(`./locales/fi/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/fi/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.fr: {
|
||||
mod = await import(`./locales/fr/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/fr/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.fy: {
|
||||
mod = await import(`./locales/fy/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/fy/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ga: {
|
||||
mod = await import(`./locales/ga/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ga/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.gd: {
|
||||
mod = await import(`./locales/gd/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/gd/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.gl: {
|
||||
mod = await import(`./locales/gl/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/gl/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.hi: {
|
||||
mod = await import(`./locales/hi/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/hi/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.hu: {
|
||||
mod = await import(`./locales/hu/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/hu/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ia: {
|
||||
mod = await import(`./locales/ia/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ia/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.id: {
|
||||
mod = await import(`./locales/id/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/id/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.it: {
|
||||
mod = await import(`./locales/it/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/it/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ja: {
|
||||
mod = await import(`./locales/ja/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ja/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.km: {
|
||||
mod = await import(`./locales/km/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/km/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ko: {
|
||||
mod = await import(`./locales/ko/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ko/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ne: {
|
||||
mod = await import(`./locales/ne/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ne/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
mod = await import(`./locales/nl/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/nl/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.pl: {
|
||||
mod = await import(`./locales/pl/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/pl/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.pt_BR: {
|
||||
mod = await import(`./locales/pt-BR/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/pt-BR/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.pt_PT: {
|
||||
mod = await import(`./locales/pt-PT/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/pt-PT/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ro: {
|
||||
mod = await import(`./locales/ro/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ro/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ru: {
|
||||
mod = await import(`./locales/ru/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/ru/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.sv: {
|
||||
mod = await import(`./locales/sv/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/sv/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.th: {
|
||||
mod = await import(`./locales/th/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/th/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.tr: {
|
||||
mod = await import(`./locales/tr/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/tr/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.uk: {
|
||||
mod = await import(`./locales/uk/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/uk/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.vi: {
|
||||
mod = await import(`./locales/vi/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/vi/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_CN: {
|
||||
mod = await import(`./locales/zh-CN/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/zh-CN/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_HK: {
|
||||
mod = await import(`./locales/zh-HK/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/zh-HK/messages.po`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_TW: {
|
||||
mod = await import(`./locales/zh-TW/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/zh-TW/messages.po`)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
mod = await import(`./locales/en/messages`)
|
||||
mod = await import(`@lingui/loader!./locales/en/messages.po`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
+513
-438
File diff suppressed because it is too large
Load Diff
@@ -110,6 +110,7 @@ describe('general functionality', () => {
|
||||
const timestamp = Date.now()
|
||||
const sentryTimestamp = timestamp / 1000
|
||||
|
||||
/*
|
||||
sentryTransport(
|
||||
LogLevel.Debug,
|
||||
Logger.Context.Default,
|
||||
@@ -125,6 +126,7 @@ describe('general functionality', () => {
|
||||
level: LogLevel.Debug,
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
*/
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Info,
|
||||
@@ -154,7 +156,7 @@ describe('general functionality', () => {
|
||||
message,
|
||||
data: {__context__: 'logger'},
|
||||
type: 'default',
|
||||
level: 'debug', // Sentry bug, log becomes debug
|
||||
level: 'log',
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
jest.runAllTimers()
|
||||
@@ -220,7 +222,7 @@ describe('general functionality', () => {
|
||||
const sentryTimestamp = timestamp / 1000
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Debug,
|
||||
LogLevel.Info,
|
||||
undefined,
|
||||
message,
|
||||
{error: new Error('foo')},
|
||||
@@ -230,7 +232,7 @@ describe('general functionality', () => {
|
||||
message,
|
||||
data: {error: 'Error: foo'},
|
||||
type: 'default',
|
||||
level: LogLevel.Debug,
|
||||
level: LogLevel.Info,
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {Sentry} from '#/logger/sentry/lib'
|
||||
import {LogLevel, Transport} from '#/logger/types'
|
||||
import {LogLevel, type Transport} from '#/logger/types'
|
||||
import {prepareMetadata} from '#/logger/util'
|
||||
|
||||
export const sentryTransport: Transport = (
|
||||
@@ -10,6 +10,9 @@ export const sentryTransport: Transport = (
|
||||
{type, tags, ...metadata},
|
||||
timestamp,
|
||||
) => {
|
||||
// Skip debug messages entirely for now - esb
|
||||
if (level === LogLevel.Debug) return
|
||||
|
||||
const meta = {
|
||||
__context__: context,
|
||||
...prepareMetadata(metadata),
|
||||
|
||||
@@ -41,12 +41,12 @@ globalThis.atob = (str: string): string => {
|
||||
r1 === 64
|
||||
? String.fromCharCode((bitmap >> 16) & 255)
|
||||
: r2 === 64
|
||||
? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255)
|
||||
: String.fromCharCode(
|
||||
(bitmap >> 16) & 255,
|
||||
(bitmap >> 8) & 255,
|
||||
bitmap & 255,
|
||||
)
|
||||
? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255)
|
||||
: String.fromCharCode(
|
||||
(bitmap >> 16) & 255,
|
||||
(bitmap >> 8) & 255,
|
||||
bitmap & 255,
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||
requestedAccount
|
||||
? Forms.Login
|
||||
: accounts.length
|
||||
? Forms.ChooseAccount
|
||||
: Forms.Login,
|
||||
? Forms.ChooseAccount
|
||||
: Forms.Login,
|
||||
)
|
||||
|
||||
const {
|
||||
|
||||
@@ -155,7 +155,7 @@ export function MessagesScreen({navigation, route}: Props) {
|
||||
profiles: inboxPreviewConvos.slice(0, 3),
|
||||
},
|
||||
...conversations.map(
|
||||
convo => ({type: 'CONVERSATION', conversation: convo} as const),
|
||||
convo => ({type: 'CONVERSATION', conversation: convo}) as const,
|
||||
),
|
||||
] satisfies ListItem[]
|
||||
}
|
||||
|
||||
@@ -176,8 +176,8 @@ export function StepFinished() {
|
||||
avatarResult: profileStepResults.isCreatedAvatar
|
||||
? 'created'
|
||||
: profileStepResults.image
|
||||
? 'uploaded'
|
||||
: 'default',
|
||||
? 'uploaded'
|
||||
: 'default',
|
||||
})
|
||||
})(),
|
||||
requestNotificationsPermission('AfterOnboarding'),
|
||||
|
||||
@@ -311,33 +311,34 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
||||
isRoot && [a.pt_lg],
|
||||
]}>
|
||||
<View style={[a.flex_row, a.gap_md, a.pb_md]}>
|
||||
<PreviewableUserAvatar
|
||||
size={42}
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
live={live}
|
||||
onBeforePress={onOpenAuthor}
|
||||
/>
|
||||
<ProfileHoverCard did={post.author.did}>
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Link
|
||||
to={authorHref}
|
||||
style={[a.flex_shrink]}
|
||||
label={sanitizeDisplayName(
|
||||
post.author.displayName ||
|
||||
sanitizeHandle(post.author.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
onPress={onOpenAuthor}>
|
||||
<View collapsable={false}>
|
||||
<PreviewableUserAvatar
|
||||
size={42}
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
live={live}
|
||||
onBeforePress={onOpenAuthor}
|
||||
/>
|
||||
</View>
|
||||
<Link
|
||||
to={authorHref}
|
||||
style={[a.flex_1]}
|
||||
label={sanitizeDisplayName(
|
||||
post.author.displayName || sanitizeHandle(post.author.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
onPress={onOpenAuthor}>
|
||||
<View style={[a.flex_1, a.align_start]}>
|
||||
<ProfileHoverCard did={post.author.did} style={[a.w_full]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
a.flex_shrink,
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
a.self_start,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeDisplayName(
|
||||
@@ -346,32 +347,25 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</Text>
|
||||
</Link>
|
||||
|
||||
<View style={[{paddingLeft: 3, top: -1}]}>
|
||||
<VerificationCheckButton profile={authorShadow} size="md" />
|
||||
<View style={[{paddingLeft: 3, top: -1}]}>
|
||||
<VerificationCheckButton profile={authorShadow} size="md" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[a.align_start]}>
|
||||
<Link
|
||||
style={[a.flex_shrink]}
|
||||
to={authorHref}
|
||||
label={sanitizeHandle(post.author.handle, '@')}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(post.author.handle, '@')}
|
||||
</Text>
|
||||
</Link>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeHandle(post.author.handle, '@')}
|
||||
</Text>
|
||||
</ProfileHoverCard>
|
||||
</View>
|
||||
</ProfileHoverCard>
|
||||
</Link>
|
||||
{showFollowButton && (
|
||||
<View>
|
||||
<View collapsable={false}>
|
||||
<PostThreadFollowBtn did={post.author.did} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -54,13 +54,16 @@ export function PostThread({uri}: {uri: string}) {
|
||||
* One query to rule them all
|
||||
*/
|
||||
const thread = usePostThread({anchor: uri})
|
||||
const anchor = useMemo(() => {
|
||||
const {anchor, hasParents} = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
let hasParents = false
|
||||
for (const item of thread.data.items) {
|
||||
if (item.type === 'threadPost' && item.depth === 0) {
|
||||
return item
|
||||
return {anchor: item, hasParents}
|
||||
}
|
||||
hasParents = true
|
||||
}
|
||||
return
|
||||
return {hasParents}
|
||||
}, [thread.data.items])
|
||||
|
||||
const {openComposer} = useOpenComposer()
|
||||
@@ -481,6 +484,8 @@ export function PostThread({uri}: {uri: string}) {
|
||||
],
|
||||
)
|
||||
|
||||
const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout.Header.Outer headerRef={headerRef}>
|
||||
@@ -537,8 +542,10 @@ export function PostThread({uri}: {uri: string}) {
|
||||
* back to the top of the screen when handling scroll.
|
||||
*/
|
||||
height={platform({
|
||||
web: windowHeight - 200,
|
||||
default: deferParents ? windowHeight * 2 : windowHeight - 200,
|
||||
web: defaultListFooterHeight,
|
||||
default: deferParents
|
||||
? windowHeight * 2
|
||||
: defaultListFooterHeight,
|
||||
})}
|
||||
style={isTombstoneView ? {borderTopWidth: 0} : undefined}
|
||||
/>
|
||||
|
||||
@@ -214,8 +214,8 @@ let ProfileHeaderLabeler = ({
|
||||
? t.palette.contrast_50
|
||||
: t.palette.contrast_25
|
||||
: state.hovered || state.pressed
|
||||
? tokens.color.temp_purple_dark
|
||||
: tokens.color.temp_purple,
|
||||
? tokens.color.temp_purple_dark
|
||||
: tokens.color.temp_purple,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
|
||||
@@ -342,11 +342,11 @@ export function Explore({
|
||||
])
|
||||
|
||||
const topBorder = useMemo(
|
||||
() => ({type: 'topBorder', key: 'top-border'} as const),
|
||||
() => ({type: 'topBorder', key: 'top-border'}) as const,
|
||||
[],
|
||||
)
|
||||
const trendingTopicsModule = useMemo(
|
||||
() => ({type: 'trendingTopics', key: 'trending-topics'} as const),
|
||||
() => ({type: 'trendingTopics', key: 'trending-topics'}) as const,
|
||||
[],
|
||||
)
|
||||
const suggestedFollowsModule = useMemo(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {useMemo} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
|
||||
import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
|
||||
|
||||
export function useAppIconSets() {
|
||||
const {_} = useLingui()
|
||||
@@ -13,20 +13,28 @@ export function useAppIconSets() {
|
||||
id: 'default_light',
|
||||
name: _(msg({context: 'Name of app icon variant', message: 'Light'})),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_default_light.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_default_light.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_default_light.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_default_light.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default_dark',
|
||||
name: _(msg({context: 'Name of app icon variant', message: 'Dark'})),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_default_dark.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_default_dark.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_default_dark.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_default_dark.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
] satisfies AppIconSet[]
|
||||
@@ -39,10 +47,14 @@ export function useAppIconSets() {
|
||||
id: 'core_aurora',
|
||||
name: _(msg({context: 'Name of app icon variant', message: 'Aurora'})),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_aurora.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_aurora.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_aurora.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_aurora.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
// {
|
||||
@@ -59,20 +71,28 @@ export function useAppIconSets() {
|
||||
id: 'core_sunrise',
|
||||
name: _(msg({context: 'Name of app icon variant', message: 'Sunrise'})),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_sunrise.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_sunrise.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_sunrise.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_sunrise.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core_sunset',
|
||||
name: _(msg({context: 'Name of app icon variant', message: 'Sunset'})),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_sunset.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_sunset.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_sunset.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_sunset.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -81,10 +101,14 @@ export function useAppIconSets() {
|
||||
msg({context: 'Name of app icon variant', message: 'Midnight'}),
|
||||
),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_midnight.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_midnight.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_midnight.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_midnight.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -93,10 +117,14 @@ export function useAppIconSets() {
|
||||
msg({context: 'Name of app icon variant', message: 'Flat Blue'}),
|
||||
),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_flat_blue.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_flat_blue.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_flat_blue.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_flat_blue.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -105,10 +133,14 @@ export function useAppIconSets() {
|
||||
msg({context: 'Name of app icon variant', message: 'Flat White'}),
|
||||
),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_flat_white.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_flat_white.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_flat_white.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_flat_white.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -117,10 +149,14 @@ export function useAppIconSets() {
|
||||
msg({context: 'Name of app icon variant', message: 'Flat Black'}),
|
||||
),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_flat_black.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_flat_black.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_flat_black.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_flat_black.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -132,10 +168,14 @@ export function useAppIconSets() {
|
||||
}),
|
||||
),
|
||||
iosImage: () => {
|
||||
return require(`../../../../assets/app-icons/ios_icon_core_classic.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/ios_icon_core_classic.png`,
|
||||
)
|
||||
},
|
||||
androidImage: () => {
|
||||
return require(`../../../../assets/app-icons/android_icon_core_classic.png`)
|
||||
return require(
|
||||
`../../../../assets/app-icons/android_icon_core_classic.png`,
|
||||
)
|
||||
},
|
||||
},
|
||||
] satisfies AppIconSet[]
|
||||
|
||||
@@ -113,13 +113,9 @@ function Inner({
|
||||
},
|
||||
)
|
||||
await Promise.all([
|
||||
await qc.resetQueries({
|
||||
queryKey: createSuggestedStarterPacksQueryKey(),
|
||||
}),
|
||||
await qc.resetQueries({queryKey: createGetSuggestedFeedsQueryKey()}),
|
||||
await qc.resetQueries({
|
||||
queryKey: createGetSuggestedUsersQueryKey({}),
|
||||
}),
|
||||
qc.resetQueries({queryKey: createSuggestedStarterPacksQueryKey()}),
|
||||
qc.resetQueries({queryKey: createGetSuggestedFeedsQueryKey()}),
|
||||
qc.resetQueries({queryKey: createGetSuggestedUsersQueryKey({})}),
|
||||
])
|
||||
|
||||
Toast.show(
|
||||
|
||||
@@ -38,7 +38,7 @@ export function LikesOnRepostsNotificationSettingsScreen({}: Props) {
|
||||
<SettingsList.ItemIcon icon={LikeRepostIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
bold
|
||||
titleText={<Trans>Likes on your reposts</Trans>}
|
||||
titleText={<Trans>Likes of your reposts</Trans>}
|
||||
subtitleText={
|
||||
<Trans>
|
||||
Get notifications when people like posts that you've reposted.
|
||||
|
||||
@@ -53,11 +53,7 @@ export function ReplyNotificationSettingsScreen({}: Props) {
|
||||
</Admonition>
|
||||
</View>
|
||||
) : (
|
||||
<PreferenceControls
|
||||
name="reply"
|
||||
preference={preferences?.reply}
|
||||
allowDisableInApp={false}
|
||||
/>
|
||||
<PreferenceControls name="reply" preference={preferences?.reply} />
|
||||
)}
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
@@ -27,6 +28,10 @@ export function PreferenceControls({
|
||||
preference?: AppBskyNotificationDefs.Preference | FilterablePreference
|
||||
allowDisableInApp?: boolean
|
||||
}) {
|
||||
const gate = useGate()
|
||||
|
||||
if (!gate('reengagement_features')) return null
|
||||
|
||||
if (!preference)
|
||||
return (
|
||||
<View style={[a.w_full, a.pt_5xl, a.align_center]}>
|
||||
@@ -85,7 +90,7 @@ export function Inner({
|
||||
|
||||
const newPreference = {
|
||||
...preference,
|
||||
filter: change,
|
||||
include: change,
|
||||
} satisfies typeof preference
|
||||
|
||||
mutate({
|
||||
@@ -98,7 +103,7 @@ export function Inner({
|
||||
<View style={[a.px_xl, a.pt_md, a.gap_sm]}>
|
||||
<Toggle.Group
|
||||
type="checkbox"
|
||||
label={_(`Select your preferred notification channels`)}
|
||||
label={_(msg`Select your preferred notification channels`)}
|
||||
values={channels}
|
||||
onChange={onChangeChannels}>
|
||||
<View style={[a.gap_sm]}>
|
||||
@@ -138,14 +143,16 @@ export function Inner({
|
||||
)}
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
{'filter' in preference && (
|
||||
{'include' in preference && (
|
||||
<>
|
||||
<Divider />
|
||||
<Text style={[a.font_bold, a.text_md]}>From</Text>
|
||||
<Text style={[a.font_bold, a.text_md]}>
|
||||
<Trans>From</Trans>
|
||||
</Text>
|
||||
<Toggle.Group
|
||||
type="radio"
|
||||
label={_('Filter who you receive notifications from')}
|
||||
values={[preference.filter]}
|
||||
label={_(msg`Filter who you receive notifications from`)}
|
||||
values={[preference.include]}
|
||||
onChange={onChangeFilter}
|
||||
disabled={channels.length === 0}>
|
||||
<View style={[a.gap_sm]}>
|
||||
|
||||
@@ -117,6 +117,28 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
</View>
|
||||
)}
|
||||
<View style={[a.gap_sm]}>
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for like notifications`)}
|
||||
to={{screen: 'LikeNotificationSettings'}}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={HeartIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>Likes</Trans>}
|
||||
subtitleText={<SettingPreview preference={settings?.like} />}
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for new follower notifications`)}
|
||||
to={{screen: 'NewFollowerNotificationSettings'}}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={PersonPlusIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>New followers</Trans>}
|
||||
subtitleText={<SettingPreview preference={settings?.follow} />}
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for reply notifications`)}
|
||||
to={{screen: 'ReplyNotificationSettings'}}
|
||||
@@ -150,17 +172,6 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for like notifications`)}
|
||||
to={{screen: 'LikeNotificationSettings'}}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={HeartIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>Likes</Trans>}
|
||||
subtitleText={<SettingPreview preference={settings?.like} />}
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for repost notifications`)}
|
||||
to={{screen: 'RepostNotificationSettings'}}
|
||||
@@ -172,17 +183,6 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
label={_(msg`Settings for new follower notifications`)}
|
||||
to={{screen: 'NewFollowerNotificationSettings'}}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={PersonPlusIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>New followers</Trans>}
|
||||
subtitleText={<SettingPreview preference={settings?.follow} />}
|
||||
showSkeleton={!settings}
|
||||
/>
|
||||
</SettingsList.LinkItem>
|
||||
{/* <SettingsList.LinkItem
|
||||
label={_(msg`Settings for activity alerts`)}
|
||||
to={{screen: 'ActivityNotificationSettings'}}
|
||||
@@ -199,13 +199,13 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
</SettingsList.LinkItem> */}
|
||||
<SettingsList.LinkItem
|
||||
label={_(
|
||||
msg`Settings for notifications for likes on your reposts`,
|
||||
msg`Settings for notifications for likes of your reposts`,
|
||||
)}
|
||||
to={{screen: 'LikesOnRepostsNotificationSettings'}}
|
||||
contentContainerStyle={[a.align_start]}>
|
||||
<SettingsList.ItemIcon icon={LikeRepostIcon} />
|
||||
<ItemTextWithSubtitle
|
||||
titleText={<Trans>Likes on your reposts</Trans>}
|
||||
titleText={<Trans>Likes of your reposts</Trans>}
|
||||
subtitleText={
|
||||
<SettingPreview preference={settings?.likeViaRepost} />
|
||||
}
|
||||
@@ -260,8 +260,8 @@ function SettingPreview({
|
||||
if (!preference) {
|
||||
return null
|
||||
} else {
|
||||
if ('filter' in preference) {
|
||||
if (preference.filter === 'all') {
|
||||
if ('include' in preference) {
|
||||
if (preference.include === 'all') {
|
||||
if (preference.list && preference.push) {
|
||||
return _(msg`In-app, Push, Everyone`)
|
||||
} else if (preference.list) {
|
||||
@@ -269,7 +269,7 @@ function SettingPreview({
|
||||
} else if (preference.push) {
|
||||
return _(msg`Push, Everyone`)
|
||||
}
|
||||
} else if (preference.filter === 'follows') {
|
||||
} else if (preference.include === 'follows') {
|
||||
if (preference.list && preference.push) {
|
||||
return _(msg`In-app, Push, People you follow`)
|
||||
} else if (preference.list) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type CommonNavigatorParams,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -81,6 +82,7 @@ export function SettingsScreen({}: Props) {
|
||||
const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
|
||||
const [showAccounts, setShowAccounts] = useState(false)
|
||||
const [showDevOptions, setShowDevOptions] = useState(false)
|
||||
const gate = useGate()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
@@ -181,14 +183,16 @@ export function SettingsScreen({}: Props) {
|
||||
<Trans>Moderation</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/notifications"
|
||||
label={_(msg`Notifications`)}>
|
||||
<SettingsList.ItemIcon icon={NotificationIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Notifications</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
{gate('reengagement_features') && (
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/notifications"
|
||||
label={_(msg`Notifications`)}>
|
||||
<SettingsList.ItemIcon icon={NotificationIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Notifications</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
)}
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/content-and-media"
|
||||
label={_(msg`Content and media`)}>
|
||||
|
||||
@@ -525,8 +525,8 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||
isVerified
|
||||
? _(msg`Update to ${domain}`)
|
||||
: dnsPanel
|
||||
? _(msg`Verify DNS Record`)
|
||||
: _(msg`Verify Text File`)
|
||||
? _(msg`Verify DNS Record`)
|
||||
: _(msg`Verify Text File`)
|
||||
}
|
||||
variant="solid"
|
||||
size="large"
|
||||
|
||||
Vendored
+2
-2
@@ -144,8 +144,8 @@ function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
|
||||
'status' in shadow
|
||||
? shadow.status
|
||||
: 'status' in profile
|
||||
? profile.status
|
||||
: undefined,
|
||||
? profile.status
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import EventEmitter from 'eventemitter3'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {Logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {
|
||||
@@ -130,7 +131,7 @@ export class Convo {
|
||||
|
||||
getSnapshot(): ConvoState {
|
||||
if (!this.snapshot) this.snapshot = this.generateSnapshot()
|
||||
// logger.debug('Convo: snapshotted', {})
|
||||
// logger.debug('snapshotted', {})
|
||||
return this.snapshot
|
||||
}
|
||||
|
||||
@@ -392,7 +393,7 @@ export class Convo {
|
||||
break
|
||||
}
|
||||
|
||||
logger.debug(`Convo: dispatch '${action.event}'`, {
|
||||
logger.debug(`dispatch '${action.event}'`, {
|
||||
id: this.id,
|
||||
prev: prevStatus,
|
||||
next: this.status,
|
||||
@@ -467,13 +468,13 @@ export class Convo {
|
||||
* Some validation prior to `Ready` status
|
||||
*/
|
||||
if (!this.convo) {
|
||||
throw new Error('Convo: could not find convo')
|
||||
throw new Error('could not find convo')
|
||||
}
|
||||
if (!this.sender) {
|
||||
throw new Error('Convo: could not find sender in convo')
|
||||
throw new Error('could not find sender in convo')
|
||||
}
|
||||
if (!this.recipients) {
|
||||
throw new Error('Convo: could not find recipients in convo')
|
||||
throw new Error('could not find recipients in convo')
|
||||
}
|
||||
|
||||
const userIsDisabled = Boolean(this.sender.chatDisabled)
|
||||
@@ -484,7 +485,11 @@ export class Convo {
|
||||
this.dispatch({event: ConvoDispatchEvent.Ready})
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e, {message: 'Convo: setup failed'})
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error('setup failed', {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
event: ConvoDispatchEvent.Error,
|
||||
@@ -589,7 +594,11 @@ export class Convo {
|
||||
this.sender = sender || this.sender
|
||||
this.recipients = recipients || this.recipients
|
||||
} catch (e: any) {
|
||||
logger.error(e, {message: `Convo: failed to refresh convo`})
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error(`failed to refresh convo`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +608,7 @@ export class Convo {
|
||||
}
|
||||
| undefined
|
||||
async fetchMessageHistory() {
|
||||
logger.debug('Convo: fetch message history', {})
|
||||
logger.debug('fetch message history', {})
|
||||
|
||||
/*
|
||||
* If oldestRev is null, we've fetched all history.
|
||||
@@ -653,7 +662,11 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('Convo: failed to fetch message history')
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error('failed to fetch message history', {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
|
||||
this.fetchMessageHistoryError = {
|
||||
retry: () => {
|
||||
@@ -802,7 +815,7 @@ export class Convo {
|
||||
// Ignore empty messages for now since they have no other purpose atm
|
||||
if (!message.text.trim() && !message.embed) return
|
||||
|
||||
logger.debug('Convo: send message', {})
|
||||
logger.debug('send message', {})
|
||||
|
||||
const tempId = nanoid()
|
||||
|
||||
@@ -836,7 +849,7 @@ export class Convo {
|
||||
|
||||
async processPendingMessages() {
|
||||
logger.debug(
|
||||
`Convo: processing messages (${this.pendingMessages.size} remaining)`,
|
||||
`processing messages (${this.pendingMessages.size} remaining)`,
|
||||
{},
|
||||
)
|
||||
|
||||
@@ -881,7 +894,6 @@ export class Convo {
|
||||
// continue queue processing
|
||||
await this.processPendingMessages()
|
||||
} catch (e: any) {
|
||||
logger.error(e, {message: `Convo: failed to send message`})
|
||||
this.handleSendMessageFailure(e)
|
||||
this.isProcessingPendingMessages = false
|
||||
}
|
||||
@@ -914,21 +926,23 @@ export class Convo {
|
||||
case 'recipient has disabled incoming messages':
|
||||
break
|
||||
default:
|
||||
logger.warn(
|
||||
`Convo handleSendMessageFailure could not handle error`,
|
||||
{
|
||||
if (!isNetworkError(e)) {
|
||||
logger.warn(`handleSendMessageFailure could not handle error`, {
|
||||
status: e.status,
|
||||
message: e.message,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.pendingMessageFailure = 'unrecoverable'
|
||||
logger.error(e, {
|
||||
message: `Convo handleSendMessageFailure received unknown error`,
|
||||
})
|
||||
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error(`handleSendMessageFailure received unknown error`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.commit()
|
||||
@@ -944,7 +958,7 @@ export class Convo {
|
||||
this.commit()
|
||||
|
||||
logger.debug(
|
||||
`Convo: batch retrying ${this.pendingMessages.size} pending messages`,
|
||||
`batch retrying ${this.pendingMessages.size} pending messages`,
|
||||
{},
|
||||
)
|
||||
|
||||
@@ -977,18 +991,14 @@ export class Convo {
|
||||
|
||||
this.commit()
|
||||
|
||||
logger.debug(
|
||||
`Convo: sent ${this.pendingMessages.size} pending messages`,
|
||||
{},
|
||||
)
|
||||
logger.debug(`sent ${this.pendingMessages.size} pending messages`, {})
|
||||
} catch (e: any) {
|
||||
logger.error(e, {message: `Convo: failed to batch retry messages`})
|
||||
this.handleSendMessageFailure(e)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMessage(messageId: string) {
|
||||
logger.debug('Convo: delete message', {})
|
||||
logger.debug('delete message', {})
|
||||
|
||||
this.deletedMessages.add(messageId)
|
||||
this.commit()
|
||||
@@ -1004,7 +1014,11 @@ export class Convo {
|
||||
)
|
||||
})
|
||||
} catch (e: any) {
|
||||
logger.error(e, {message: `Convo: failed to delete message`})
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error(`failed to delete message`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
this.deletedMessages.delete(messageId)
|
||||
this.commit()
|
||||
throw e
|
||||
@@ -1232,7 +1246,7 @@ export class Convo {
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Adding reaction ${emoji} to message ${messageId}`)
|
||||
logger.debug(`Adding reaction ${emoji} to message ${messageId}`)
|
||||
const {data} = await this.agent.chat.bsky.convo.addReaction(
|
||||
{messageId, value: emoji, convoId: this.convoId},
|
||||
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
||||
@@ -1297,7 +1311,7 @@ export class Convo {
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Removing reaction ${emoji} from message ${messageId}`)
|
||||
logger.debug(`Removing reaction ${emoji} from message ${messageId}`)
|
||||
await this.agent.chat.bsky.convo.removeReaction(
|
||||
{messageId, value: emoji, convoId: this.convoId},
|
||||
{encoding: 'application/json', headers: DM_SERVICE_HEADERS},
|
||||
|
||||
@@ -3,6 +3,7 @@ import EventEmitter from 'eventemitter3'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {Logger} from '#/logger'
|
||||
import {
|
||||
BACKGROUND_POLL_INTERVAL,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
} from '#/state/messages/events/types'
|
||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||
|
||||
const LOGGER_CONTEXT = 'MessagesEventBus'
|
||||
const logger = Logger.create(Logger.Context.DMsAgent)
|
||||
|
||||
export class MessagesEventBus {
|
||||
@@ -91,17 +91,17 @@ export class MessagesEventBus {
|
||||
}
|
||||
|
||||
background() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: background`, {})
|
||||
logger.debug(`background`, {})
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Background})
|
||||
}
|
||||
|
||||
suspend() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: suspend`, {})
|
||||
logger.debug(`suspend`, {})
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Suspend})
|
||||
}
|
||||
|
||||
resume() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: resume`, {})
|
||||
logger.debug(`resume`, {})
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Resume})
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export class MessagesEventBus {
|
||||
break
|
||||
}
|
||||
|
||||
logger.debug(`${LOGGER_CONTEXT}: dispatch '${action.event}'`, {
|
||||
logger.debug(`dispatch '${action.event}'`, {
|
||||
id: this.id,
|
||||
prev: prevStatus,
|
||||
next: this.status,
|
||||
@@ -236,7 +236,7 @@ export class MessagesEventBus {
|
||||
}
|
||||
|
||||
private async init() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: init`, {})
|
||||
logger.debug(`init`, {})
|
||||
|
||||
try {
|
||||
const response = await networkRetry(2, () => {
|
||||
@@ -260,9 +260,11 @@ export class MessagesEventBus {
|
||||
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Ready})
|
||||
} catch (e: any) {
|
||||
logger.error(e, {
|
||||
message: `${LOGGER_CONTEXT}: init failed`,
|
||||
})
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error(`init failed`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
event: MessagesEventBusDispatchEvent.Error,
|
||||
@@ -324,7 +326,7 @@ export class MessagesEventBus {
|
||||
this.isPolling = true
|
||||
|
||||
// logger.debug(
|
||||
// `${LOGGER_CONTEXT}: poll`,
|
||||
// `poll`,
|
||||
// {
|
||||
// requestedPollIntervals: Array.from(
|
||||
// this.requestedPollIntervals.values(),
|
||||
@@ -370,16 +372,14 @@ export class MessagesEventBus {
|
||||
}
|
||||
|
||||
if (needsEmit) {
|
||||
try {
|
||||
this.emitter.emit('event', {type: 'logs', logs: batch})
|
||||
} catch (e: any) {
|
||||
logger.error(e, {
|
||||
message: `${LOGGER_CONTEXT}: process latest events`,
|
||||
})
|
||||
}
|
||||
this.emitter.emit('event', {type: 'logs', logs: batch})
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e, {message: `${LOGGER_CONTEXT}: poll events failed`})
|
||||
if (!isNetworkError(e)) {
|
||||
logger.error(`poll events failed`, {
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
event: MessagesEventBusDispatchEvent.Error,
|
||||
|
||||
@@ -43,13 +43,6 @@ export interface ChangePasswordModal {
|
||||
name: 'change-password'
|
||||
}
|
||||
|
||||
export interface LinkWarningModal {
|
||||
name: 'link-warning'
|
||||
text: string
|
||||
href: string
|
||||
share?: boolean
|
||||
}
|
||||
|
||||
export type Modal =
|
||||
// Account
|
||||
| DeleteAccountModal
|
||||
@@ -67,9 +60,6 @@ export type Modal =
|
||||
| WaitlistModal
|
||||
| InviteCodesModal
|
||||
|
||||
// Generic
|
||||
| LinkWarningModal
|
||||
|
||||
const ModalContext = React.createContext<{
|
||||
isModalActive: boolean
|
||||
activeModals: Modal[]
|
||||
|
||||
@@ -36,38 +36,27 @@ const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
|
||||
const LIMIT = 8 // sliced to 6, overfetch to account for moderation
|
||||
const PINNED_POST_URIS: Record<string, boolean> = {
|
||||
// 📰 News
|
||||
'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b':
|
||||
true,
|
||||
'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b': true,
|
||||
// Gardening
|
||||
'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27':
|
||||
true,
|
||||
'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27': true,
|
||||
// Web Development Trending
|
||||
'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22':
|
||||
true,
|
||||
'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22': true,
|
||||
// Anime & Manga EN
|
||||
'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y':
|
||||
true,
|
||||
'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y': true,
|
||||
// 📽️ Film
|
||||
'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g':
|
||||
true,
|
||||
'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g': true,
|
||||
// PopSky
|
||||
'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g':
|
||||
true,
|
||||
'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g': true,
|
||||
// Science
|
||||
'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s':
|
||||
true,
|
||||
'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s': true,
|
||||
// Birds! 🦉
|
||||
'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d':
|
||||
true,
|
||||
'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d': true,
|
||||
// Astronomy
|
||||
'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w':
|
||||
true,
|
||||
'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w': true,
|
||||
// What's Cooking 🍽️
|
||||
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q':
|
||||
true,
|
||||
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q': true,
|
||||
// BookSky 💙📚 #booksky
|
||||
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e':
|
||||
true,
|
||||
'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e': true,
|
||||
}
|
||||
|
||||
export type FeedPreviewItem =
|
||||
|
||||
@@ -14,7 +14,9 @@ import * as Toast from '#/view/com/util/Toast'
|
||||
const RQKEY_ROOT = 'notification-settings'
|
||||
const RQKEY = [RQKEY_ROOT]
|
||||
|
||||
export function useNotificationSettingsQuery() {
|
||||
export function useNotificationSettingsQuery({
|
||||
enabled,
|
||||
}: {enabled?: boolean} = {}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
@@ -23,6 +25,7 @@ export function useNotificationSettingsQuery() {
|
||||
const response = await agent.app.bsky.notification.getPreferences()
|
||||
return response.data.preferences
|
||||
},
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
export function useNotificationSettingsUpdateMutation() {
|
||||
@@ -33,9 +36,8 @@ export function useNotificationSettingsUpdateMutation() {
|
||||
mutationFn: async (
|
||||
update: Partial<AppBskyNotificationDefs.Preferences>,
|
||||
) => {
|
||||
const response = await agent.app.bsky.notification.putPreferencesV2(
|
||||
update,
|
||||
)
|
||||
const response =
|
||||
await agent.app.bsky.notification.putPreferencesV2(update)
|
||||
return response.data.preferences
|
||||
},
|
||||
onMutate: update => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {useAgent, useSession} from '#/state/session'
|
||||
import {useModerationOpts} from '../../preferences/moderation-opts'
|
||||
import {truncateAndInvalidate} from '../util'
|
||||
import {RQKEY as RQKEY_NOTIFS} from './feed'
|
||||
import {CachedFeedPage, FeedPage} from './types'
|
||||
import {type CachedFeedPage, type FeedPage} from './types'
|
||||
import {fetchPage} from './util'
|
||||
|
||||
const UPDATE_INTERVAL = 30 * 1e3 // 30sec
|
||||
@@ -94,8 +94,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
data.event === '30+'
|
||||
? 30
|
||||
: data.event === ''
|
||||
? 0
|
||||
: parseInt(data.event, 10) || 1,
|
||||
? 0
|
||||
: parseInt(data.event, 10) || 1,
|
||||
}
|
||||
setNumUnread(data.event)
|
||||
}
|
||||
@@ -164,8 +164,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
unreadCount >= 30
|
||||
? '30+'
|
||||
: unreadCount === 0
|
||||
? ''
|
||||
: String(unreadCount)
|
||||
? ''
|
||||
: String(unreadCount)
|
||||
|
||||
// track last sync
|
||||
const now = new Date()
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react'
|
||||
import {
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPostgate,
|
||||
AtUri,
|
||||
BskyAgent,
|
||||
type BskyAgent,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -139,7 +139,7 @@ export function usePostgateQuery({postUri}: {postUri: string}) {
|
||||
staleTime: STALE.SECONDS.THIRTY,
|
||||
queryKey: createPostgateQueryKey(postUri),
|
||||
async queryFn() {
|
||||
return (await getPostgateRecord({agent, postUri})) ?? null
|
||||
return await getPostgateRecord({agent, postUri}).then(res => res ?? null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ export function usePostThread({anchor}: {anchor?: string}) {
|
||||
return view === 'linear'
|
||||
? LINEAR_VIEW_BELOW
|
||||
: isWeb && gtPhone
|
||||
? TREE_VIEW_BELOW_DESKTOP
|
||||
: TREE_VIEW_BELOW
|
||||
? TREE_VIEW_BELOW_DESKTOP
|
||||
: TREE_VIEW_BELOW
|
||||
}, [view, gtPhone])
|
||||
|
||||
const postThreadQueryKey = createPostThreadQueryKey({
|
||||
|
||||
@@ -444,7 +444,7 @@ export function buildThread({
|
||||
const anchorPost = items.at(0)
|
||||
const hasAnchorFromCache = anchorPost && anchorPost.type === 'threadPost'
|
||||
const skeletonReplies = hasAnchorFromCache
|
||||
? anchorPost.value.post.replyCount ?? 4
|
||||
? (anchorPost.value.post.replyCount ?? 4)
|
||||
: 4
|
||||
|
||||
if (!items.length) {
|
||||
|
||||
+5
-13
@@ -1,7 +1,7 @@
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {MMKV} from 'react-native-mmkv'
|
||||
|
||||
import {Account, Device} from '#/storage/schema'
|
||||
import {type Account, type Device} from '#/storage/schema'
|
||||
|
||||
export * from '#/storage/schema'
|
||||
|
||||
@@ -83,18 +83,10 @@ export class Storage<Scopes extends unknown[], Schema> {
|
||||
}
|
||||
}
|
||||
|
||||
type StorageSchema<T extends Storage<any, any>> = T extends Storage<
|
||||
any,
|
||||
infer U
|
||||
>
|
||||
? U
|
||||
: never
|
||||
type StorageScopes<T extends Storage<any, any>> = T extends Storage<
|
||||
infer S,
|
||||
any
|
||||
>
|
||||
? S
|
||||
: never
|
||||
type StorageSchema<T extends Storage<any, any>> =
|
||||
T extends Storage<any, infer U> ? U : never
|
||||
type StorageScopes<T extends Storage<any, any>> =
|
||||
T extends Storage<infer S, any> ? S : never
|
||||
|
||||
/**
|
||||
* Hook to use a storage instance. Acts like a useState hook, but persists the
|
||||
|
||||
+2
-1
@@ -328,7 +328,8 @@ input[type='range'][orient='vertical']::-moz-range-thumb {
|
||||
|
||||
/* #/components/Select/index.web.tsx */
|
||||
.radix-select-content {
|
||||
box-shadow: 0px 6px 24px -10px rgba(22, 23, 24, 0.25),
|
||||
box-shadow:
|
||||
0px 6px 24px -10px rgba(22, 23, 24, 0.25),
|
||||
0px 6px 12px -12px rgba(22, 23, 24, 0.15);
|
||||
min-width: var(--radix-select-trigger-width);
|
||||
max-height: var(--radix-select-content-available-height);
|
||||
|
||||
@@ -518,8 +518,8 @@ export const ComposePost = ({
|
||||
thread.posts.length > 1
|
||||
? _(msg`Your posts have been published`)
|
||||
: replyTo
|
||||
? _(msg`Your reply has been published`)
|
||||
: _(msg`Your post has been published`),
|
||||
? _(msg`Your reply has been published`)
|
||||
: _(msg`Your post has been published`),
|
||||
)
|
||||
}, [
|
||||
_,
|
||||
@@ -1000,20 +1000,20 @@ function ComposerTopBar({
|
||||
}),
|
||||
)
|
||||
: isThread
|
||||
? _(
|
||||
msg({
|
||||
message: 'Publish posts',
|
||||
comment:
|
||||
'Accessibility label for button to publish multiple posts in a thread',
|
||||
}),
|
||||
)
|
||||
: _(
|
||||
msg({
|
||||
message: 'Publish post',
|
||||
comment:
|
||||
'Accessibility label for button to publish a single post',
|
||||
}),
|
||||
)
|
||||
? _(
|
||||
msg({
|
||||
message: 'Publish posts',
|
||||
comment:
|
||||
'Accessibility label for button to publish multiple posts in a thread',
|
||||
}),
|
||||
)
|
||||
: _(
|
||||
msg({
|
||||
message: 'Publish post',
|
||||
comment:
|
||||
'Accessibility label for button to publish a single post',
|
||||
}),
|
||||
)
|
||||
}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
ImageStyle,
|
||||
type ImageStyle,
|
||||
Keyboard,
|
||||
LayoutChangeEvent,
|
||||
type LayoutChangeEvent,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
@@ -14,14 +14,14 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {Dimensions} from '#/lib/media/types'
|
||||
import {type Dimensions} from '#/lib/media/types'
|
||||
import {colors, s} from '#/lib/styles'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {ComposerImage, cropImage} from '#/state/gallery'
|
||||
import {type ComposerImage, cropImage} from '#/state/gallery'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {PostAction} from '../state/composer'
|
||||
import {type PostAction} from '../state/composer'
|
||||
import {EditImageDialog} from './EditImageDialog'
|
||||
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
||||
|
||||
@@ -74,8 +74,8 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
altTextControlStyle: isOverflow
|
||||
? {left: 4, bottom: 4}
|
||||
: !isMobile && images.length < 3
|
||||
? {left: 8, top: 8}
|
||||
: {left: 4, top: 4},
|
||||
? {left: 8, top: 8}
|
||||
: {left: 4, top: 4},
|
||||
imageControlsStyle: {
|
||||
display: 'flex' as const,
|
||||
flexDirection: 'row' as const,
|
||||
@@ -83,8 +83,8 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
...(isOverflow
|
||||
? {top: 4, right: 4, gap: 4}
|
||||
: !isMobile && images.length < 3
|
||||
? {top: 8, right: 8, gap: 8}
|
||||
: {top: 4, right: 4, gap: 4}),
|
||||
? {top: 8, right: 8, gap: 8}
|
||||
: {top: 4, right: 4, gap: 4}),
|
||||
zIndex: 1,
|
||||
},
|
||||
imageStyle: {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||
import {
|
||||
AppBskyFeedPostgate,
|
||||
type AppBskyFeedPostgate,
|
||||
AppBskyRichtextFacet,
|
||||
BskyPreferences,
|
||||
type BskyPreferences,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {SelfLabel} from '#/lib/moderation'
|
||||
import {type SelfLabel} from '#/lib/moderation'
|
||||
import {insertMentionAt} from '#/lib/strings/mention-manip'
|
||||
import {shortenLinks} from '#/lib/strings/rich-text-manip'
|
||||
import {
|
||||
@@ -15,17 +15,22 @@ import {
|
||||
postUriToRelativePath,
|
||||
toBskyAppUrl,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {ComposerImage, createInitialImages} from '#/state/gallery'
|
||||
import {type ComposerImage, createInitialImages} from '#/state/gallery'
|
||||
import {createPostgateRecord} from '#/state/queries/postgate/util'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {type Gif} from '#/state/queries/tenor'
|
||||
import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate'
|
||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||
import {type ComposerOpts} from '#/state/shell/composer'
|
||||
import {
|
||||
LinkFacetMatch,
|
||||
type LinkFacetMatch,
|
||||
suggestLinkCardUri,
|
||||
} from '#/view/com/composer/text-input/text-input-util'
|
||||
import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
|
||||
import {
|
||||
createVideoState,
|
||||
type VideoAction,
|
||||
videoReducer,
|
||||
type VideoState,
|
||||
} from './video'
|
||||
|
||||
type ImagesMedia = {
|
||||
type: 'images'
|
||||
@@ -514,12 +519,12 @@ export function createComposerState({
|
||||
text: initText
|
||||
? initText
|
||||
: initMention
|
||||
? insertMentionAt(
|
||||
`@${initMention}`,
|
||||
initMention.length + 1,
|
||||
`${initMention}`,
|
||||
)
|
||||
: '',
|
||||
? insertMentionAt(
|
||||
`@${initMention}`,
|
||||
initMention.length + 1,
|
||||
`${initMention}`,
|
||||
)
|
||||
: '',
|
||||
})
|
||||
|
||||
let link: Link | undefined
|
||||
|
||||
@@ -209,8 +209,8 @@ function AutocompleteProfileCard({
|
||||
itemIndex === 0
|
||||
? styles.firstMention
|
||||
: itemIndex === totalItems - 1
|
||||
? styles.lastMention
|
||||
: undefined,
|
||||
? styles.lastMention
|
||||
: undefined,
|
||||
]}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {
|
||||
Image,
|
||||
ImageStyle,
|
||||
type ImageStyle,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
ViewStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
type FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -21,7 +21,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {colors, s} from '#/lib/styles'
|
||||
import {useLightbox, useLightboxControls} from '#/state/lightbox'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ImageSource} from './ImageViewing/@types'
|
||||
import {type ImageSource} from './ImageViewing/@types'
|
||||
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
|
||||
|
||||
export function Lightbox() {
|
||||
@@ -121,8 +121,8 @@ function LightboxInner({
|
||||
img.type === 'circle-avi'
|
||||
? '50%'
|
||||
: img.type === 'rect-avi'
|
||||
? '10%'
|
||||
: 0,
|
||||
? '10%'
|
||||
: 0,
|
||||
} as ImageStyle
|
||||
}
|
||||
alt={img.alt}
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Animated, {FadeOut} from 'react-native-reanimated'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {MAX_DESCRIPTION, MAX_DISPLAY_NAME, urls} from '#/lib/constants'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {compressIfNeeded} from '#/lib/media/manip'
|
||||
import {type PickerImage} from '#/lib/media/picker.shared'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
import {colors, gradients, s} from '#/lib/styles'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useProfileUpdateMutation} from '#/state/queries/profile'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {useSimpleVerificationState} from '#/components/verification'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
|
||||
const AnimatedTouchableOpacity =
|
||||
Animated.createAnimatedComponent(TouchableOpacity)
|
||||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
||||
export function Component({
|
||||
profile,
|
||||
onUpdate,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
onUpdate?: () => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const updateMutation = useProfileUpdateMutation()
|
||||
const [imageError, setImageError] = useState<string>('')
|
||||
const initialDisplayName = profile.displayName || ''
|
||||
const [displayName, setDisplayName] = useState<string>(
|
||||
profile.displayName || '',
|
||||
)
|
||||
const [description, setDescription] = useState<string>(
|
||||
profile.description || '',
|
||||
)
|
||||
const [userBanner, setUserBanner] = useState<string | undefined | null>(
|
||||
profile.banner,
|
||||
)
|
||||
const [userAvatar, setUserAvatar] = useState<string | undefined | null>(
|
||||
profile.avatar,
|
||||
)
|
||||
const [newUserBanner, setNewUserBanner] = useState<
|
||||
PickerImage | undefined | null
|
||||
>()
|
||||
const [newUserAvatar, setNewUserAvatar] = useState<
|
||||
PickerImage | undefined | null
|
||||
>()
|
||||
const onPressCancel = () => {
|
||||
closeModal()
|
||||
}
|
||||
const onSelectNewAvatar = useCallback(
|
||||
async (img: PickerImage | null) => {
|
||||
setImageError('')
|
||||
if (img === null) {
|
||||
setNewUserAvatar(null)
|
||||
setUserAvatar(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserAvatar(finalImg)
|
||||
setUserAvatar(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[setNewUserAvatar, setUserAvatar, setImageError],
|
||||
)
|
||||
|
||||
const onSelectNewBanner = useCallback(
|
||||
async (img: PickerImage | null) => {
|
||||
setImageError('')
|
||||
if (!img) {
|
||||
setNewUserBanner(null)
|
||||
setUserBanner(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserBanner(finalImg)
|
||||
setUserBanner(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[setNewUserBanner, setUserBanner, setImageError],
|
||||
)
|
||||
|
||||
const onPressSave = useCallback(async () => {
|
||||
setImageError('')
|
||||
try {
|
||||
await updateMutation.mutateAsync({
|
||||
profile,
|
||||
updates: {
|
||||
displayName,
|
||||
description,
|
||||
},
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
})
|
||||
Toast.show(_(msg({message: 'Profile updated', context: 'toast'})))
|
||||
onUpdate?.()
|
||||
closeModal()
|
||||
} catch (e: any) {
|
||||
logger.error('Failed to update user profile', {message: String(e)})
|
||||
}
|
||||
}, [
|
||||
updateMutation,
|
||||
profile,
|
||||
onUpdate,
|
||||
closeModal,
|
||||
displayName,
|
||||
description,
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
setImageError,
|
||||
_,
|
||||
])
|
||||
const verification = useSimpleVerificationState({
|
||||
profile,
|
||||
})
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={s.flex1} behavior="height">
|
||||
<ScrollView style={[pal.view]} testID="editProfileModal">
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
<Trans>Edit my profile</Trans>
|
||||
</Text>
|
||||
<View style={styles.photos}>
|
||||
<UserBanner
|
||||
banner={userBanner}
|
||||
onSelectNewBanner={onSelectNewBanner}
|
||||
/>
|
||||
<View style={[styles.avi, {borderColor: pal.colors.background}]}>
|
||||
<EditableUserAvatar
|
||||
size={80}
|
||||
avatar={userAvatar}
|
||||
onSelectNewAvatar={onSelectNewAvatar}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{updateMutation.isError && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={cleanError(updateMutation.error)} />
|
||||
</View>
|
||||
)}
|
||||
{imageError !== '' && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={imageError} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.form}>
|
||||
<View>
|
||||
<Text style={[styles.label, pal.text]}>
|
||||
<Trans>Display Name</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editProfileDisplayNameInput"
|
||||
style={[styles.textInput, pal.border, pal.text]}
|
||||
placeholder={_(msg`e.g. Alice Roberts`)}
|
||||
placeholderTextColor={colors.gray4}
|
||||
value={displayName}
|
||||
onChangeText={v =>
|
||||
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
||||
}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Display name`)}
|
||||
accessibilityHint={_(msg`Edit your display name`)}
|
||||
/>
|
||||
|
||||
{verification.isVerified &&
|
||||
verification.role === 'default' &&
|
||||
displayName !== initialDisplayName && (
|
||||
<View style={{paddingTop: 8}}>
|
||||
<Admonition type="error">
|
||||
<Trans>
|
||||
You are verified. You will lose your verification status
|
||||
if you change your display name.{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Learn more`)}
|
||||
to={urls.website.blog.initialVerificationAnnouncement}>
|
||||
<Trans>Learn more.</Trans>
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={s.pb10}>
|
||||
<Text style={[styles.label, pal.text]}>
|
||||
<Trans>Description</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editProfileDescriptionInput"
|
||||
style={[styles.textArea, pal.border, pal.text]}
|
||||
placeholder={_(msg`e.g. Artist, dog-lover, and avid reader.`)}
|
||||
placeholderTextColor={colors.gray4}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
multiline
|
||||
value={description}
|
||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Description`)}
|
||||
accessibilityHint={_(msg`Edit your profile description`)}
|
||||
/>
|
||||
</View>
|
||||
{updateMutation.isPending ? (
|
||||
<View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="editProfileSaveBtn"
|
||||
style={s.mt10}
|
||||
onPress={onPressSave}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Save`)}
|
||||
accessibilityHint={_(msg`Saves any changes to your profile`)}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold]}>
|
||||
<Trans>Save Changes</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!updateMutation.isPending && (
|
||||
<AnimatedTouchableOpacity
|
||||
exiting={!isWeb ? FadeOut : undefined}
|
||||
testID="editProfileCancelBtn"
|
||||
style={s.mt5}
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Cancel profile editing`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}>
|
||||
<View style={[styles.btn]}>
|
||||
<Text style={[s.black, s.bold, pal.text]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</AnimatedTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
fontSize: 24,
|
||||
marginBottom: 18,
|
||||
},
|
||||
label: {
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 4,
|
||||
marginTop: 20,
|
||||
},
|
||||
form: {
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
textInput: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
},
|
||||
textArea: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 10,
|
||||
fontSize: 16,
|
||||
height: 120,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
borderRadius: 32,
|
||||
padding: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
avi: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
left: 24,
|
||||
width: 84,
|
||||
height: 84,
|
||||
borderWidth: 2,
|
||||
borderRadius: 42,
|
||||
},
|
||||
photos: {
|
||||
marginBottom: 36,
|
||||
marginHorizontal: -14,
|
||||
},
|
||||
errorContainer: {marginTop: 20},
|
||||
})
|
||||
@@ -1,180 +0,0 @@
|
||||
import React from 'react'
|
||||
import {SafeAreaView, StyleSheet, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {isPossiblyAUrl, splitApexDomain} from '#/lib/strings/url-helpers'
|
||||
import {colors, s} from '#/lib/styles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {Button} from '#/view/com/util/forms/Button'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {ScrollView} from './util'
|
||||
|
||||
export const snapPoints = ['50%']
|
||||
|
||||
export function Component({
|
||||
text,
|
||||
href,
|
||||
share,
|
||||
}: {
|
||||
text: string
|
||||
href: string
|
||||
share?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {_} = useLingui()
|
||||
const potentiallyMisleading = isPossiblyAUrl(text)
|
||||
const openLink = useOpenLink()
|
||||
|
||||
const onPressVisit = () => {
|
||||
closeModal()
|
||||
if (share) {
|
||||
shareUrl(href)
|
||||
} else {
|
||||
openLink(href, false, true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[s.flex1, pal.view]}>
|
||||
<ScrollView
|
||||
testID="linkWarningModal"
|
||||
style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
|
||||
<View style={styles.titleSection}>
|
||||
{potentiallyMisleading ? (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon="circle-exclamation"
|
||||
color={pal.colors.text}
|
||||
size={18}
|
||||
/>
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
<Trans>Potentially Misleading Link</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text type="title-lg" style={[pal.text, styles.title]}>
|
||||
<Trans>Leaving Bluesky</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{gap: 10}}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>This link is taking you to the following website:</Trans>
|
||||
</Text>
|
||||
|
||||
<LinkBox href={href} />
|
||||
|
||||
{potentiallyMisleading && (
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Make sure this is where you intend to go!</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={[styles.btnContainer, isMobile && {paddingBottom: 40}]}>
|
||||
<Button
|
||||
testID="confirmBtn"
|
||||
type="primary"
|
||||
onPress={onPressVisit}
|
||||
accessibilityLabel={share ? _(msg`Share Link`) : _(msg`Visit Site`)}
|
||||
accessibilityHint={
|
||||
share
|
||||
? _(msg`Shares the linked website`)
|
||||
: _(msg`Opens the linked website`)
|
||||
}
|
||||
label={share ? _(msg`Share Link`) : _(msg`Visit Site`)}
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
labelStyle={[s.f18]}
|
||||
/>
|
||||
<Button
|
||||
testID="cancelBtn"
|
||||
type="default"
|
||||
onPress={() => {
|
||||
closeModal()
|
||||
}}
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint={_(msg`Cancels opening the linked website`)}
|
||||
label={_(msg`Cancel`)}
|
||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
||||
labelStyle={[s.f18]}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
function LinkBox({href}: {href: string}) {
|
||||
const pal = usePalette('default')
|
||||
const [scheme, hostname, rest] = React.useMemo(() => {
|
||||
try {
|
||||
const urlp = new URL(href)
|
||||
const [subdomain, apexdomain] = splitApexDomain(urlp.hostname)
|
||||
return [
|
||||
urlp.protocol + '//' + subdomain,
|
||||
apexdomain,
|
||||
urlp.pathname + urlp.search + urlp.hash,
|
||||
]
|
||||
} catch {
|
||||
return ['', href, '']
|
||||
}
|
||||
}, [href])
|
||||
return (
|
||||
<View style={[pal.view, pal.border, styles.linkBox]}>
|
||||
<Text type="lg" style={pal.textLight}>
|
||||
{scheme}
|
||||
<Text type="lg-bold" style={pal.text}>
|
||||
{hostname}
|
||||
</Text>
|
||||
{rest}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: isWeb ? 0 : 40,
|
||||
},
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingTop: isWeb ? 0 : 4,
|
||||
paddingBottom: isWeb ? 14 : 10,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
},
|
||||
linkBox: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 32,
|
||||
padding: 14,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
btnContainer: {
|
||||
paddingTop: 20,
|
||||
gap: 6,
|
||||
},
|
||||
})
|
||||
@@ -13,7 +13,6 @@ import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as LinkWarningModal from './LinkWarning'
|
||||
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
||||
|
||||
const DEFAULT_SNAPPOINTS = ['90%']
|
||||
@@ -68,9 +67,6 @@ export function ModalsContainer() {
|
||||
} else if (activeModal?.name === 'change-password') {
|
||||
snapPoints = ChangePasswordModal.snapPoints
|
||||
element = <ChangePasswordModal.Component />
|
||||
} else if (activeModal?.name === 'link-warning') {
|
||||
snapPoints = LinkWarningModal.snapPoints
|
||||
element = <LinkWarningModal.Component {...activeModal} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
import * as LinkWarningModal from './LinkWarning'
|
||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||
|
||||
export function ModalsContainer() {
|
||||
@@ -65,8 +64,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||
element = <PostLanguagesSettingsModal.Component />
|
||||
} else if (modal.name === 'change-password') {
|
||||
element = <ChangePasswordModal.Component />
|
||||
} else if (modal.name === 'link-warning') {
|
||||
element = <LinkWarningModal.Component {...modal} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {List, type ListRef} from '#/view/com/util/List'
|
||||
import {List, type ListProps, type ListRef} from '#/view/com/util/List'
|
||||
import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||
import {NotificationFeedItem} from './NotificationFeedItem'
|
||||
@@ -39,7 +39,7 @@ export function NotificationFeed({
|
||||
scrollElRef?: ListRef
|
||||
onPressTryAgain?: () => void
|
||||
onScrolledDownChange: (isScrolledDown: boolean) => void
|
||||
ListHeaderComponent?: () => JSX.Element
|
||||
ListHeaderComponent?: ListProps['ListHeaderComponent']
|
||||
refreshNotifications: () => Promise<void>
|
||||
}) {
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
|
||||
@@ -630,8 +630,8 @@ let PostThreadItemLoaded = ({
|
||||
showChildReplyLine && !isThreadedChild
|
||||
? 0
|
||||
: isThreadedChildAdjacentBot
|
||||
? 4
|
||||
: 8,
|
||||
? 4
|
||||
: 8,
|
||||
},
|
||||
]}>
|
||||
{/* If we are in threaded mode, the avatar is rendered in PostMeta */}
|
||||
|
||||
@@ -132,12 +132,11 @@ type FeedRow =
|
||||
key: string
|
||||
}
|
||||
|
||||
export function getItemsForFeedback(feedRow: FeedRow):
|
||||
| {
|
||||
item: FeedPostSliceItem
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}[] {
|
||||
export function getItemsForFeedback(feedRow: FeedRow): {
|
||||
item: FeedPostSliceItem
|
||||
feedContext: string | undefined
|
||||
reqId: string | undefined
|
||||
}[] {
|
||||
if (feedRow.type === 'sliceItem') {
|
||||
return feedRow.slice.items.map(item => ({
|
||||
item,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, AppBskyFeedGetAuthorFeed, AtUri} from '@atproto/api'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
AppBskyFeedGetAuthorFeed,
|
||||
AtUri,
|
||||
} from '@atproto/api'
|
||||
import {msg as msgLingui, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useRemoveFeedMutation} from '#/state/queries/preferences'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {EmptyState} from '../util/EmptyState'
|
||||
@@ -119,7 +123,7 @@ function FeedgenErrorMessage({
|
||||
[KnownError.FeedTooManyRequests]: _l(
|
||||
msgLingui`This feed is currently receiving high traffic and is temporarily unavailable. Please try again later.`,
|
||||
),
|
||||
}[knownError]),
|
||||
})[knownError],
|
||||
[_l, knownError],
|
||||
)
|
||||
const [_, uri] = feedDesc.split('|')
|
||||
|
||||
@@ -461,12 +461,12 @@ let ProfileMenu = ({
|
||||
msg`The account will be able to interact with you after unblocking.`,
|
||||
)
|
||||
: profile.associated?.labeler
|
||||
? _(
|
||||
msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`,
|
||||
)
|
||||
: _(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
)
|
||||
? _(
|
||||
msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`,
|
||||
)
|
||||
: _(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
)
|
||||
}
|
||||
onConfirm={blockAccount}
|
||||
confirmButtonCta={
|
||||
|
||||
@@ -30,6 +30,7 @@ import {emitSoftReset} from '#/state/events'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper'
|
||||
import {useTheme} from '#/alf'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {router} from '../../../routes'
|
||||
import {PressableWithHover} from './PressableWithHover'
|
||||
import {Text} from './text/Text'
|
||||
@@ -189,7 +190,8 @@ export const TextLink = memo(function TextLink({
|
||||
onBeforePress?: () => void
|
||||
} & TextProps) {
|
||||
const navigation = useNavigationDeduped()
|
||||
const {openModal, closeModal} = useModalControls()
|
||||
const {closeModal} = useModalControls()
|
||||
const {linkWarningDialogControl} = useGlobalDialogsControlContext()
|
||||
const openLink = useOpenLink()
|
||||
|
||||
if (!disableMismatchWarning && typeof text !== 'string') {
|
||||
@@ -211,9 +213,8 @@ export const TextLink = memo(function TextLink({
|
||||
linkRequiresWarning(href, typeof text === 'string' ? text : '')
|
||||
if (requiresWarning) {
|
||||
e?.preventDefault?.()
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: typeof text === 'string' ? text : '',
|
||||
linkWarningDialogControl.open({
|
||||
displayText: typeof text === 'string' ? text : '',
|
||||
href,
|
||||
})
|
||||
}
|
||||
@@ -245,13 +246,13 @@ export const TextLink = memo(function TextLink({
|
||||
onBeforePress,
|
||||
onPressProp,
|
||||
closeModal,
|
||||
openModal,
|
||||
navigation,
|
||||
href,
|
||||
text,
|
||||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
openLink,
|
||||
linkWarningDialogControl,
|
||||
],
|
||||
)
|
||||
const hrefAttrs = useMemo(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {memo} from 'react'
|
||||
import {RefreshControl, ViewToken} from 'react-native'
|
||||
import {RefreshControl, type ViewToken} from 'react-native'
|
||||
import {
|
||||
FlatListPropsWithLayout,
|
||||
type FlatListPropsWithLayout,
|
||||
runOnJS,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
@@ -11,7 +11,7 @@ import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIX
|
||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||
import {addStyle} from '#/lib/styles'
|
||||
import {isAndroid, isIOS} from '#/platform/detection'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useLightbox} from '#/state/lightbox'
|
||||
import {useTheme} from '#/alf'
|
||||
import {FlatList_INTERNAL} from './Views'
|
||||
@@ -152,7 +152,7 @@ let List = React.forwardRef<ListMethods, ListProps>(
|
||||
|
||||
return (
|
||||
<FlatList_INTERNAL
|
||||
showsVerticalScrollIndicator={!isAndroid} // overridable
|
||||
showsVerticalScrollIndicator // overridable
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
{...props}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user