Merge branch 'main' into app-1934

This commit is contained in:
vineyardbovines
2026-04-06 08:44:48 -04:00
43 changed files with 404 additions and 313 deletions
+13
View File
@@ -127,6 +127,7 @@ export default defineConfig(
*/ */
...react.configs.recommended.rules, ...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules, ...react.configs['jsx-runtime'].rules,
'react/hook-use-state': 'warn',
'react/no-unescaped-entities': 'off', 'react/no-unescaped-entities': 'off',
'react/prop-types': 'off', 'react/prop-types': 'off',
'react-native/no-inline-styles': 'off', 'react-native/no-inline-styles': 'off',
@@ -189,6 +190,18 @@ export default defineConfig(
*/ */
ignore: ['^#\/locale\/locales\/.+\/messages'], ignore: ['^#\/locale\/locales\/.+\/messages'],
}], }],
'import-x/no-extraneous-dependencies': ['error', {
'whitelist': [
// test files only
'@jest/globals',
// we only use a really simple util from this, and we know it will be present
'expo-modules-core',
// this is a dep for @atproto/api, but we absolutely need them in sync, so just
// rely on the transient version
'@atproto/common-web',
]
}],
'import-x/no-nodejs-modules': 'error',
/** /**
* TypeScript-specific rules * TypeScript-specific rules
+1
View File
@@ -9,6 +9,7 @@ jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock'), require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
) )
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => { jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
// eslint-disable-next-line import-x/no-nodejs-modules
const {EventEmitter} = require('events') const {EventEmitter} = require('events')
return { return {
__esModule: true, __esModule: true,
-23
View File
@@ -1,23 +0,0 @@
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {render} from '@testing-library/react-native'
import {ThemeProvider} from '../src/lib/ThemeContext'
import {type RootStoreModel, RootStoreProvider} from '../src/state'
const customRender = (ui: any, rootStore: RootStoreModel) =>
render(
<GestureHandlerRootView style={{flex: 1}}>
<RootStoreProvider value={rootStore}>
<ThemeProvider theme="light">
<SafeAreaProvider>{ui}</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</GestureHandlerRootView>,
)
// re-export everything
export * from '@testing-library/react-native'
// override render method
export {customRender as render}
+5 -4
View File
@@ -81,7 +81,7 @@
"icons:optimize": "svgo -f ./assets/icons" "icons:optimize": "svgo -f ./assets/icons"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.19.3", "@atproto/api": "^0.19.5",
"@bitdrift/react-native": "^0.6.8", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.7", "@bsky.app/alf": "^0.1.7",
@@ -116,9 +116,9 @@
"@react-navigation/native": "^7.1.33", "@react-navigation/native": "^7.1.33",
"@react-navigation/native-stack": "^7.14.4", "@react-navigation/native-stack": "^7.14.4",
"@sentry/react-native": "~6.20.0", "@sentry/react-native": "~6.20.0",
"@tanstack/query-async-storage-persister": "^5.95.2", "@tanstack/query-async-storage-persister": "^5.96.2",
"@tanstack/react-query": "^5.95.2", "@tanstack/react-query": "^5.96.2",
"@tanstack/react-query-persist-client": "^5.95.2", "@tanstack/react-query-persist-client": "^5.96.2",
"@tiptap/core": "^2.9.1", "@tiptap/core": "^2.9.1",
"@tiptap/extension-document": "^2.9.1", "@tiptap/extension-document": "^2.9.1",
"@tiptap/extension-hard-break": "^2.9.1", "@tiptap/extension-hard-break": "^2.9.1",
@@ -200,6 +200,7 @@
"react": "19.1.0", "react": "19.1.0",
"react-compiler-runtime": "^19.1.0-rc.1", "react-compiler-runtime": "^19.1.0-rc.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hotkeys-hook": "5.2.4",
"react-image-crop": "^11.0.7", "react-image-crop": "^11.0.7",
"react-is": "19", "react-is": "19",
"react-keyed-flatten-children": "^5.0.0", "react-keyed-flatten-children": "^5.0.0",
+13 -14
View File
@@ -11,8 +11,7 @@ import {
import * as ScreenOrientation from 'expo-screen-orientation' import * as ScreenOrientation from 'expo-screen-orientation'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
import * as SystemUI from 'expo-system-ui' import * as SystemUI from 'expo-system-ui'
import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import * as Sentry from '@sentry/react-native' import * as Sentry from '@sentry/react-native'
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
@@ -89,9 +88,9 @@ import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet' import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
SplashScreen.preventAutoHideAsync() void SplashScreen.preventAutoHideAsync()
if (IS_IOS) { if (IS_IOS) {
SystemUI.setBackgroundColorAsync('black') void SystemUI.setBackgroundColorAsync('black')
} }
if (IS_ANDROID) { if (IS_ANDROID) {
// iOS is handled by the config plugin -sfn // iOS is handled by the config plugin -sfn
@@ -105,17 +104,17 @@ if (IS_ANDROID) {
/** /**
* Begin geolocation ASAP * Begin geolocation ASAP
*/ */
Geo.resolve() void Geo.resolve()
prefetchAgeAssuranceConfig() void prefetchAgeAssuranceConfig()
prefetchLiveEvents() void prefetchLiveEvents()
prefetchAppConfig() void prefetchAppConfig()
function InnerApp() { function InnerApp() {
const [isReady, setIsReady] = useState(false) const [isReady, setIsReady] = useState(false)
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const theme = useColorModeTheme() const theme = useColorModeTheme()
const {_} = useLingui() const {t: l} = useLingui()
const hasCheckedReferrer = useStarterPackEntry() const hasCheckedReferrer = useStarterPackEntry()
// init // init
@@ -134,16 +133,16 @@ function InnerApp() {
} }
} }
const account = readLastActiveAccount() const account = readLastActiveAccount()
onLaunch(account) void onLaunch(account)
}, [resumeSession]) }, [resumeSession])
useEffect(() => { useEffect(() => {
return listenSessionDropped(() => { return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), { Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
type: 'info', type: 'info',
}) })
}) })
}, [_]) }, [l])
return ( return (
<Alf theme={theme}> <Alf theme={theme}>
@@ -220,8 +219,8 @@ function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
useEffect(() => { useEffect(() => {
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() => void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
setReady(true), () => setReady(true),
) )
}, []) }, [])
+17 -15
View File
@@ -5,10 +5,10 @@ import './style.css'
import {Fragment, useEffect, useState} from 'react' import {Fragment, useEffect, useState} from 'react'
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller' import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
import {SafeAreaProvider} from 'react-native-safe-area-context' import {SafeAreaProvider} from 'react-native-safe-area-context'
import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import * as Sentry from '@sentry/react-native' import * as Sentry from '@sentry/react-native'
import {Provider as HotkeysProvider} from '#/lib/hotkeys'
import {QueryProvider} from '#/lib/react-query' import {QueryProvider} from '#/lib/react-query'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import {Provider as TranslateOnDeviceProvider} from '#/lib/translation' import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
@@ -82,17 +82,17 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
/** /**
* Begin geolocation ASAP * Begin geolocation ASAP
*/ */
Geo.resolve() void Geo.resolve()
prefetchAgeAssuranceConfig() void prefetchAgeAssuranceConfig()
prefetchLiveEvents() void prefetchLiveEvents()
prefetchAppConfig() void prefetchAppConfig()
function InnerApp() { function InnerApp() {
const [isReady, setIsReady] = useState(false) const [isReady, setIsReady] = useState(false)
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const theme = useColorModeTheme() const theme = useColorModeTheme()
const {_} = useLingui() const {t: l} = useLingui()
const hasCheckedReferrer = useStarterPackEntry() const hasCheckedReferrer = useStarterPackEntry()
// init // init
@@ -105,22 +105,22 @@ function InnerApp() {
await features.init await features.init
} }
} catch (e) { } catch (e) {
logger.error(`session: resumeSession failed`, {message: e}) logger.error('session: resumeSession failed', {message: e})
} finally { } finally {
setIsReady(true) setIsReady(true)
} }
} }
const account = readLastActiveAccount() const account = readLastActiveAccount()
onLaunch(account) void onLaunch(account)
}, [resumeSession]) }, [resumeSession])
useEffect(() => { useEffect(() => {
return listenSessionDropped(() => { return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), { Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
type: 'info', type: 'info',
}) })
}) })
}, [_]) }, [l])
return ( return (
<Alf theme={theme}> <Alf theme={theme}>
@@ -156,8 +156,10 @@ function InnerApp() {
<HideBottomBarBorderProvider> <HideBottomBarBorderProvider>
<IntentDialogProvider> <IntentDialogProvider>
<TranslateOnDeviceProvider> <TranslateOnDeviceProvider>
<Shell /> <HotkeysProvider>
<ToastOutlet /> <Shell />
<ToastOutlet />
</HotkeysProvider>
</TranslateOnDeviceProvider> </TranslateOnDeviceProvider>
</IntentDialogProvider> </IntentDialogProvider>
</HideBottomBarBorderProvider> </HideBottomBarBorderProvider>
@@ -195,8 +197,8 @@ function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
useEffect(() => { useEffect(() => {
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() => void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
setReady(true), () => setReady(true),
) )
}, []) }, [])
+15 -6
View File
@@ -1,7 +1,7 @@
import {View} from 'react-native' import {useWindowDimensions, View} from 'react-native'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {atoms as a, type ViewStyleProp} from '#/alf' import {atoms as a, useAlf, type ViewStyleProp} from '#/alf'
import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge' import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge'
import {useSimpleVerificationState} from '#/components/verification' import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck' import {VerificationCheck} from '#/components/verification/VerificationCheck'
@@ -38,12 +38,21 @@ export function ProfileBadges({
}) { }) {
const shadowed = useProfileShadow(profile) const shadowed = useProfileShadow(profile)
const verification = useSimpleVerificationState({profile}) const verification = useSimpleVerificationState({profile})
const {fontScale: nativeScaleMultiplier} = useWindowDimensions()
const {
fonts: {scaleMultiplier: alfScaleMultiplier},
} = useAlf()
// if nothing to show, don't render the container at all // if nothing to show, don't render the container at all
if (!verification.showBadge && !isBotAccount(shadowed)) return null if (!verification.showBadge && !isBotAccount(shadowed)) return null
const isOnTheSmallSide = size === 'xs' || size === 'sm' const isOnTheSmallSide = size === 'xs' || size === 'sm'
const verificationIconWidth =
verificationIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
const botIconWidth =
botIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
return ( return (
<View <View
style={[ style={[
@@ -56,19 +65,19 @@ export function ProfileBadges({
<> <>
<VerificationCheckButton <VerificationCheckButton
profile={shadowed} profile={shadowed}
width={verificationIconSizes[size]} width={verificationIconWidth}
/> />
<BotBadgeButton profile={shadowed} width={botIconSizes[size]} /> <BotBadgeButton profile={shadowed} width={botIconWidth} />
</> </>
) : ( ) : (
<> <>
{verification.showBadge && ( {verification.showBadge && (
<VerificationCheck <VerificationCheck
verifier={verification.role === 'verifier'} verifier={verification.role === 'verifier'}
width={verificationIconSizes[size]} width={verificationIconWidth}
/> />
)} )}
<BotBadge profile={shadowed} width={botIconSizes[size]} /> <BotBadge profile={shadowed} width={botIconWidth} />
</> </>
)} )}
</View> </View>
@@ -1,6 +1,6 @@
import {useState} from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {XRPCError} from '@atproto/xrpc' import {XRPCError} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
+1 -1
View File
@@ -1,5 +1,5 @@
import {useEffect} from 'react' import {useEffect} from 'react'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
const events = new EventEmitter<{ const events = new EventEmitter<{
emailVerified: void emailVerified: void
+79 -62
View File
@@ -1,8 +1,9 @@
import {forwardRef} from 'react' import {useEffect, useRef} from 'react'
import {type TextInput, View} from 'react-native' import {type TextInput, View} from 'react-native'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {listenFocusSearch} from '#/state/events'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
@@ -10,73 +11,89 @@ import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
type SearchInputProps = Omit<TextField.InputProps, 'label'> & { type Props = Omit<TextField.InputProps, 'label'> & {
label?: TextField.InputProps['label'] label?: TextField.InputProps['label']
/** /**
* Called when the user presses the (X) button * Called when the user presses the (X) button
*/ */
onClearText?: () => void onClearText?: () => void
hotkey?: boolean
ref?: React.RefObject<TextInput | null>
} }
export const SearchInput = forwardRef<TextInput, SearchInputProps>( export function SearchInput({
function SearchInput({value, label, onClearText, ...rest}, ref) { value,
const t = useTheme() label,
const {t: l} = useLingui() onClearText,
const showClear = value && value.length > 0 hotkey,
ref,
...rest
}: Props) {
const t = useTheme()
const {t: l} = useLingui()
const showClear = value && value.length > 0
const internalRef = useRef<TextInput>(null)
const inputRef = ref ?? internalRef
return ( useEffect(() => {
<View style={[a.w_full, a.relative]}> if (!hotkey) return
<TextField.Root> return listenFocusSearch(() => {
<TextField.Icon icon={MagnifyingGlassIcon} /> inputRef.current?.focus()
<TextField.Input })
inputRef={ref} }, [hotkey, inputRef])
label={label || l`Search`}
value={value}
placeholder={l`Search`}
returnKeyType="search"
keyboardAppearance={t.scheme}
selectTextOnFocus={IS_NATIVE}
autoFocus={false}
accessibilityRole="search"
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
style={[
showClear
? {
paddingRight: 24,
}
: {},
]}
{...rest}
/>
</TextField.Root>
{showClear && ( return (
<View <View style={[a.w_full, a.relative]}>
style={[ <TextField.Root>
a.absolute, <TextField.Icon icon={MagnifyingGlassIcon} />
a.z_20, <TextField.Input
a.my_auto, inputRef={inputRef}
a.inset_0, label={label || l`Search`}
a.justify_center, value={value}
a.pr_sm, placeholder={l`Search`}
{left: 'auto'}, returnKeyType="search"
]}> keyboardAppearance={t.scheme}
<Button selectTextOnFocus={IS_NATIVE}
testID="searchTextInputClearBtn" autoFocus={false}
onPress={onClearText} accessibilityRole="search"
label={l`Clear search query`} autoCorrect={false}
hitSlop={HITSLOP_10} autoComplete="off"
size="tiny" autoCapitalize="none"
shape="round" style={[
variant="ghost" showClear
color="secondary"> ? {
<ButtonIcon icon={X} size="xs" /> paddingRight: 24,
</Button> }
</View> : {},
)} ]}
</View> {...rest}
) />
}, </TextField.Root>
)
{showClear && (
<View
style={[
a.absolute,
a.z_20,
a.my_auto,
a.inset_0,
a.justify_center,
a.pr_sm,
{left: 'auto'},
]}>
<Button
testID="searchTextInputClearBtn"
onPress={onClearText}
label={l`Clear search query`}
hitSlop={HITSLOP_10}
size="tiny"
shape="round"
variant="ghost"
color="secondary">
<ButtonIcon icon={X} size="xs" />
</Button>
</View>
)}
</View>
)
}
@@ -1,7 +1,7 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api' import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api'
import {XRPCError} from '@atproto/xrpc' import {XRPCError} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
+1 -1
View File
@@ -1,5 +1,5 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
import { import {
+1 -1
View File
@@ -1,5 +1,5 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {useNavigation} from '@react-navigation/core' import {useNavigation} from '@react-navigation/native'
import {useDedupe} from '#/lib/hooks/useDedupe' import {useDedupe} from '#/lib/hooks/useDedupe'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
+1 -1
View File
@@ -1,5 +1,5 @@
import {useEffect, useMemo, useState} from 'react' import {useEffect, useMemo, useState} from 'react'
import {type EventArg, useNavigation} from '@react-navigation/core' import {type EventArg, useNavigation} from '@react-navigation/native'
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
// Tell the brower not to mess with the scroll. // Tell the brower not to mess with the scroll.
+76
View File
@@ -0,0 +1,76 @@
import React from 'react'
import {useLingui} from '@lingui/react/macro'
import {
HotkeysProvider,
useHotkeys,
useHotkeysContext,
} from 'react-hotkeys-hook'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {emitFocusSearch} from '#/state/events'
import {useSession} from '#/state/session'
enum Hotkeys {
OPEN_COMPOSER = 'n',
FOCUS_SEARCH = 'slash',
}
export function Provider({children}: React.PropsWithChildren<unknown>) {
return (
<HotkeysProvider initiallyActiveScopes={['global']}>
<KeyboardShortcuts>{children}</KeyboardShortcuts>
</HotkeysProvider>
)
}
export {useHotkeysContext}
function KeyboardShortcuts({children}: React.PropsWithChildren<unknown>) {
useKeyboardShortcuts()
return children
}
function useKeyboardShortcuts() {
const {openComposer} = useOpenComposer()
const {hasSession} = useSession()
const {t: l} = useLingui()
const shouldIgnore = (requiresSession: boolean = false) => {
if (requiresSession && !hasSession) {
return true
}
return false
}
const handleKey = (
callback: () => void,
options?: {requiresSession?: boolean},
) => {
if (shouldIgnore(options?.requiresSession)) {
return
}
callback()
}
useHotkeys(
Hotkeys.OPEN_COMPOSER,
() =>
handleKey(
() => {
openComposer({logContext: 'Other'})
},
{
requiresSession: true,
},
),
{scopes: ['global'], description: l`Compose new post`},
[openComposer],
)
useHotkeys(Hotkeys.FOCUS_SEARCH, () => handleKey(emitFocusSearch), {
scopes: ['global'],
preventDefault: true,
description: l`Focus the search field`,
})
}
+6 -2
View File
@@ -15,7 +15,6 @@ import {
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import {Buffer} from 'buffer'
import {POST_IMG_MAX} from '#/lib/constants' import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -322,7 +321,12 @@ export async function saveBytesToDisk(
bytes: Uint8Array, bytes: Uint8Array,
type: string, type: string,
) { ) {
const encoded = Buffer.from(bytes).toString('base64') // ideally we'd use `bytes.toBase64()`, but that's only baseline newly available
let binary = ''
for (const byte of bytes) {
binary += String.fromCharCode(byte)
}
const encoded = btoa(binary)
return await saveToDevice(filename, encoded, type) return await saveToDevice(filename, encoded, type)
} }
+4 -1
View File
@@ -190,7 +190,10 @@ function QueryProviderInner({
}) })
useEffect(() => { useEffect(() => {
if (IS_WEB) { if (IS_WEB) {
window.__TANSTACK_QUERY_CLIENT__ = queryClient // WARNING, BROKEN
// something since v5.32.0 causes OOMs. not important
// so disable for now
// window.__TANSTACK_QUERY_CLIENT__ = queryClient
} }
}, [queryClient]) }, [queryClient])
return ( return (
+1 -1
View File
@@ -1,4 +1,4 @@
import {XRPCError} from '@atproto/xrpc' import {XRPCError} from '@atproto/api'
import {t} from '@lingui/core/macro' import {t} from '@lingui/core/macro'
export function cleanError(str: any): string { export function cleanError(str: any): string {
+16 -15
View File
@@ -1953,7 +1953,7 @@ msgstr ""
#: src/screens/Deactivated.tsx:150 #: src/screens/Deactivated.tsx:150
#: src/screens/Profile/Header/EditProfileDialog.tsx:215 #: src/screens/Profile/Header/EditProfileDialog.tsx:215
#: src/screens/Profile/Header/EditProfileDialog.tsx:223 #: src/screens/Profile/Header/EditProfileDialog.tsx:223
#: src/screens/Search/Shell.tsx:396 #: src/screens/Search/Shell.tsx:397
#: src/screens/Settings/AppIconSettings/index.tsx:42 #: src/screens/Settings/AppIconSettings/index.tsx:42
#: src/screens/Settings/AppIconSettings/index.tsx:228 #: src/screens/Settings/AppIconSettings/index.tsx:228
#: src/screens/Settings/components/ChangeHandleDialog.tsx:80 #: src/screens/Settings/components/ChangeHandleDialog.tsx:80
@@ -1979,7 +1979,7 @@ msgstr ""
msgid "Cancel reactivation and sign out" msgid "Cancel reactivation and sign out"
msgstr "" msgstr ""
#: src/screens/Search/Shell.tsx:387 #: src/screens/Search/Shell.tsx:388
msgid "Cancel search" msgid "Cancel search"
msgstr "" msgstr ""
@@ -2218,7 +2218,7 @@ msgstr ""
msgid "Clear image cache" msgid "Clear image cache"
msgstr "" msgstr ""
#: src/components/forms/SearchInput.tsx:69 #: src/components/forms/SearchInput.tsx:87
msgid "Clear search query" msgid "Clear search query"
msgstr "" msgstr ""
@@ -2338,7 +2338,7 @@ msgstr ""
msgid "Close dialog" msgid "Close dialog"
msgstr "" msgstr ""
#: src/view/shell/index.web.tsx:131 #: src/view/shell/index.web.tsx:129
msgid "Close drawer menu" msgid "Close drawer menu"
msgstr "" msgstr ""
@@ -2430,6 +2430,7 @@ msgstr ""
msgid "Complete the challenge" msgid "Complete the challenge"
msgstr "" msgstr ""
#: src/lib/hotkeys/index.tsx:67
#: src/view/com/feeds/ComposerPrompt.tsx:147 #: src/view/com/feeds/ComposerPrompt.tsx:147
#: src/view/shell/desktop/LeftNav.tsx:575 #: src/view/shell/desktop/LeftNav.tsx:575
msgid "Compose new post" msgid "Compose new post"
@@ -4349,7 +4350,7 @@ msgstr ""
msgid "Find people to follow" msgid "Find people to follow"
msgstr "" msgstr ""
#: src/screens/Search/Shell.tsx:529 #: src/screens/Search/Shell.tsx:530
msgid "Find posts, users, and feeds on Bluesky" msgid "Find posts, users, and feeds on Bluesky"
msgstr "" msgstr ""
@@ -4393,6 +4394,10 @@ msgstr ""
msgid "Focus code input" msgid "Focus code input"
msgstr "" msgstr ""
#: src/lib/hotkeys/index.tsx:74
msgid "Focus the search field"
msgstr "Focus the search field"
#. User is not following this account, click to follow #. User is not following this account, click to follow
#: src/components/ProfileCard.tsx:546 #: src/components/ProfileCard.tsx:546
#: src/components/ProfileHoverCard/index.web.tsx:495 #: src/components/ProfileHoverCard/index.web.tsx:495
@@ -6485,13 +6490,9 @@ msgid "New post"
msgstr "" msgstr ""
#: src/view/com/feeds/FeedPage.tsx:181 #: src/view/com/feeds/FeedPage.tsx:181
msgctxt "action"
msgid "New post"
msgstr ""
#: src/view/shell/desktop/LeftNav.tsx:583 #: src/view/shell/desktop/LeftNav.tsx:583
msgctxt "action" msgctxt "action"
msgid "New Post" msgid "New post"
msgstr "" msgstr ""
#: src/view/com/notifications/NotificationFeedItem.tsx:545 #: src/view/com/notifications/NotificationFeedItem.tsx:545
@@ -8731,10 +8732,10 @@ msgid "Scroll to top"
msgstr "" msgstr ""
#: src/components/dialogs/SearchablePeopleList.tsx:515 #: src/components/dialogs/SearchablePeopleList.tsx:515
#: src/components/forms/SearchInput.tsx:33 #: src/components/forms/SearchInput.tsx:51
#: src/components/forms/SearchInput.tsx:35 #: src/components/forms/SearchInput.tsx:53
#: src/screens/Search/Shell.tsx:354 #: src/screens/Search/Shell.tsx:354
#: src/screens/Search/Shell.tsx:517 #: src/screens/Search/Shell.tsx:518
#: src/view/shell/bottom-bar/BottomBar.tsx:199 #: src/view/shell/bottom-bar/BottomBar.tsx:199
msgid "Search" msgid "Search"
msgstr "" msgstr ""
@@ -8772,7 +8773,7 @@ msgstr ""
msgid "Search for \"{searchText}\"" msgid "Search for \"{searchText}\""
msgstr "" msgstr ""
#: src/view/shell/desktop/Search.tsx:128 #: src/view/shell/desktop/Search.tsx:129
msgid "Search for “{tQuery}”" msgid "Search for “{tQuery}”"
msgstr "Search for “{tQuery}”" msgstr "Search for “{tQuery}”"
@@ -9634,7 +9635,7 @@ msgstr ""
msgid "Sorry, we're unable to load account suggestions at this time." msgid "Sorry, we're unable to load account suggestions at this time."
msgstr "" msgstr ""
#: src/App.native.tsx:142 #: src/App.native.tsx:141
#: src/App.web.tsx:119 #: src/App.web.tsx:119
msgid "Sorry! Your session expired. Please sign in again." msgid "Sorry! Your session expired. Please sign in again."
msgstr "" msgstr ""
+1
View File
@@ -380,6 +380,7 @@ export function SearchScreenShell({
inputPlaceholder ?? l`Search for posts, users, or feeds` inputPlaceholder ?? l`Search for posts, users, or feeds`
} }
hitSlop={{...HITSLOP_20, top: 0}} hitSlop={{...HITSLOP_20, top: 0}}
hotkey={true}
/> />
</View> </View>
{showAutocomplete && ( {showAutocomplete && (
+1 -1
View File
@@ -5,7 +5,7 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
} from '@atproto/api' } from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import {batchedUpdates} from '#/lib/batchedUpdates' import {batchedUpdates} from '#/lib/batchedUpdates'
import {findAllPostsInQueryData as findAllPostsInBookmarksQueryData} from '#/state/queries/bookmarks/useBookmarksQuery' import {findAllPostsInQueryData as findAllPostsInBookmarksQueryData} from '#/state/queries/bookmarks/useBookmarksQuery'
+1 -1
View File
@@ -1,7 +1,7 @@
import {useEffect, useMemo, useState} from 'react' import {useEffect, useMemo, useState} from 'react'
import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api' import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api'
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import {batchedUpdates} from '#/lib/batchedUpdates' import {batchedUpdates} from '#/lib/batchedUpdates'
import {findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData} from '#/state/queries/activity-subscriptions' import {findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData} from '#/state/queries/activity-subscriptions'
+18 -8
View File
@@ -7,6 +7,7 @@ import {
useState, useState,
} from 'react' } from 'react'
import {useHotkeysContext} from '#/lib/hotkeys'
import {type DialogControlRefProps} from '#/components/Dialog' import {type DialogControlRefProps} from '#/components/Dialog'
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
@@ -62,6 +63,7 @@ export function useDialogFullyExpandedCountContext() {
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [fullyExpandedCount, setFullyExpandedCount] = useState(0) const [fullyExpandedCount, setFullyExpandedCount] = useState(0)
const {disableScope, enableScope} = useHotkeysContext()
const activeDialogs = useRef< const activeDialogs = useRef<
Map<string, React.MutableRefObject<DialogControlRefProps>> Map<string, React.MutableRefObject<DialogControlRefProps>>
@@ -77,18 +79,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return openDialogs.current.size > 0 return openDialogs.current.size > 0
} else { } else {
BottomSheetNativeComponent.dismissAll() void BottomSheetNativeComponent.dismissAll()
return false return false
} }
}, []) }, [])
const setDialogIsOpen = useCallback((id: string, isOpen: boolean) => { const setDialogIsOpen = useCallback(
if (isOpen) { (id: string, isOpen: boolean) => {
openDialogs.current.add(id) if (isOpen) {
} else { openDialogs.current.add(id)
openDialogs.current.delete(id) } else {
} openDialogs.current.delete(id)
}, []) }
if (openDialogs.current.size > 0) {
disableScope('global')
} else {
enableScope('global')
}
},
[disableScope, enableScope],
)
const context = useMemo<IDialogContext>( const context = useMemo<IDialogContext>(
() => ({ () => ({
+9 -1
View File
@@ -1,4 +1,4 @@
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
type UnlistenFn = () => void type UnlistenFn = () => void
@@ -45,3 +45,11 @@ export function listenPostCreated(fn: () => void): UnlistenFn {
emitter.on('post-created', fn) emitter.on('post-created', fn)
return () => emitter.off('post-created', fn) return () => emitter.off('post-created', fn)
} }
export function emitFocusSearch() {
emitter.emit('focus-search')
}
export function listenFocusSearch(fn: () => void): UnlistenFn {
emitter.on('focus-search', fn)
return () => emitter.off('focus-search', fn)
}
+1 -1
View File
@@ -7,7 +7,7 @@ import {
type GestureUpdateEvent, type GestureUpdateEvent,
type PanGestureHandlerEventPayload, type PanGestureHandlerEventPayload,
} from 'react-native-gesture-handler' } from 'react-native-gesture-handler'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
export type GlobalGestureEvents = { export type GlobalGestureEvents = {
begin: GestureStateChangeEvent<PanGestureHandlerEventPayload> begin: GestureStateChangeEvent<PanGestureHandlerEventPayload>
+11 -1
View File
@@ -1,7 +1,8 @@
import {createContext, useContext, useMemo, useState} from 'react' import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useHotkeysContext} from '#/lib/hotkeys'
import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types' import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types'
export type Lightbox = { export type Lightbox = {
@@ -28,6 +29,15 @@ LightboxControlContext.displayName = 'LightboxControlContext'
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [activeLightbox, setActiveLightbox] = useState<Lightbox | null>(null) const [activeLightbox, setActiveLightbox] = useState<Lightbox | null>(null)
const {disableScope, enableScope} = useHotkeysContext()
useEffect(() => {
if (activeLightbox) {
disableScope('global')
} else {
enableScope('global')
}
}, [activeLightbox, disableScope, enableScope])
const openLightbox = useNonReactiveCallback( const openLightbox = useNonReactiveCallback(
(lightbox: Omit<Lightbox, 'id'>) => { (lightbox: Omit<Lightbox, 'id'>) => {
+2 -2
View File
@@ -5,8 +5,8 @@ import {
type ChatBskyConvoGetLog, type ChatBskyConvoGetLog,
type ChatBskyConvoSendMessage, type ChatBskyConvoSendMessage,
} from '@atproto/api' } from '@atproto/api'
import {XRPCError} from '@atproto/xrpc' import {XRPCError} from '@atproto/api'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
+1 -1
View File
@@ -1,5 +1,5 @@
import {type BskyAgent, type ChatBskyConvoGetLog} from '@atproto/api' import {type BskyAgent, type ChatBskyConvoGetLog} from '@atproto/api'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
+11 -1
View File
@@ -1,6 +1,7 @@
import {createContext, useContext, useMemo, useState} from 'react' import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useHotkeysContext} from '#/lib/hotkeys'
export interface UserAddRemoveListsModal { export interface UserAddRemoveListsModal {
name: 'user-add-remove-lists' name: 'user-add-remove-lists'
@@ -47,6 +48,15 @@ ModalControlContext.displayName = 'ModalControlContext'
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [activeModals, setActiveModals] = useState<Modal[]>([]) const [activeModals, setActiveModals] = useState<Modal[]>([])
const {disableScope, enableScope} = useHotkeysContext()
useEffect(() => {
if (activeModals.length > 0) {
disableScope('global')
} else {
enableScope('global')
}
}, [activeModals.length, disableScope, enableScope])
const openModal = useNonReactiveCallback((modal: Modal) => { const openModal = useNonReactiveCallback((modal: Modal) => {
setActiveModals(modals => [...modals, modal]) setActiveModals(modals => [...modals, modal])
+1 -1
View File
@@ -1,4 +1,4 @@
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import BroadcastChannel from '#/lib/broadcast' import BroadcastChannel from '#/lib/broadcast'
import {logger} from '#/logger' import {logger} from '#/logger'
+1 -1
View File
@@ -12,7 +12,7 @@ import {
} from 'react' } from 'react'
import {AppState} from 'react-native' import {AppState} from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import BroadcastChannel from '#/lib/broadcast' import BroadcastChannel from '#/lib/broadcast'
import {resetBadgeCount} from '#/lib/notifications/notifications' import {resetBadgeCount} from '#/lib/notifications/notifications'
@@ -1,77 +0,0 @@
import {useEffect} from 'react'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useDialogStateContext} from '#/state/dialogs'
import {useLightbox} from '#/state/lightbox'
import {useModals} from '#/state/modals'
import {useSession} from '#/state/session'
import {useIsDrawerOpen} from '#/state/shell/drawer-open'
/**
* Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2}
*/
function shouldIgnore(event: KeyboardEvent) {
const target: any = event.target || event.srcElement
if (!target) return false
const {tagName} = target
if (!tagName) return false
const isInput =
tagName === 'INPUT' &&
![
'checkbox',
'radio',
'range',
'button',
'file',
'reset',
'submit',
'color',
].includes(target.type)
// ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
if (
target.isContentEditable ||
((isInput || tagName === 'TEXTAREA' || tagName === 'SELECT') &&
!target.readOnly)
) {
return true
}
return false
}
export function useComposerKeyboardShortcut() {
const {openComposer} = useOpenComposer()
const {openDialogs} = useDialogStateContext()
const {isModalActive} = useModals()
const {activeLightbox} = useLightbox()
const isDrawerOpen = useIsDrawerOpen()
const {hasSession} = useSession()
useEffect(() => {
if (!hasSession) {
return
}
function handler(event: KeyboardEvent) {
if (shouldIgnore(event)) return
if (
openDialogs?.current.size > 0 ||
isModalActive ||
activeLightbox ||
isDrawerOpen
)
return
if (event.key === 'n' || event.key === 'N') {
openComposer({logContext: 'Other'})
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [
openComposer,
isModalActive,
openDialogs,
activeLightbox,
isDrawerOpen,
hasSession,
])
}
+15 -1
View File
@@ -1,5 +1,7 @@
import {createContext, useContext, useState} from 'react' import {createContext, useContext, useState} from 'react'
import {useHotkeysContext} from '#/lib/hotkeys'
type StateContext = boolean type StateContext = boolean
type SetContext = (v: boolean) => void type SetContext = (v: boolean) => void
@@ -10,10 +12,22 @@ setContext.displayName = 'DrawerOpenSetContext'
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = useState(false) const [state, setState] = useState(false)
const {disableScope, enableScope} = useHotkeysContext()
const setDrawerOpen = (open: boolean) => {
if (open) {
disableScope('global')
} else {
enableScope('global')
}
setState(open)
}
return ( return (
<stateContext.Provider value={state}> <stateContext.Provider value={state}>
<setContext.Provider value={setState}>{children}</setContext.Provider> <setContext.Provider value={setDrawerOpen}>
{children}
</setContext.Provider>
</stateContext.Provider> </stateContext.Provider>
) )
} }
@@ -1,3 +1,3 @@
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
export const textInputWebEmitter = new EventEmitter() export const textInputWebEmitter = new EventEmitter()
+1 -1
View File
@@ -7,7 +7,7 @@ import {
withSpring, withSpring,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import EventEmitter from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import {useMinimalShellMode} from '#/state/shell' import {useMinimalShellMode} from '#/state/shell'
+8 -2
View File
@@ -400,10 +400,16 @@ function Btn({
a.rounded_full, a.rounded_full,
{backgroundColor: t.palette.primary_500}, {backgroundColor: t.palette.primary_500},
]}> ]}>
<Text style={styles.notificationCountLabel}>{notificationCount}</Text> <Text
style={styles.notificationCountLabel}
maxFontSizeMultiplier={1.5}>
{notificationCount}
</Text>
</View> </View>
) : hasNew ? ( ) : hasNew ? (
<View style={[styles.hasNewBadge, a.rounded_full]} /> <View
style={[styles.hasNewBadge, {backgroundColor: t.palette.primary_500}]}
/>
) : null} ) : null}
</PressableScale> </PressableScale>
) )
@@ -1,6 +1,5 @@
import {StyleSheet} from 'react-native' import {StyleSheet} from 'react-native'
import {colors} from '#/lib/styles'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
@@ -24,8 +23,9 @@ export const styles = StyleSheet.create({
position: 'absolute', position: 'absolute',
left: '52%', left: '52%',
top: 8, top: 8,
paddingHorizontal: 4, paddingHorizontal: 5,
paddingBottom: 1, paddingTop: 1,
paddingBottom: 2,
borderRadius: 6, borderRadius: 6,
zIndex: 1, zIndex: 1,
}, },
@@ -37,8 +37,9 @@ export const styles = StyleSheet.create({
notificationCountLabel: { notificationCountLabel: {
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: '600',
color: colors.white, color: 'white',
fontVariant: ['tabular-nums'], fontVariant: ['tabular-nums'],
includeFontPadding: false,
}, },
hasNewBadge: { hasNewBadge: {
position: 'absolute', position: 'absolute',
@@ -47,8 +48,7 @@ export const styles = StyleSheet.create({
top: 10, top: 10,
width: 8, width: 8,
height: 8, height: 8,
backgroundColor: colors.blue3, borderRadius: 4,
borderRadius: 6,
zIndex: 1, zIndex: 1,
}, },
ctrlIcon: { ctrlIcon: {
+3 -1
View File
@@ -313,7 +313,9 @@ const NavItem: React.FC<{
<Text style={styles.notificationCountLabel}>{notificationCount}</Text> <Text style={styles.notificationCountLabel}>{notificationCount}</Text>
</View> </View>
) : hasNew ? ( ) : hasNew ? (
<View style={styles.hasNewBadge} /> <View
style={[styles.hasNewBadge, {backgroundColor: t.palette.primary_500}]}
/>
) : null} ) : null}
</Link> </Link>
) )
+1 -1
View File
@@ -580,7 +580,7 @@ function ComposeBtn() {
style={[a.rounded_full]}> style={[a.rounded_full]}>
<ButtonIcon icon={EditBig} position="left" /> <ButtonIcon icon={EditBig} position="left" />
<ButtonText> <ButtonText>
<Trans context="action">New Post</Trans> <Trans context="action">New post</Trans>
</ButtonText> </ButtonText>
</Button> </Button>
</View> </View>
+1 -1
View File
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/core' import {useNavigation} from '@react-navigation/native'
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
import {useKawaiiMode} from '#/state/preferences/kawaii' import {useKawaiiMode} from '#/state/preferences/kawaii'
+1
View File
@@ -105,6 +105,7 @@ export function DesktopSearch() {
onChangeText={onChangeText} onChangeText={onChangeText}
onClearText={onPressCancelSearch} onClearText={onPressCancelSearch}
onSubmitEditing={onSubmit} onSubmitEditing={onSubmit}
hotkey={true}
/> />
{tQuery !== '' && isActive && moderationOpts && ( {tQuery !== '' && isActive && moderationOpts && (
<View <View
+1 -3
View File
@@ -9,7 +9,6 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
import {useCloseAllActiveElements} from '#/state/util' import {useCloseAllActiveElements} from '#/state/util'
import {Lightbox} from '#/view/com/lightbox/Lightbox' import {Lightbox} from '#/view/com/lightbox/Lightbox'
import {ModalsContainer} from '#/view/com/modals/Modal' import {ModalsContainer} from '#/view/com/modals/Modal'
@@ -36,7 +35,7 @@ import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen'
import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
import {PassiveAnalytics} from '#/analytics/PassiveAnalytics' import {PassiveAnalytics} from '#/analytics/PassiveAnalytics'
import {FlatNavigator, RoutesContainer} from '#/Navigation' import {FlatNavigator, RoutesContainer} from '#/Navigation'
import {Composer} from './Composer.web' import {Composer} from './Composer'
import {DrawerContent} from './Drawer' import {DrawerContent} from './Drawer'
function ShellInner() { function ShellInner() {
@@ -45,7 +44,6 @@ function ShellInner() {
const {state: policyUpdateState} = usePolicyUpdateContext() const {state: policyUpdateState} = usePolicyUpdateContext()
const welcomeModalControl = useWelcomeModal() const welcomeModalControl = useWelcomeModal()
useComposerKeyboardShortcut()
useIntentHandler() useIntentHandler()
useEffect(() => { useEffect(() => {
+56 -51
View File
@@ -20,46 +20,46 @@
"@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/trace-mapping" "^0.3.9"
"@atproto/api@^0.19.3": "@atproto/api@^0.19.5":
version "0.19.3" version "0.19.5"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.3.tgz#61de8d2e31abe9eb2b4c8f4ad124ed79d4a77e89" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.19.5.tgz#6388e5d6d3a1693fe04b5f37c705682bac8601d3"
integrity sha512-G8YpBpRouHdTAIagi/QQIUZOhGd1jfBQWkJy9QfxAzjjEpPvaVOSk4e1S85QzGLm/xbzVONzGkmdtiOSfP6wVg== integrity sha512-u6R5TecYJDO8l8QFN09AMuJASYnUkJ4HhYE5hg4/dha/z14a+OAil2/dli/208uM5AHPFLtlnB8kIK9XU5GgQQ==
dependencies: dependencies:
"@atproto/common-web" "^0.4.18" "@atproto/common-web" "^0.4.19"
"@atproto/lexicon" "^0.6.2" "@atproto/lexicon" "^0.6.2"
"@atproto/syntax" "^0.5.0" "@atproto/syntax" "^0.5.2"
"@atproto/xrpc" "^0.7.7" "@atproto/xrpc" "^0.7.7"
await-lock "^2.2.2" await-lock "^2.2.2"
multiformats "^9.9.0" multiformats "^9.9.0"
tlds "^1.234.0" tlds "^1.234.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/common-web@^0.4.18": "@atproto/common-web@^0.4.18", "@atproto/common-web@^0.4.19":
version "0.4.18" version "0.4.19"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.18.tgz#832976340457afd3d29345ad2c6f0ac0bff087dd" resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.19.tgz#bbd7f84f545ebe73ca3bc00314ccf4ee66e7069e"
integrity sha512-ilImzP+9N/mtse440kN60pGrEzG7wi4xsV13nGeLrS+Zocybc/ISOpKlbZM13o+twPJ+Q7veGLw9CtGg0GAFoQ== integrity sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==
dependencies: dependencies:
"@atproto/lex-data" "^0.0.13" "@atproto/lex-data" "^0.0.14"
"@atproto/lex-json" "^0.0.13" "@atproto/lex-json" "^0.0.14"
"@atproto/syntax" "^0.5.0" "@atproto/syntax" "^0.5.1"
zod "^3.23.8" zod "^3.23.8"
"@atproto/lex-data@^0.0.13": "@atproto/lex-data@^0.0.14":
version "0.0.13" version "0.0.14"
resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.13.tgz#db1bcfa12d5056210f6eb7f3b8bac909909d6b9c" resolved "https://registry.yarnpkg.com/@atproto/lex-data/-/lex-data-0.0.14.tgz#2f2f3c64699925a0d4785e5afd0e7731ba1d46c0"
integrity sha512-7Z7RwZ1Y/JzBF/Tcn/I4UJ/vIGfh5zn1zjv0KX+flke2JtgFkSE8uh2hOtqgBQMNqE3zdJFM+dcSWln86hR3MQ== integrity sha512-53DUa9664SS76nGAMYopWsO10OH0AAdf7P/HSKB6Wzx3iqe6lk/K61QZnKxOG1LreYl5CfvIJU6eNf4txI6GlQ==
dependencies: dependencies:
multiformats "^9.9.0" multiformats "^9.9.0"
tslib "^2.8.1" tslib "^2.8.1"
uint8arrays "3.0.0" uint8arrays "3.0.0"
unicode-segmenter "^0.14.0" unicode-segmenter "^0.14.0"
"@atproto/lex-json@^0.0.13": "@atproto/lex-json@^0.0.14":
version "0.0.13" version "0.0.14"
resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.13.tgz#b0081f786aeeb1707087318fb03c928e75c19059" resolved "https://registry.yarnpkg.com/@atproto/lex-json/-/lex-json-0.0.14.tgz#717e533ab583aa5f580acb2a77d9aa3e7eddaa17"
integrity sha512-hwLhkKaIHulGJpt0EfXAEWdrxqM2L1tV/tvilzhMp3QxPqYgXchFnrfVmLsyFDx6P6qkH1GsX/XC2V36U0UlPQ== integrity sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww==
dependencies: dependencies:
"@atproto/lex-data" "^0.0.13" "@atproto/lex-data" "^0.0.14"
tslib "^2.8.1" tslib "^2.8.1"
"@atproto/lexicon@^0.6.0", "@atproto/lexicon@^0.6.2": "@atproto/lexicon@^0.6.0", "@atproto/lexicon@^0.6.2":
@@ -73,10 +73,10 @@
multiformats "^9.9.0" multiformats "^9.9.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/syntax@^0.5.0": "@atproto/syntax@^0.5.0", "@atproto/syntax@^0.5.1", "@atproto/syntax@^0.5.2":
version "0.5.0" version "0.5.2"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.5.0.tgz#061ef538aee784f8e5fa1ea50a7f5beb4c276c9b" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.5.2.tgz#d4b32c9feb421ceeb5ade1fa80bc42764d51e52e"
integrity sha512-UA2DSpGdOQzUQ4gi5SH+NEJz/YR3a3Fg3y2oh+xETDSiTRmA4VhHRCojhXAVsBxUT6EnItw190C/KN+DWW90kw== integrity sha512-W41szOnkppoHr0iCUrzL8gy3OD6qmDyp1UvUgmTx2oFQfgbudpz51T/gznesiCcqiUT5obfHdx4PJ+WdlEOE7Q==
dependencies: dependencies:
tslib "^2.8.1" tslib "^2.8.1"
@@ -5089,39 +5089,39 @@
dependencies: dependencies:
"@sinonjs/commons" "^3.0.0" "@sinonjs/commons" "^3.0.0"
"@tanstack/query-async-storage-persister@^5.95.2": "@tanstack/query-async-storage-persister@^5.96.2":
version "5.95.2" version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.95.2.tgz#0c7ed1c8013823e2d5abbb8d55bc0e9305abf7e0" resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.96.2.tgz#29423b35f2d8c5f63afbf72475baa7799ffaf022"
integrity sha512-ZhPIHH8J833OVZhEWwwdOk0uhY94d9Wgdnq97JoQx4Ui4xx4Dh6e7WPUrjlUWo88Yqi4Ij+T1o/VR7Vlbnkbjw== integrity sha512-lYJm+TwzOEUVkxCJapLSzRXPzmPpv7Vy3zSB1RXYQ6+vznEgXBqLjn+ZwBRvHpkRda9VXis64wv44rPIi9nCwg==
dependencies: dependencies:
"@tanstack/query-core" "5.95.2" "@tanstack/query-core" "5.96.2"
"@tanstack/query-persist-client-core" "5.95.2" "@tanstack/query-persist-client-core" "5.96.2"
"@tanstack/query-core@5.95.2": "@tanstack/query-core@5.96.2":
version "5.95.2" version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.95.2.tgz#9e3299d0c1c8785dd9e3d0cac1993e45f35113f2" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.96.2.tgz#766dab253476afd0b27959b66abb606d8d2dd9f5"
integrity sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ== integrity sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==
"@tanstack/query-persist-client-core@5.95.2": "@tanstack/query-persist-client-core@5.96.2":
version "5.95.2" version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.95.2.tgz#1c94a87c9886a8e1c6a0a3ebbb325afdaf486f81" resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.96.2.tgz#65ea5a2104a85a2f39ef1a007f6f0ad63fbf1c49"
integrity sha512-Opfj34WZ594YXpEcZEs8WBiyPGrjrKlGILfk/Ss283uwWQ36C5nX3tRY/bBiXmM82KWauUuNvahwGwiyco/8cQ== integrity sha512-BYsP8folbvxzZsNnWJxSenEAdepGNfv809150U78D84yt/THi33EwfUCcdKWFbma5XKwlaFQGWMJKeWnVJ6GVA==
dependencies: dependencies:
"@tanstack/query-core" "5.95.2" "@tanstack/query-core" "5.96.2"
"@tanstack/react-query-persist-client@^5.95.2": "@tanstack/react-query-persist-client@^5.96.2":
version "5.95.2" version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.95.2.tgz#4d6fe899513725978e86c13c1727ee7e393eaca5" resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.96.2.tgz#b47d62fc990a9fd38ddcf4a080d1300ae887e5a0"
integrity sha512-i3fvzD8gaLgQyFvRc/+iSUr60aL31tMN+5QM11zdPRg0K9CirIQjHD7WgXFBnD29KJDvcjcv7OrIBaPwZ+H9xw== integrity sha512-smQ38oVPlnvkG+G7R60IAD9X6azJLRjHEd7twml9XBLYM31ncPDP0tUKy/Gv/4ItVmKTtjZ5VabXpVZxnaWSww==
dependencies: dependencies:
"@tanstack/query-persist-client-core" "5.95.2" "@tanstack/query-persist-client-core" "5.96.2"
"@tanstack/react-query@^5.95.2": "@tanstack/react-query@^5.96.2":
version "5.95.2" version "5.96.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.95.2.tgz#7daf77342a4e374c22fad88bb98ea13fc19ae086" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.96.2.tgz#a164abfb80eb5e7772bbcddfa7240f3fd8d0d7be"
integrity sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA== integrity sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==
dependencies: dependencies:
"@tanstack/query-core" "5.95.2" "@tanstack/query-core" "5.96.2"
"@testing-library/react-native@^13.2.0": "@testing-library/react-native@^13.2.0":
version "13.2.0" version "13.2.0"
@@ -13851,6 +13851,11 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d" resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d"
integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g== integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==
react-hotkeys-hook@5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-5.2.4.tgz#45ad54d78823b2a929963d482aff98efad0530f2"
integrity sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A==
react-image-crop@^11.0.7: react-image-crop@^11.0.7:
version "11.0.7" version "11.0.7"
resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.7.tgz#25f3d37ccbb65a05d19d23b4740a5912835c741e" resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.7.tgz#25f3d37ccbb65a05d19d23b4740a5912835c741e"