Merge branch 'main' into app-1934
This commit is contained in:
+13
-14
@@ -11,8 +11,7 @@ import {
|
||||
import * as ScreenOrientation from 'expo-screen-orientation'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
|
||||
@@ -89,9 +88,9 @@ import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
void SplashScreen.preventAutoHideAsync()
|
||||
if (IS_IOS) {
|
||||
SystemUI.setBackgroundColorAsync('black')
|
||||
void SystemUI.setBackgroundColorAsync('black')
|
||||
}
|
||||
if (IS_ANDROID) {
|
||||
// iOS is handled by the config plugin -sfn
|
||||
@@ -105,17 +104,17 @@ if (IS_ANDROID) {
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
Geo.resolve()
|
||||
prefetchAgeAssuranceConfig()
|
||||
prefetchLiveEvents()
|
||||
prefetchAppConfig()
|
||||
void Geo.resolve()
|
||||
void prefetchAgeAssuranceConfig()
|
||||
void prefetchLiveEvents()
|
||||
void prefetchAppConfig()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const {currentAccount} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const hasCheckedReferrer = useStarterPackEntry()
|
||||
|
||||
// init
|
||||
@@ -134,16 +133,16 @@ function InnerApp() {
|
||||
}
|
||||
}
|
||||
const account = readLastActiveAccount()
|
||||
onLaunch(account)
|
||||
void onLaunch(account)
|
||||
}, [resumeSession])
|
||||
|
||||
useEffect(() => {
|
||||
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',
|
||||
})
|
||||
})
|
||||
}, [_])
|
||||
}, [l])
|
||||
|
||||
return (
|
||||
<Alf theme={theme}>
|
||||
@@ -220,8 +219,8 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
|
||||
setReady(true),
|
||||
void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
|
||||
() => setReady(true),
|
||||
)
|
||||
}, [])
|
||||
|
||||
|
||||
+17
-15
@@ -5,10 +5,10 @@ import './style.css'
|
||||
import {Fragment, useEffect, useState} from 'react'
|
||||
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
import {Provider as HotkeysProvider} from '#/lib/hotkeys'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
|
||||
@@ -82,17 +82,17 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
Geo.resolve()
|
||||
prefetchAgeAssuranceConfig()
|
||||
prefetchLiveEvents()
|
||||
prefetchAppConfig()
|
||||
void Geo.resolve()
|
||||
void prefetchAgeAssuranceConfig()
|
||||
void prefetchLiveEvents()
|
||||
void prefetchAppConfig()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const {currentAccount} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const hasCheckedReferrer = useStarterPackEntry()
|
||||
|
||||
// init
|
||||
@@ -105,22 +105,22 @@ function InnerApp() {
|
||||
await features.init
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`session: resumeSession failed`, {message: e})
|
||||
logger.error('session: resumeSession failed', {message: e})
|
||||
} finally {
|
||||
setIsReady(true)
|
||||
}
|
||||
}
|
||||
const account = readLastActiveAccount()
|
||||
onLaunch(account)
|
||||
void onLaunch(account)
|
||||
}, [resumeSession])
|
||||
|
||||
useEffect(() => {
|
||||
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',
|
||||
})
|
||||
})
|
||||
}, [_])
|
||||
}, [l])
|
||||
|
||||
return (
|
||||
<Alf theme={theme}>
|
||||
@@ -156,8 +156,10 @@ function InnerApp() {
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<TranslateOnDeviceProvider>
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
<HotkeysProvider>
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
</HotkeysProvider>
|
||||
</TranslateOnDeviceProvider>
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
@@ -195,8 +197,8 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
|
||||
setReady(true),
|
||||
void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
|
||||
() => setReady(true),
|
||||
)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {View} from 'react-native'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
|
||||
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 {useSimpleVerificationState} from '#/components/verification'
|
||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||
@@ -38,12 +38,21 @@ export function ProfileBadges({
|
||||
}) {
|
||||
const shadowed = useProfileShadow(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 (!verification.showBadge && !isBotAccount(shadowed)) return null
|
||||
|
||||
const isOnTheSmallSide = size === 'xs' || size === 'sm'
|
||||
|
||||
const verificationIconWidth =
|
||||
verificationIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
|
||||
const botIconWidth =
|
||||
botIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -56,19 +65,19 @@ export function ProfileBadges({
|
||||
<>
|
||||
<VerificationCheckButton
|
||||
profile={shadowed}
|
||||
width={verificationIconSizes[size]}
|
||||
width={verificationIconWidth}
|
||||
/>
|
||||
<BotBadgeButton profile={shadowed} width={botIconSizes[size]} />
|
||||
<BotBadgeButton profile={shadowed} width={botIconWidth} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{verification.showBadge && (
|
||||
<VerificationCheck
|
||||
verifier={verification.role === 'verifier'}
|
||||
width={verificationIconSizes[size]}
|
||||
width={verificationIconWidth}
|
||||
/>
|
||||
)}
|
||||
<BotBadge profile={shadowed} width={botIconSizes[size]} />
|
||||
<BotBadge profile={shadowed} width={botIconWidth} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {XRPCError} from '@atproto/xrpc'
|
||||
import {XRPCError} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect} from 'react'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
const events = new EventEmitter<{
|
||||
emailVerified: void
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {forwardRef} from 'react'
|
||||
import {useEffect, useRef} from 'react'
|
||||
import {type TextInput, View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {listenFocusSearch} from '#/state/events'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
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 {IS_NATIVE} from '#/env'
|
||||
|
||||
type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
|
||||
type Props = Omit<TextField.InputProps, 'label'> & {
|
||||
label?: TextField.InputProps['label']
|
||||
/**
|
||||
* Called when the user presses the (X) button
|
||||
*/
|
||||
onClearText?: () => void
|
||||
hotkey?: boolean
|
||||
ref?: React.RefObject<TextInput | null>
|
||||
}
|
||||
|
||||
export const SearchInput = forwardRef<TextInput, SearchInputProps>(
|
||||
function SearchInput({value, label, onClearText, ...rest}, ref) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const showClear = value && value.length > 0
|
||||
export function SearchInput({
|
||||
value,
|
||||
label,
|
||||
onClearText,
|
||||
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 (
|
||||
<View style={[a.w_full, a.relative]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={MagnifyingGlassIcon} />
|
||||
<TextField.Input
|
||||
inputRef={ref}
|
||||
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>
|
||||
useEffect(() => {
|
||||
if (!hotkey) return
|
||||
return listenFocusSearch(() => {
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
}, [hotkey, inputRef])
|
||||
|
||||
{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>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<View style={[a.w_full, a.relative]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={MagnifyingGlassIcon} />
|
||||
<TextField.Input
|
||||
inputRef={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 && (
|
||||
<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 {View} from 'react-native'
|
||||
import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api'
|
||||
import {XRPCError} from '@atproto/xrpc'
|
||||
import {XRPCError} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useMemo} from 'react'
|
||||
import {useNavigation} from '@react-navigation/core'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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) {
|
||||
// Tell the brower not to mess with the scroll.
|
||||
|
||||
@@ -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`,
|
||||
})
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import {Buffer} from 'buffer'
|
||||
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
@@ -322,7 +321,12 @@ export async function saveBytesToDisk(
|
||||
bytes: Uint8Array,
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +190,10 @@ function QueryProviderInner({
|
||||
})
|
||||
useEffect(() => {
|
||||
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])
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {XRPCError} from '@atproto/xrpc'
|
||||
import {XRPCError} from '@atproto/api'
|
||||
import {t} from '@lingui/core/macro'
|
||||
|
||||
export function cleanError(str: any): string {
|
||||
|
||||
@@ -1953,7 +1953,7 @@ msgstr ""
|
||||
#: src/screens/Deactivated.tsx:150
|
||||
#: src/screens/Profile/Header/EditProfileDialog.tsx:215
|
||||
#: 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:228
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:80
|
||||
@@ -1979,7 +1979,7 @@ msgstr ""
|
||||
msgid "Cancel reactivation and sign out"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Shell.tsx:387
|
||||
#: src/screens/Search/Shell.tsx:388
|
||||
msgid "Cancel search"
|
||||
msgstr ""
|
||||
|
||||
@@ -2218,7 +2218,7 @@ msgstr ""
|
||||
msgid "Clear image cache"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/forms/SearchInput.tsx:69
|
||||
#: src/components/forms/SearchInput.tsx:87
|
||||
msgid "Clear search query"
|
||||
msgstr ""
|
||||
|
||||
@@ -2338,7 +2338,7 @@ msgstr ""
|
||||
msgid "Close dialog"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/shell/index.web.tsx:131
|
||||
#: src/view/shell/index.web.tsx:129
|
||||
msgid "Close drawer menu"
|
||||
msgstr ""
|
||||
|
||||
@@ -2430,6 +2430,7 @@ msgstr ""
|
||||
msgid "Complete the challenge"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/hotkeys/index.tsx:67
|
||||
#: src/view/com/feeds/ComposerPrompt.tsx:147
|
||||
#: src/view/shell/desktop/LeftNav.tsx:575
|
||||
msgid "Compose new post"
|
||||
@@ -4349,7 +4350,7 @@ msgstr ""
|
||||
msgid "Find people to follow"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Shell.tsx:529
|
||||
#: src/screens/Search/Shell.tsx:530
|
||||
msgid "Find posts, users, and feeds on Bluesky"
|
||||
msgstr ""
|
||||
|
||||
@@ -4393,6 +4394,10 @@ msgstr ""
|
||||
msgid "Focus code input"
|
||||
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
|
||||
#: src/components/ProfileCard.tsx:546
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:495
|
||||
@@ -6485,13 +6490,9 @@ msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/feeds/FeedPage.tsx:181
|
||||
msgctxt "action"
|
||||
msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/shell/desktop/LeftNav.tsx:583
|
||||
msgctxt "action"
|
||||
msgid "New Post"
|
||||
msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:545
|
||||
@@ -8731,10 +8732,10 @@ msgid "Scroll to top"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dialogs/SearchablePeopleList.tsx:515
|
||||
#: src/components/forms/SearchInput.tsx:33
|
||||
#: src/components/forms/SearchInput.tsx:35
|
||||
#: src/components/forms/SearchInput.tsx:51
|
||||
#: src/components/forms/SearchInput.tsx:53
|
||||
#: 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
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
@@ -8772,7 +8773,7 @@ msgstr ""
|
||||
msgid "Search for \"{searchText}\""
|
||||
msgstr ""
|
||||
|
||||
#: src/view/shell/desktop/Search.tsx:128
|
||||
#: src/view/shell/desktop/Search.tsx:129
|
||||
msgid "Search for “{tQuery}”"
|
||||
msgstr "Search for “{tQuery}”"
|
||||
|
||||
@@ -9634,7 +9635,7 @@ msgstr ""
|
||||
msgid "Sorry, we're unable to load account suggestions at this time."
|
||||
msgstr ""
|
||||
|
||||
#: src/App.native.tsx:142
|
||||
#: src/App.native.tsx:141
|
||||
#: src/App.web.tsx:119
|
||||
msgid "Sorry! Your session expired. Please sign in again."
|
||||
msgstr ""
|
||||
|
||||
@@ -380,6 +380,7 @@ export function SearchScreenShell({
|
||||
inputPlaceholder ?? l`Search for posts, users, or feeds`
|
||||
}
|
||||
hitSlop={{...HITSLOP_20, top: 0}}
|
||||
hotkey={true}
|
||||
/>
|
||||
</View>
|
||||
{showAutocomplete && (
|
||||
|
||||
Vendored
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
type AppBskyFeedDefs,
|
||||
} from '@atproto/api'
|
||||
import {type QueryClient} from '@tanstack/react-query'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {findAllPostsInQueryData as findAllPostsInBookmarksQueryData} from '#/state/queries/bookmarks/useBookmarksQuery'
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import {type AppBskyActorDefs, type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {type QueryClient} from '@tanstack/react-query'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInActivitySubscriptionsQueryData} from '#/state/queries/activity-subscriptions'
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||
import {type DialogControlRefProps} from '#/components/Dialog'
|
||||
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
||||
import {IS_WEB} from '#/env'
|
||||
@@ -62,6 +63,7 @@ export function useDialogFullyExpandedCountContext() {
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [fullyExpandedCount, setFullyExpandedCount] = useState(0)
|
||||
const {disableScope, enableScope} = useHotkeysContext()
|
||||
|
||||
const activeDialogs = useRef<
|
||||
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
||||
@@ -77,18 +79,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
return openDialogs.current.size > 0
|
||||
} else {
|
||||
BottomSheetNativeComponent.dismissAll()
|
||||
void BottomSheetNativeComponent.dismissAll()
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setDialogIsOpen = useCallback((id: string, isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
openDialogs.current.add(id)
|
||||
} else {
|
||||
openDialogs.current.delete(id)
|
||||
}
|
||||
}, [])
|
||||
const setDialogIsOpen = useCallback(
|
||||
(id: string, isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
openDialogs.current.add(id)
|
||||
} else {
|
||||
openDialogs.current.delete(id)
|
||||
}
|
||||
if (openDialogs.current.size > 0) {
|
||||
disableScope('global')
|
||||
} else {
|
||||
enableScope('global')
|
||||
}
|
||||
},
|
||||
[disableScope, enableScope],
|
||||
)
|
||||
|
||||
const context = useMemo<IDialogContext>(
|
||||
() => ({
|
||||
|
||||
+9
-1
@@ -1,4 +1,4 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
type UnlistenFn = () => void
|
||||
|
||||
@@ -45,3 +45,11 @@ export function listenPostCreated(fn: () => void): UnlistenFn {
|
||||
emitter.on('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)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type GestureUpdateEvent,
|
||||
type PanGestureHandlerEventPayload,
|
||||
} from 'react-native-gesture-handler'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
export type GlobalGestureEvents = {
|
||||
begin: GestureStateChangeEvent<PanGestureHandlerEventPayload>
|
||||
|
||||
+11
-1
@@ -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 {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||
import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
|
||||
export type Lightbox = {
|
||||
@@ -28,6 +29,15 @@ LightboxControlContext.displayName = 'LightboxControlContext'
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
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(
|
||||
(lightbox: Omit<Lightbox, 'id'>) => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
type ChatBskyConvoGetLog,
|
||||
type ChatBskyConvoSendMessage,
|
||||
} from '@atproto/api'
|
||||
import {XRPCError} from '@atproto/xrpc'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {XRPCError} from '@atproto/api'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {type BskyAgent, type ChatBskyConvoGetLog} from '@atproto/api'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
|
||||
@@ -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 {useHotkeysContext} from '#/lib/hotkeys'
|
||||
|
||||
export interface UserAddRemoveListsModal {
|
||||
name: 'user-add-remove-lists'
|
||||
@@ -47,6 +48,15 @@ ModalControlContext.displayName = 'ModalControlContext'
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
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) => {
|
||||
setActiveModals(modals => [...modals, modal])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'react'
|
||||
import {AppState} from 'react-native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
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,
|
||||
])
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import {createContext, useContext, useState} from 'react'
|
||||
|
||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||
|
||||
type StateContext = boolean
|
||||
type SetContext = (v: boolean) => void
|
||||
|
||||
@@ -10,10 +12,22 @@ setContext.displayName = 'DrawerOpenSetContext'
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [state, setState] = useState(false)
|
||||
const {disableScope, enableScope} = useHotkeysContext()
|
||||
|
||||
const setDrawerOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
disableScope('global')
|
||||
} else {
|
||||
enableScope('global')
|
||||
}
|
||||
setState(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
<setContext.Provider value={setState}>{children}</setContext.Provider>
|
||||
<setContext.Provider value={setDrawerOpen}>
|
||||
{children}
|
||||
</setContext.Provider>
|
||||
</stateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
export const textInputWebEmitter = new EventEmitter()
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
withSpring,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||
import {useMinimalShellMode} from '#/state/shell'
|
||||
|
||||
@@ -400,10 +400,16 @@ function Btn({
|
||||
a.rounded_full,
|
||||
{backgroundColor: t.palette.primary_500},
|
||||
]}>
|
||||
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
|
||||
<Text
|
||||
style={styles.notificationCountLabel}
|
||||
maxFontSizeMultiplier={1.5}>
|
||||
{notificationCount}
|
||||
</Text>
|
||||
</View>
|
||||
) : hasNew ? (
|
||||
<View style={[styles.hasNewBadge, a.rounded_full]} />
|
||||
<View
|
||||
style={[styles.hasNewBadge, {backgroundColor: t.palette.primary_500}]}
|
||||
/>
|
||||
) : null}
|
||||
</PressableScale>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {StyleSheet} from 'react-native'
|
||||
|
||||
import {colors} from '#/lib/styles'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
@@ -24,8 +23,9 @@ export const styles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
left: '52%',
|
||||
top: 8,
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 1,
|
||||
paddingHorizontal: 5,
|
||||
paddingTop: 1,
|
||||
paddingBottom: 2,
|
||||
borderRadius: 6,
|
||||
zIndex: 1,
|
||||
},
|
||||
@@ -37,8 +37,9 @@ export const styles = StyleSheet.create({
|
||||
notificationCountLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
color: 'white',
|
||||
fontVariant: ['tabular-nums'],
|
||||
includeFontPadding: false,
|
||||
},
|
||||
hasNewBadge: {
|
||||
position: 'absolute',
|
||||
@@ -47,8 +48,7 @@ export const styles = StyleSheet.create({
|
||||
top: 10,
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: colors.blue3,
|
||||
borderRadius: 6,
|
||||
borderRadius: 4,
|
||||
zIndex: 1,
|
||||
},
|
||||
ctrlIcon: {
|
||||
|
||||
@@ -313,7 +313,9 @@ const NavItem: React.FC<{
|
||||
<Text style={styles.notificationCountLabel}>{notificationCount}</Text>
|
||||
</View>
|
||||
) : hasNew ? (
|
||||
<View style={styles.hasNewBadge} />
|
||||
<View
|
||||
style={[styles.hasNewBadge, {backgroundColor: t.palette.primary_500}]}
|
||||
/>
|
||||
) : null}
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -580,7 +580,7 @@ function ComposeBtn() {
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonIcon icon={EditBig} position="left" />
|
||||
<ButtonText>
|
||||
<Trans context="action">New Post</Trans>
|
||||
<Trans context="action">New post</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -3,7 +3,7 @@ import {View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
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 {useKawaiiMode} from '#/state/preferences/kawaii'
|
||||
|
||||
@@ -105,6 +105,7 @@ export function DesktopSearch() {
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onPressCancelSearch}
|
||||
onSubmitEditing={onSubmit}
|
||||
hotkey={true}
|
||||
/>
|
||||
{tQuery !== '' && isActive && moderationOpts && (
|
||||
<View
|
||||
|
||||
@@ -9,7 +9,6 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
|
||||
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {Lightbox} from '#/view/com/lightbox/Lightbox'
|
||||
import {ModalsContainer} from '#/view/com/modals/Modal'
|
||||
@@ -36,7 +35,7 @@ import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen'
|
||||
import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay'
|
||||
import {PassiveAnalytics} from '#/analytics/PassiveAnalytics'
|
||||
import {FlatNavigator, RoutesContainer} from '#/Navigation'
|
||||
import {Composer} from './Composer.web'
|
||||
import {Composer} from './Composer'
|
||||
import {DrawerContent} from './Drawer'
|
||||
|
||||
function ShellInner() {
|
||||
@@ -45,7 +44,6 @@ function ShellInner() {
|
||||
const {state: policyUpdateState} = usePolicyUpdateContext()
|
||||
const welcomeModalControl = useWelcomeModal()
|
||||
|
||||
useComposerKeyboardShortcut()
|
||||
useIntentHandler()
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user