Create codemod for addressing ESLint warnings (#10032)

This commit is contained in:
DS Boyce
2026-03-11 15:51:31 -07:00
committed by GitHub
parent 80429ec902
commit 1db01a09a8
240 changed files with 1560 additions and 1347 deletions
+135
View File
@@ -0,0 +1,135 @@
/**
* Codemod to replace namespaced React calls with named imports
*
* Before:
* import React from 'react'
* React.useEffect(() => {}, [])
*
* After:
* import { useEffect } from 'react'
* useEffect(() => {}, [])
*
* Usage: jscodeshift -t .jscodeshift/react-import.js <file-path>
* Example: jscodeshift -t .jscodeshift/react-import.js src/App.native.tsx
*/
/* eslint-disable */
export const parser = 'tsx'
export default function transformer(file, api) {
const j = api.jscodeshift
const root = j(file.source)
// Find the React import
let reactImportPath = null
const reactMembers = new Set()
root.find(j.ImportDeclaration).forEach(path => {
const node = path.value
if (node.source.value === 'react') {
node.specifiers.forEach(spec => {
// Check if this is a default import of React
if (
spec.type === 'ImportDefaultSpecifier' &&
spec.local.name === 'React'
) {
reactImportPath = path
}
})
}
})
if (!reactImportPath) {
// No React import found, nothing to do
return file.source
}
// Find all React.* member expressions
root
.find(j.MemberExpression)
.filter(path => {
const node = path.value
return (
node.object.type === 'Identifier' &&
node.object.name === 'React' &&
node.property.type === 'Identifier'
)
})
.forEach(path => {
const propertyName = path.value.property.name
reactMembers.add(propertyName)
})
// Find all React.* JSX member expressions (e.g., <React.Fragment>)
root
.find(j.JSXMemberExpression)
.filter(path => {
const node = path.value
return node.object.name === 'React' && node.property.name
})
.forEach(path => {
const propertyName = path.value.property.name
reactMembers.add(propertyName)
})
// If no React members are used, remove the import
if (reactMembers.size === 0) {
reactImportPath.prune()
return root.toSource()
}
// Sort the members for consistent output
const sortedMembers = Array.from(reactMembers).sort()
// Create new import specifiers
const newSpecifiers = sortedMembers.map(name =>
j.importSpecifier(j.identifier(name), j.identifier(name)),
)
// Get the existing import specifiers
const sortedImports = Array.from(reactImportPath.value.specifiers).sort()
const existingSpecifiers = sortedImports.filter(
specifier => specifier.type !== 'ImportDefaultSpecifier',
)
const allSpecifiers = [
...new Map(
[...existingSpecifiers, ...newSpecifiers].map(item => [
item.imported.name,
item,
]),
).values(),
]
// Update the import declaration
reactImportPath.value.specifiers = allSpecifiers
// Replace all React.* member expressions with just the identifier
root
.find(j.MemberExpression)
.filter(path => {
const node = path.value
return (
node.object.type === 'Identifier' &&
node.object.name === 'React' &&
node.property.type === 'Identifier'
)
})
.replaceWith(path => {
return j.identifier(path.value.property.name)
})
// Replace all React.* JSX member expressions with just the identifier
root
.find(j.JSXMemberExpression)
.filter(path => {
const node = path.value
return node.object.name === 'React' && node.property.name
})
.replaceWith(path => {
return j.jsxIdentifier(path.value.property.name)
})
return root.toSource()
}
+1
View File
@@ -37,6 +37,7 @@ export default defineConfig(
'*.e2e.ts', '*.e2e.ts',
'*.e2e.tsx', '*.e2e.tsx',
'eslint.config.mjs', 'eslint.config.mjs',
'.jscodeshift/**',
], ],
}, },
+5 -5
View File
@@ -1,7 +1,7 @@
import '#/logger/sentry/setup' import '#/logger/sentry/setup'
import '#/view/icons' import '#/view/icons'
import React, {useEffect, useState} from 'react' import {Fragment, useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler' import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller' import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
import { import {
@@ -111,7 +111,7 @@ prefetchLiveEvents()
prefetchAppConfig() prefetchAppConfig()
function InnerApp() { function InnerApp() {
const [isReady, setIsReady] = React.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()
@@ -152,7 +152,7 @@ function InnerApp() {
<ContextMenuProvider> <ContextMenuProvider>
<Splash isReady={isReady && hasCheckedReferrer}> <Splash isReady={isReady && hasCheckedReferrer}>
<VideoVolumeProvider> <VideoVolumeProvider>
<React.Fragment <Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<AnalyticsFeaturesContext> <AnalyticsFeaturesContext>
@@ -208,7 +208,7 @@ function InnerApp() {
</PolicyUpdateOverlayProvider> </PolicyUpdateOverlayProvider>
</QueryProvider> </QueryProvider>
</AnalyticsFeaturesContext> </AnalyticsFeaturesContext>
</React.Fragment> </Fragment>
</VideoVolumeProvider> </VideoVolumeProvider>
</Splash> </Splash>
</ContextMenuProvider> </ContextMenuProvider>
@@ -220,7 +220,7 @@ function InnerApp() {
function App() { function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
React.useEffect(() => { useEffect(() => {
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() => Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
setReady(true), setReady(true),
) )
+6 -8
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect} from 'react' import {forwardRef, useCallback, useEffect, useState} from 'react'
import { import {
AccessibilityInfo, AccessibilityInfo,
Image as RNImage, Image as RNImage,
@@ -29,7 +29,7 @@ const darkSplashImageUri = RNImage.resolveAssetSource(
darkSplashImagePointer, darkSplashImagePointer,
).uri ).uri
export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) { export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
const width = 1000 const width = 1000
const height = width * (67 / 64) const height = width * (67 / 64)
return ( return (
@@ -58,12 +58,10 @@ export function Splash(props: React.PropsWithChildren<Props>) {
const outroLogo = useSharedValue(0) const outroLogo = useSharedValue(0)
const outroApp = useSharedValue(0) const outroApp = useSharedValue(0)
const outroAppOpacity = useSharedValue(0) const outroAppOpacity = useSharedValue(0)
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) const [isAnimationComplete, setIsAnimationComplete] = useState(false)
const [isImageLoaded, setIsImageLoaded] = React.useState(false) const [isImageLoaded, setIsImageLoaded] = useState(false)
const [isLayoutReady, setIsLayoutReady] = React.useState(false) const [isLayoutReady, setIsLayoutReady] = useState(false)
const [reduceMotion, setReduceMotion] = React.useState<boolean | undefined>( const [reduceMotion, setReduceMotion] = useState<boolean | undefined>(false)
false,
)
const isReady = const isReady =
props.isReady && props.isReady &&
isImageLoaded && isImageLoaded &&
+11 -15
View File
@@ -1,4 +1,4 @@
import React from 'react' import {createContext, useCallback, useContext, useMemo, useState} from 'react'
import {type Theme, type ThemeName} from '@bsky.app/alf' import {type Theme, type ThemeName} from '@bsky.app/alf'
import { import {
@@ -46,7 +46,7 @@ export type Alf = {
/* /*
* Context * Context
*/ */
export const Context = React.createContext<Alf>({ export const Context = createContext<Alf>({
themeName: 'light', themeName: 'light',
theme: themes.light, theme: themes.light,
themes, themes,
@@ -65,15 +65,13 @@ export function ThemeProvider({
children, children,
theme: themeName, theme: themeName,
}: React.PropsWithChildren<{theme: ThemeName}>) { }: React.PropsWithChildren<{theme: ThemeName}>) {
const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() => const [fontScale, setFontScale] = useState<Alf['fonts']['scale']>(() =>
getFontScale(), getFontScale(),
) )
const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() => const [fontScaleMultiplier, setFontScaleMultiplier] = useState(() =>
computeFontScaleMultiplier(fontScale), computeFontScaleMultiplier(fontScale),
) )
const setFontScaleAndPersist = React.useCallback< const setFontScaleAndPersist = useCallback<Alf['fonts']['setFontScale']>(
Alf['fonts']['setFontScale']
>(
fs => { fs => {
setFontScale(fs) setFontScale(fs)
persistFontScale(fs) persistFontScale(fs)
@@ -81,12 +79,10 @@ export function ThemeProvider({
}, },
[setFontScale], [setFontScale],
) )
const [fontFamily, setFontFamily] = React.useState<Alf['fonts']['family']>( const [fontFamily, setFontFamily] = useState<Alf['fonts']['family']>(() =>
() => getFontFamily(), getFontFamily(),
) )
const setFontFamilyAndPersist = React.useCallback< const setFontFamilyAndPersist = useCallback<Alf['fonts']['setFontFamily']>(
Alf['fonts']['setFontFamily']
>(
ff => { ff => {
setFontFamily(ff) setFontFamily(ff)
persistFontFamily(ff) persistFontFamily(ff)
@@ -94,7 +90,7 @@ export function ThemeProvider({
[setFontFamily], [setFontFamily],
) )
const value = React.useMemo<Alf>( const value = useMemo<Alf>(
() => ({ () => ({
themes, themes,
themeName: themeName, themeName: themeName,
@@ -122,12 +118,12 @@ export function ThemeProvider({
} }
export function useAlf() { export function useAlf() {
return React.useContext(Context) return useContext(Context)
} }
export function useTheme(theme?: ThemeName) { export function useTheme(theme?: ThemeName) {
const alf = useAlf() const alf = useAlf()
return React.useMemo(() => { return useMemo(() => {
return theme ? alf.themes[theme] : alf.theme return theme ? alf.themes[theme] : alf.theme
}, [theme, alf]) }, [theme, alf])
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useLayoutEffect} from 'react'
import {type ColorSchemeName, useColorScheme} from 'react-native' import {type ColorSchemeName, useColorScheme} from 'react-native'
import {type ThemeName} from '@bsky.app/alf' import {type ThemeName} from '@bsky.app/alf'
@@ -9,7 +9,7 @@ import {IS_WEB} from '#/env'
export function useColorModeTheme(): ThemeName { export function useColorModeTheme(): ThemeName {
const theme = useThemeName() const theme = useThemeName()
React.useLayoutEffect(() => { useLayoutEffect(() => {
updateDocument(theme) updateDocument(theme)
}, [theme]) }, [theme])
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import {type Breakpoint, useBreakpoints} from '#/alf/breakpoints' import {type Breakpoint, useBreakpoints} from '#/alf/breakpoints'
import * as tokens from '#/alf/tokens' import * as tokens from '#/alf/tokens'
@@ -52,7 +52,7 @@ export function useGutters([top, right, bottom, left]: Gutter[]) {
bottom = top bottom = top
left = right left = right
} }
return React.useMemo(() => { return useMemo(() => {
return { return {
paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'],
paddingRight: paddingRight:
+3 -3
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import {Fragment, useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type AppBskyActorDefs} from '@atproto/api' import {type AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -52,7 +52,7 @@ export function AccountList({
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
]}> ]}>
{accounts.map(account => ( {accounts.map(account => (
<React.Fragment key={account.did}> <Fragment key={account.did}>
<AccountItem <AccountItem
profile={profiles?.profiles.find(p => p.did === account.did)} profile={profiles?.profiles.find(p => p.did === account.did)}
account={account} account={account}
@@ -61,7 +61,7 @@ export function AccountList({
isPendingAccount={account.did === pendingDid} isPendingAccount={account.did === pendingDid}
/> />
<View style={[a.border_b, t.atoms.border_contrast_low]} /> <View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment> </Fragment>
))} ))}
<Button <Button
testID="chooseAddAccountBtn" testID="chooseAddAccountBtn"
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@@ -20,7 +20,7 @@ export function AppLanguageDropdown() {
const setLangPrefs = useLanguagePrefsApi() const setLangPrefs = useLanguagePrefsApi()
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
const onChangeAppLanguage = React.useCallback( const onChangeAppLanguage = useCallback(
(value: string) => { (value: string) => {
if (!value) return if (!value) return
if (sanitizedLang !== value) { if (sanitizedLang !== value) {
+77 -71
View File
@@ -1,4 +1,11 @@
import React from 'react' import {
createContext,
forwardRef,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
import { import {
type AccessibilityProps, type AccessibilityProps,
type GestureResponderEvent, type GestureResponderEvent,
@@ -108,7 +115,7 @@ export type ButtonProps = Pick<
export type ButtonTextProps = TextProps & export type ButtonTextProps = TextProps &
VariantProps & {disabled?: boolean; emoji?: boolean} VariantProps & {disabled?: boolean; emoji?: boolean}
const Context = React.createContext<VariantProps & ButtonState>({ const Context = createContext<VariantProps & ButtonState>({
hovered: false, hovered: false,
focused: false, focused: false,
pressed: false, pressed: false,
@@ -117,10 +124,10 @@ const Context = React.createContext<VariantProps & ButtonState>({
Context.displayName = 'ButtonContext' Context.displayName = 'ButtonContext'
export function useButtonContext() { export function useButtonContext() {
return React.useContext(Context) return useContext(Context)
} }
export const Button = React.forwardRef<View, ButtonProps>( export const Button = forwardRef<View, ButtonProps>(
( (
{ {
children, children,
@@ -153,13 +160,13 @@ export const Button = React.forwardRef<View, ButtonProps>(
} }
const t = useTheme() const t = useTheme()
const [state, setState] = React.useState({ const [state, setState] = useState({
pressed: false, pressed: false,
hovered: false, hovered: false,
focused: false, focused: false,
}) })
const onPressIn = React.useCallback( const onPressIn = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setState(s => ({ setState(s => ({
...s, ...s,
@@ -169,7 +176,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}, },
[setState, onPressInOuter], [setState, onPressInOuter],
) )
const onPressOut = React.useCallback( const onPressOut = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
setState(s => ({ setState(s => ({
...s, ...s,
@@ -179,7 +186,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}, },
[setState, onPressOutOuter], [setState, onPressOutOuter],
) )
const onHoverIn = React.useCallback( const onHoverIn = useCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
setState(s => ({ setState(s => ({
...s, ...s,
@@ -189,7 +196,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}, },
[setState, onHoverInOuter], [setState, onHoverInOuter],
) )
const onHoverOut = React.useCallback( const onHoverOut = useCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
setState(s => ({ setState(s => ({
...s, ...s,
@@ -199,7 +206,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}, },
[setState, onHoverOutOuter], [setState, onHoverOutOuter],
) )
const onFocus = React.useCallback( const onFocus = useCallback(
(e: NativeSyntheticEvent<TargetedEvent>) => { (e: NativeSyntheticEvent<TargetedEvent>) => {
setState(s => ({ setState(s => ({
...s, ...s,
@@ -209,7 +216,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}, },
[setState, onFocusOuter], [setState, onFocusOuter],
) )
const onBlur = React.useCallback( const onBlur = useCallback(
(e: NativeSyntheticEvent<TargetedEvent>) => { (e: NativeSyntheticEvent<TargetedEvent>) => {
setState(s => ({ setState(s => ({
...s, ...s,
@@ -220,7 +227,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
[setState, onBlurOuter], [setState, onBlurOuter],
) )
const {baseStyles, hoverStyles} = React.useMemo(() => { const {baseStyles, hoverStyles} = useMemo(() => {
const baseStyles: ViewStyle[] = [] const baseStyles: ViewStyle[] = []
const hoverStyles: ViewStyle[] = [] const hoverStyles: ViewStyle[] = []
@@ -526,7 +533,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
} }
}, [t, variant, color, size, shape, disabled]) }, [t, variant, color, size, shape, disabled])
const context = React.useMemo<ButtonContext>( const context = useMemo<ButtonContext>(
() => ({ () => ({
...state, ...state,
variant, variant,
@@ -581,7 +588,7 @@ Button.displayName = 'Button'
export function useSharedButtonTextStyles() { export function useSharedButtonTextStyles() {
const t = useTheme() const t = useTheme()
const {color, variant, disabled, size} = useButtonContext() const {color, variant, disabled, size} = useButtonContext()
return React.useMemo(() => { return useMemo(() => {
const baseStyles: TextStyle[] = [] const baseStyles: TextStyle[] = []
/* /*
@@ -778,67 +785,66 @@ export function ButtonIcon({
}) { }) {
const {size: buttonSize, shape: buttonShape} = useButtonContext() const {size: buttonSize, shape: buttonShape} = useButtonContext()
const textStyles = useSharedButtonTextStyles() const textStyles = useSharedButtonTextStyles()
const {iconSize, iconContainerSize, iconNegativeMargin} = const {iconSize, iconContainerSize, iconNegativeMargin} = useMemo(() => {
React.useMemo(() => { /**
/** * Pre-set icon sizes for different button sizes
* Pre-set icon sizes for different button sizes */
*/ const iconSizeShorthand =
const iconSizeShorthand = size ??
size ?? (({
(({ large: 'md',
large: 'md', small: 'sm',
small: 'sm', tiny: 'xs',
tiny: 'xs', }[buttonSize || 'small'] || 'sm') as Exclude<
}[buttonSize || 'small'] || 'sm') as Exclude< SVGIconProps['size'],
SVGIconProps['size'], undefined
undefined >)
>)
/* /*
* Copied here from icons/common.tsx so we can tweak if we need to, but * Copied here from icons/common.tsx so we can tweak if we need to, but
* also so that we can calculate transforms. * also so that we can calculate transforms.
*/ */
const iconSize = { const iconSize = {
xs: 12, xs: 12,
sm: 16, sm: 16,
md: 18, md: 18,
lg: 24, lg: 24,
xl: 28, xl: 28,
'2xs': 8, '2xs': 8,
'2xl': 32, '2xl': 32,
'3xl': 40, '3xl': 40,
}[iconSizeShorthand] }[iconSizeShorthand]
/* /*
* Goal here is to match rendered text size so that different size icons * Goal here is to match rendered text size so that different size icons
* don't increase button size * don't increase button size
*/ */
const iconContainerSize = { const iconContainerSize = {
large: 20, large: 20,
small: 17, small: 17,
tiny: 15, tiny: 15,
}[buttonSize || 'small']
/*
* The icon needs to be closer to the edge of the button than the text. Therefore
* we make the gap slightly too large, and then pull in the sides using negative margins.
*/
let iconNegativeMargin = 0
if (buttonShape === 'default') {
iconNegativeMargin = {
large: -2,
small: -2,
tiny: -1,
}[buttonSize || 'small'] }[buttonSize || 'small']
}
/* return {
* The icon needs to be closer to the edge of the button than the text. Therefore iconSize,
* we make the gap slightly too large, and then pull in the sides using negative margins. iconContainerSize,
*/ iconNegativeMargin,
let iconNegativeMargin = 0 }
}, [buttonSize, buttonShape, size])
if (buttonShape === 'default') {
iconNegativeMargin = {
large: -2,
small: -2,
tiny: -1,
}[buttonSize || 'small']
}
return {
iconSize,
iconContainerSize,
iconNegativeMargin,
}
}, [buttonSize, buttonShape, size])
return ( return (
<View <View
+8 -5
View File
@@ -1,4 +1,7 @@
import React, { import {
cloneElement,
Fragment,
isValidElement,
useCallback, useCallback,
useEffect, useEffect,
useId, useId,
@@ -689,22 +692,22 @@ export function Outer({
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
]}> ]}>
{flattenReactChildren(children).map((child, i) => { {flattenReactChildren(children).map((child, i) => {
return React.isValidElement(child) && return isValidElement(child) &&
(child.type === Item || child.type === Divider) ? ( (child.type === Item || child.type === Divider) ? (
<React.Fragment key={i}> <Fragment key={i}>
{i > 0 ? ( {i > 0 ? (
<View <View
style={[a.border_b, t.atoms.border_contrast_low]} style={[a.border_b, t.atoms.border_contrast_low]}
/> />
) : null} ) : null}
{React.cloneElement(child, { {cloneElement(child, {
// @ts-expect-error not typed // @ts-expect-error not typed
style: { style: {
borderRadius: 0, borderRadius: 0,
borderWidth: 0, borderWidth: 0,
}, },
})} })}
</React.Fragment> </Fragment>
) : null ) : null
})} })}
</View> </View>
+16 -9
View File
@@ -1,4 +1,11 @@
import React, {useImperativeHandle} from 'react' import {
forwardRef,
useCallback,
useContext,
useImperativeHandle,
useMemo,
useState,
} from 'react'
import { import {
FlatList, FlatList,
type FlatListProps, type FlatListProps,
@@ -48,15 +55,15 @@ export function Outer({
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const [isOpen, setIsOpen] = React.useState(false) const [isOpen, setIsOpen] = useState(false)
const {setDialogIsOpen} = useDialogStateControlContext() const {setDialogIsOpen} = useDialogStateControlContext()
const open = React.useCallback(() => { const open = useCallback(() => {
setDialogIsOpen(control.id, true) setDialogIsOpen(control.id, true)
setIsOpen(true) setIsOpen(true)
}, [setIsOpen, setDialogIsOpen, control.id]) }, [setIsOpen, setDialogIsOpen, control.id])
const close = React.useCallback<DialogControlProps['close']>( const close = useCallback<DialogControlProps['close']>(
cb => { cb => {
setDialogIsOpen(control.id, false) setDialogIsOpen(control.id, false)
setIsOpen(false) setIsOpen(false)
@@ -80,7 +87,7 @@ export function Outer({
[control.id, onClose, setDialogIsOpen], [control.id, onClose, setDialogIsOpen],
) )
const handleBackgroundPress = React.useCallback( const handleBackgroundPress = useCallback(
async (e: GestureResponderEvent) => { async (e: GestureResponderEvent) => {
webOptions?.onBackgroundPress ? webOptions.onBackgroundPress(e) : close() webOptions?.onBackgroundPress ? webOptions.onBackgroundPress(e) : close()
}, },
@@ -96,7 +103,7 @@ export function Outer({
[close, open], [close, open],
) )
const context = React.useMemo( const context = useMemo(
() => ({ () => ({
close, close,
isNativeDialog: false, isNativeDialog: false,
@@ -165,7 +172,7 @@ export function Inner({
contentContainerStyle, contentContainerStyle,
}: DialogInnerProps) { }: DialogInnerProps) {
const t = useTheme() const t = useTheme()
const {close} = React.useContext(Context) const {close} = useContext(Context)
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {reduceMotionEnabled} = useA11y() const {reduceMotionEnabled} = useA11y()
FocusGuards.useFocusGuards() FocusGuards.useFocusGuards()
@@ -215,7 +222,7 @@ export function Inner({
export const ScrollableInner = Inner export const ScrollableInner = Inner
export const InnerFlatList = React.forwardRef< export const InnerFlatList = forwardRef<
FlatList, FlatList,
FlatListProps<any> & {label: string} & { FlatListProps<any> & {label: string} & {
webInnerStyle?: StyleProp<ViewStyle> webInnerStyle?: StyleProp<ViewStyle>
@@ -284,7 +291,7 @@ export function FlatListFooter({
export function Close() { export function Close() {
const {_} = useLingui() const {_} = useLingui()
const {close} = React.useContext(Context) const {close} = useContext(Context)
return ( return (
<View <View
style={[ style={[
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react' import {useEffect} from 'react'
import {type DialogControlProps} from '#/components/Dialog/types' import {type DialogControlProps} from '#/components/Dialog/types'
export function useAutoOpen(control: DialogControlProps, showTimeout?: number) { export function useAutoOpen(control: DialogControlProps, showTimeout?: number) {
React.useEffect(() => { useEffect(() => {
if (showTimeout) { if (showTimeout) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
control.open() control.open()
-1
View File
@@ -1,5 +1,4 @@
import {View} from 'react-native' import {View} from 'react-native'
import type React from 'react'
import {atoms as a, type ViewStyleProp} from '#/alf' import {atoms as a, type ViewStyleProp} from '#/alf'
+4 -6
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useRef} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
type AppBskyActorDefs, type AppBskyActorDefs,
@@ -46,9 +46,7 @@ export function KnownFollowers({
minimal?: boolean minimal?: boolean
showIfEmpty?: boolean showIfEmpty?: boolean
}) { }) {
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>( const cache = useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(new Map())
new Map(),
)
/* /*
* Results for `knownFollowers` are not sorted consistently, so when * Results for `knownFollowers` are not sorted consistently, so when
@@ -190,7 +188,7 @@ function KnownFollowersInner({
numberOfLines={2}> numberOfLines={2}>
{slice.length >= 2 ? ( {slice.length >= 2 ? (
// 2-n followers, including blocks // 2-n followers, including blocks
serverCount > 2 ? ( serverCount > 2 ? ( // only 2
<Trans> <Trans>
Followed by{' '} Followed by{' '}
<Text emoji key={slice[0].profile.did} style={textStyle}> <Text emoji key={slice[0].profile.did} style={textStyle}>
@@ -206,7 +204,7 @@ function KnownFollowersInner({
one="# other" one="# other"
other="# others" other="# others"
/> />
</Trans> // only 2 </Trans>
) : ( ) : (
<Trans> <Trans>
Followed by{' '} Followed by{' '}
@@ -3,7 +3,6 @@ import {type AppBskyLabelerDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro' import {Plural, Trans} from '@lingui/react/macro'
import type React from 'react'
import {getLabelingServiceTitle} from '#/lib/moderation' import {getLabelingServiceTitle} from '#/lib/moderation'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -22,7 +22,7 @@ export function LanguageSelect({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const handleOnChange = React.useCallback( const handleOnChange = useCallback(
(value: string) => { (value: string) => {
if (!value) return if (!value) return
onChange(sanitizeAppLanguageSetting(value)) onChange(sanitizeAppLanguageSetting(value))
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react' import {createContext} from 'react'
export const ScrollbarOffsetContext = React.createContext({ export const ScrollbarOffsetContext = createContext({
isWithinOffsetView: false, isWithinOffsetView: false,
}) })
ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext' ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext'
+5 -5
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useMemo, useState} from 'react'
import {type AppBskyFeedGetLikes as GetLikes} from '@atproto/api' import {type AppBskyFeedGetLikes as GetLikes} 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'
@@ -29,7 +29,7 @@ function keyExtractor(item: GetLikes.Like) {
export function LikedByList({uri}: {uri: string}) { export function LikedByList({uri}: {uri: string}) {
const {_} = useLingui() const {_} = useLingui()
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = useState(false)
const { const {
data: resolvedUri, data: resolvedUri,
@@ -49,14 +49,14 @@ export function LikedByList({uri}: {uri: string}) {
const error = resolveError || likedByError const error = resolveError || likedByError
const isError = !!resolveError || !!likedByError const isError = !!resolveError || !!likedByError
const likes = React.useMemo(() => { const likes = useMemo(() => {
if (data?.pages) { if (data?.pages) {
return data.pages.flatMap(page => page.likes) return data.pages.flatMap(page => page.likes)
} }
return [] return []
}, [data]) }, [data])
const onRefresh = React.useCallback(async () => { const onRefresh = useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
try { try {
await refetch() await refetch()
@@ -66,7 +66,7 @@ export function LikedByList({uri}: {uri: string}) {
setIsPTRing(false) setIsPTRing(false)
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => { const onEndReached = useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || isError) return if (isFetchingNextPage || !hasNextPage || isError) return
try { try {
await fetchNextPage() await fetchNextPage()
@@ -1,6 +1,5 @@
import {type StyleProp, type ViewStyle} from 'react-native' import {type StyleProp, type ViewStyle} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import type React from 'react'
import {gradients} from '#/alf/tokens' import {gradients} from '#/alf/tokens'
+4 -4
View File
@@ -1,4 +1,4 @@
import React, {useMemo} from 'react' import {useCallback, useMemo} from 'react'
import {type GestureResponderEvent, Linking} from 'react-native' import {type GestureResponderEvent, Linking} from 'react-native'
import {sanitizeUrl} from '@braintree/sanitize-url' import {sanitizeUrl} from '@braintree/sanitize-url'
import { import {
@@ -117,7 +117,7 @@ export function useLink({
const {linkWarningDialogControl} = useGlobalDialogsControlContext() const {linkWarningDialogControl} = useGlobalDialogsControlContext()
const openLink = useOpenLink() const openLink = useOpenLink()
const onPress = React.useCallback( const onPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
const exitEarlyIfFalse = outerOnPress?.(e) const exitEarlyIfFalse = outerOnPress?.(e)
@@ -217,7 +217,7 @@ export function useLink({
], ],
) )
const handleLongPress = React.useCallback(() => { const handleLongPress = useCallback(() => {
const requiresWarning = Boolean( const requiresWarning = Boolean(
!disableMismatchWarning && !disableMismatchWarning &&
displayText && displayText &&
@@ -242,7 +242,7 @@ export function useLink({
linkWarningDialogControl, linkWarningDialogControl,
]) ])
const onLongPress = React.useCallback( const onLongPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
const exitEarlyIfFalse = outerOnLongPress?.(e) const exitEarlyIfFalse = outerOnLongPress?.(e)
if (exitEarlyIfFalse === false) return if (exitEarlyIfFalse === false) return
+3 -3
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useEffect, useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
type AppBskyGraphDefs, type AppBskyGraphDefs,
@@ -88,11 +88,11 @@ export function Link({
}: Props & Omit<LinkProps, 'to' | 'label'>) { }: Props & Omit<LinkProps, 'to' | 'label'>) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const href = React.useMemo(() => { const href = useMemo(() => {
return createProfileListHref({list: view}) return createProfileListHref({list: view})
}, [view]) }, [view])
React.useEffect(() => { useEffect(() => {
precacheList(queryClient, view) precacheList(queryClient, view)
}, [view, queryClient]) }, [view, queryClient])
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useEffect} from 'react'
import Animated, { import Animated, {
Easing, Easing,
useAnimatedStyle, useAnimatedStyle,
@@ -20,7 +20,7 @@ export function Loader(props: Props) {
transform: [{rotate: rotation.get() + 'deg'}], transform: [{rotate: rotation.get() + 'deg'}],
})) }))
React.useEffect(() => { useEffect(() => {
rotation.set(() => rotation.set(() =>
withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1), withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
) )
-1
View File
@@ -1,5 +1,4 @@
import {StyleSheet} from 'react-native' import {StyleSheet} from 'react-native'
import type React from 'react'
import {atoms as a, platform, useTheme, type ViewStyleProp} from '#/alf' import {atoms as a, platform, useTheme, type ViewStyleProp} from '#/alf'
import {Fill} from '#/components/Fill' import {Fill} from '#/components/Fill'
+5 -5
View File
@@ -1,15 +1,15 @@
import React from 'react' import {createContext, useContext} from 'react'
import {type ContextType, type ItemContextType} from '#/components/Menu/types' import {type ContextType, type ItemContextType} from '#/components/Menu/types'
export const Context = React.createContext<ContextType | null>(null) export const Context = createContext<ContextType | null>(null)
Context.displayName = 'MenuContext' Context.displayName = 'MenuContext'
export const ItemContext = React.createContext<ItemContextType | null>(null) export const ItemContext = createContext<ItemContextType | null>(null)
ItemContext.displayName = 'MenuItemContext' ItemContext.displayName = 'MenuItemContext'
export function useMenuContext() { export function useMenuContext() {
const context = React.useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
throw new Error('useMenuContext must be used within a Context.Provider') throw new Error('useMenuContext must be used within a Context.Provider')
@@ -19,7 +19,7 @@ export function useMenuContext() {
} }
export function useMenuItemContext() { export function useMenuItemContext() {
const context = React.useContext(ItemContext) const context = useContext(ItemContext)
if (!context) { if (!context) {
throw new Error('useMenuItemContext must be used within a Context.Provider') throw new Error('useMenuItemContext must be used within a Context.Provider')
-1
View File
@@ -4,7 +4,6 @@ import {
type GestureResponderEvent, type GestureResponderEvent,
type PressableProps, type PressableProps,
} from 'react-native' } from 'react-native'
import type React from 'react'
import {type TextStyleProp, type ViewStyleProp} from '#/alf' import {type TextStyleProp, type ViewStyleProp} from '#/alf'
import type * as Dialog from '#/components/Dialog' import type * as Dialog from '#/components/Dialog'
+4 -4
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api' import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -32,7 +32,7 @@ export function Row({
size = 'sm', size = 'sm',
}: {children: React.ReactNode | React.ReactNode[]} & CommonProps & }: {children: React.ReactNode | React.ReactNode[]} & CommonProps &
ViewStyleProp) { ViewStyleProp) {
const styles = React.useMemo(() => { const styles = useMemo(() => {
switch (size) { switch (size) {
case 'lg': case 'lg':
return [{gap: 5}] return [{gap: 5}]
@@ -67,7 +67,7 @@ export function Label({
const isBlueskyLabel = const isBlueskyLabel =
desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID
const {outer, avi, text} = React.useMemo(() => { const {outer, avi, text} = useMemo(() => {
switch (size) { switch (size) {
case 'lg': { case 'lg': {
return { return {
@@ -154,7 +154,7 @@ export function Label({
export function FollowsYou({size = 'sm'}: CommonProps) { export function FollowsYou({size = 'sm'}: CommonProps) {
const t = useTheme() const t = useTheme()
const variantStyles = React.useMemo(() => { const variantStyles = useMemo(() => {
switch (size) { switch (size) {
case 'sm': case 'sm':
case 'lg': case 'lg':
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useRef, useState} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
type GestureResponderEvent, type GestureResponderEvent,
@@ -31,16 +31,16 @@ export function ExternalGif({
const consentDialogControl = useDialogControl() const consentDialogControl = useDialogControl()
// Tracking if the placer has been activated // Tracking if the placer has been activated
const [isPlayerActive, setIsPlayerActive] = React.useState(false) const [isPlayerActive, setIsPlayerActive] = useState(false)
// Tracking whether the gif has been loaded yet // Tracking whether the gif has been loaded yet
const [isPrefetched, setIsPrefetched] = React.useState(false) const [isPrefetched, setIsPrefetched] = useState(false)
// Tracking whether the image is animating // Tracking whether the image is animating
const [isAnimating, setIsAnimating] = React.useState(true) const [isAnimating, setIsAnimating] = useState(true)
// Used for controlling animation // Used for controlling animation
const imageRef = React.useRef<Image>(null) const imageRef = useRef<Image>(null)
const load = React.useCallback(() => { const load = useCallback(() => {
setIsPlayerActive(true) setIsPlayerActive(true)
Image.prefetch(params.playerUri).then(() => { Image.prefetch(params.playerUri).then(() => {
// Replace the image once it's fetched // Replace the image once it's fetched
@@ -48,7 +48,7 @@ export function ExternalGif({
}) })
}, [params.playerUri]) }, [params.playerUri])
const onPlayPress = React.useCallback( const onPlayPress = useCallback(
(event: GestureResponderEvent) => { (event: GestureResponderEvent) => {
// Don't propagate on web // Don't propagate on web
event.preventDefault() event.preventDefault()
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
type GestureResponderEvent, type GestureResponderEvent,
@@ -84,7 +84,7 @@ function Player({
}) { }) {
// ensures we only load what's requested // ensures we only load what's requested
// when it's a youtube video, we need to allow both bsky.app and youtube.com // when it's a youtube video, we need to allow both bsky.app and youtube.com
const onShouldStartLoadWithRequest = React.useCallback( const onShouldStartLoadWithRequest = useCallback(
(event: ShouldStartLoadRequest) => (event: ShouldStartLoadRequest) =>
event.url === params.playerUri || event.url === params.playerUri ||
(params.source.startsWith('youtube') && (params.source.startsWith('youtube') &&
@@ -129,10 +129,10 @@ export function ExternalPlayer({
const externalEmbedsPrefs = useExternalEmbedsPrefs() const externalEmbedsPrefs = useExternalEmbedsPrefs()
const consentDialogControl = useDialogControl() const consentDialogControl = useDialogControl()
const [isPlayerActive, setPlayerActive] = React.useState(false) const [isPlayerActive, setPlayerActive] = useState(false)
const [isLoading, setIsLoading] = React.useState(true) const [isLoading, setIsLoading] = useState(true)
const aspect = React.useMemo(() => { const aspect = useMemo(() => {
return getPlayerAspect({ return getPlayerAspect({
type: params.type, type: params.type,
width: windowDims.width, width: windowDims.width,
@@ -166,7 +166,7 @@ export function ExternalPlayer({
}, false) // False here disables autostarting the callback }, false) // False here disables autostarting the callback
// watch for leaving the viewport due to scrolling // watch for leaving the viewport due to scrolling
React.useEffect(() => { useEffect(() => {
// We don't want to do anything if the player isn't active // We don't want to do anything if the player isn't active
if (!isPlayerActive) return if (!isPlayerActive) return
@@ -185,11 +185,11 @@ export function ExternalPlayer({
} }
}, [navigation, isPlayerActive, frameCallback]) }, [navigation, isPlayerActive, frameCallback])
const onLoad = React.useCallback(() => { const onLoad = useCallback(() => {
setIsLoading(false) setIsLoading(false)
}, []) }, [])
const onPlayPress = React.useCallback( const onPlayPress = useCallback(
(event: GestureResponderEvent) => { (event: GestureResponderEvent) => {
// Prevent this from propagating upward on web // Prevent this from propagating upward on web
event.preventDefault() event.preventDefault()
@@ -204,7 +204,7 @@ export function ExternalPlayer({
[externalEmbedsPrefs, consentDialogControl, params.source], [externalEmbedsPrefs, consentDialogControl, params.source],
) )
const onAcceptConsent = React.useCallback(() => { const onAcceptConsent = useCallback(() => {
setPlayerActive(true) setPlayerActive(true)
}, []) }, [])
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import {useCallback, useMemo} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native' import {type StyleProp, View, type ViewStyle} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {type AppBskyEmbedExternal} from '@atproto/api' import {type AppBskyEmbedExternal} from '@atproto/api'
@@ -38,7 +38,7 @@ export const ExternalEmbed = ({
const externalEmbedPrefs = useExternalEmbedsPrefs() const externalEmbedPrefs = useExternalEmbedsPrefs()
const niceUrl = toNiceDomain(link.uri) const niceUrl = toNiceDomain(link.uri)
const imageUri = link.thumb const imageUri = link.thumb
const embedPlayerParams = React.useMemo(() => { const embedPlayerParams = useMemo(() => {
const params = parseEmbedPlayerFromUrl(link.uri) const params = parseEmbedPlayerFromUrl(link.uri)
if (params && externalEmbedPrefs?.[params.source] !== 'hide') { if (params && externalEmbedPrefs?.[params.source] !== 'hide') {
@@ -1,5 +1,7 @@
import React, { import {
createContext,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useId, useId,
useMemo, useMemo,
@@ -10,7 +12,7 @@ import {useWindowDimensions} from 'react-native'
import {IS_NATIVE, IS_WEB} from '#/env' import {IS_NATIVE, IS_WEB} from '#/env'
const Context = React.createContext<{ const Context = createContext<{
activeViewId: string | null activeViewId: string | null
setActiveView: (viewId: string) => void setActiveView: (viewId: string) => void
sendViewPosition: (viewId: string, y: number) => void sendViewPosition: (viewId: string, y: number) => void
@@ -94,7 +96,7 @@ export function Provider({children}: {children: React.ReactNode}) {
} }
export function useActiveVideoWeb() { export function useActiveVideoWeb() {
const context = React.useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
throw new Error( throw new Error(
'useActiveVideoWeb must be used within a ActiveVideoWebProvider', 'useActiveVideoWeb must be used within a ActiveVideoWebProvider',
@@ -1,7 +1,6 @@
import React from 'react' import {createContext, useContext, useMemo, useState} from 'react'
const Context = React.createContext<{ const Context = createContext<{
// native
muted: boolean muted: boolean
setMuted: React.Dispatch<React.SetStateAction<boolean>> setMuted: React.Dispatch<React.SetStateAction<boolean>>
// web // web
@@ -11,10 +10,10 @@ const Context = React.createContext<{
Context.displayName = 'VideoVolumeContext' Context.displayName = 'VideoVolumeContext'
export function Provider({children}: {children: React.ReactNode}) { export function Provider({children}: {children: React.ReactNode}) {
const [muted, setMuted] = React.useState(true) const [muted, setMuted] = useState(true)
const [volume, setVolume] = React.useState(1) const [volume, setVolume] = useState(1)
const value = React.useMemo( const value = useMemo(
() => ({ () => ({
muted, muted,
setMuted, setMuted,
@@ -28,7 +27,7 @@ export function Provider({children}: {children: React.ReactNode}) {
} }
export function useVideoVolumeState() { export function useVideoVolumeState() {
const context = React.useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
throw new Error( throw new Error(
'useVideoVolumeState must be used within a VideoVolumeProvider', 'useVideoVolumeState must be used within a VideoVolumeProvider',
@@ -38,7 +37,7 @@ export function useVideoVolumeState() {
} }
export function useVideoMuteState() { export function useVideoMuteState() {
const context = React.useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
throw new Error( throw new Error(
'useVideoMuteState must be used within a VideoVolumeProvider', 'useVideoMuteState must be used within a VideoVolumeProvider',
@@ -4,7 +4,6 @@ import {type AppBskyFeedDefs} 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'
import type React from 'react'
import {useCleanError} from '#/lib/hooks/useCleanError' import {useCleanError} from '#/lib/hooks/useCleanError'
import {type Shadow} from '#/state/cache/post-shadow' import {type Shadow} from '#/state/cache/post-shadow'
+15 -15
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import {memo, useCallback, useEffect, useMemo, useReducer, useRef} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
type AppBskyActorDefs, type AppBskyActorDefs,
@@ -61,7 +61,7 @@ const floatingMiddlewares = [
export function ProfileHoverCard(props: ProfileHoverCardProps) { export function ProfileHoverCard(props: ProfileHoverCardProps) {
const prefetchProfileQuery = usePrefetchProfileQuery() const prefetchProfileQuery = usePrefetchProfileQuery()
const prefetchedProfile = React.useRef(false) const prefetchedProfile = useRef(false)
const onPointerMove = () => { const onPointerMove = () => {
if (!prefetchedProfile.current) { if (!prefetchedProfile.current) {
prefetchedProfile.current = true prefetchedProfile.current = true
@@ -116,7 +116,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
middleware: floatingMiddlewares, middleware: floatingMiddlewares,
}) })
const [currentState, dispatch] = React.useReducer( const [currentState, dispatch] = useReducer(
// Tip: console.log(state, action) when debugging. // Tip: console.log(state, action) when debugging.
(state: State, action: Action): State => { (state: State, action: Action): State => {
// Pressing within a card should always hide it. // Pressing within a card should always hide it.
@@ -262,7 +262,7 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
{stage: 'hidden'}, {stage: 'hidden'},
) )
React.useEffect(() => { useEffect(() => {
if (currentState.effect) { if (currentState.effect) {
const effect = currentState.effect const effect = currentState.effect
return effect() return effect()
@@ -270,16 +270,16 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
}, [currentState]) }, [currentState])
const prefetchProfileQuery = usePrefetchProfileQuery() const prefetchProfileQuery = usePrefetchProfileQuery()
const prefetchedProfile = React.useRef(false) const prefetchedProfile = useRef(false)
const prefetchIfNeeded = React.useCallback(async () => { const prefetchIfNeeded = useCallback(async () => {
if (!prefetchedProfile.current) { if (!prefetchedProfile.current) {
prefetchedProfile.current = true prefetchedProfile.current = true
prefetchProfileQuery(props.did) prefetchProfileQuery(props.did)
} }
}, [prefetchProfileQuery, props.did]) }, [prefetchProfileQuery, props.did])
const didFireHover = React.useRef(false) const didFireHover = useRef(false)
const onPointerMoveTarget = React.useCallback(() => { const onPointerMoveTarget = useCallback(() => {
prefetchIfNeeded() prefetchIfNeeded()
// Conceptually we want something like onPointerEnter, // Conceptually we want something like onPointerEnter,
// but we want to ignore entering only due to scrolling. // but we want to ignore entering only due to scrolling.
@@ -290,20 +290,20 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
} }
}, [prefetchIfNeeded]) }, [prefetchIfNeeded])
const onPointerLeaveTarget = React.useCallback(() => { const onPointerLeaveTarget = useCallback(() => {
didFireHover.current = false didFireHover.current = false
dispatch('unhovered-target') dispatch('unhovered-target')
}, []) }, [])
const onPointerEnterCard = React.useCallback(() => { const onPointerEnterCard = useCallback(() => {
dispatch('hovered-card') dispatch('hovered-card')
}, []) }, [])
const onPointerLeaveCard = React.useCallback(() => { const onPointerLeaveCard = useCallback(() => {
dispatch('unhovered-card') dispatch('unhovered-card')
}, []) }, [])
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
dispatch('pressed') dispatch('pressed')
}, []) }, [])
@@ -411,7 +411,7 @@ let Card = ({
</View> </View>
) )
} }
Card = React.memo(Card) Card = memo(Card)
function Inner({ function Inner({
profile, profile,
@@ -425,7 +425,7 @@ function Inner({
const t = useTheme() const t = useTheme()
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const moderation = React.useMemo( const moderation = useMemo(
() => moderateProfile(profile, moderationOpts), () => moderateProfile(profile, moderationOpts),
[profile, moderationOpts], [profile, moderationOpts],
) )
@@ -453,7 +453,7 @@ function Inner({
did: profile.did, did: profile.did,
handle: profile.handle, handle: profile.handle,
}) })
const isMe = React.useMemo( const isMe = useMemo(
() => currentAccount?.did === profile.did, () => currentAccount?.did === profile.did,
[currentAccount, profile], [currentAccount, profile],
) )
+15 -8
View File
@@ -1,4 +1,11 @@
import React, {useImperativeHandle} from 'react' import {
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {Pressable, useWindowDimensions, View} from 'react-native' import {Pressable, useWindowDimensions, View} from 'react-native'
import Animated, { import Animated, {
Easing, Easing,
@@ -28,25 +35,25 @@ export interface ProgressGuideToastProps {
visibleDuration?: number // default 5s visibleDuration?: number // default 5s
} }
export const ProgressGuideToast = React.forwardRef< export const ProgressGuideToast = forwardRef<
ProgressGuideToastRef, ProgressGuideToastRef,
ProgressGuideToastProps ProgressGuideToastProps
>(function ProgressGuideToast({title, subtitle, visibleDuration}, ref) { >(function ProgressGuideToast({title, subtitle, visibleDuration}, ref) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const [isOpen, setIsOpen] = React.useState(false) const [isOpen, setIsOpen] = useState(false)
const translateY = useSharedValue(0) const translateY = useSharedValue(0)
const opacity = useSharedValue(0) const opacity = useSharedValue(0)
const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null) const animatedCheckRef = useRef<AnimatedCheckRef | null>(null)
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined) const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
const winDim = useWindowDimensions() const winDim = useWindowDimensions()
/** /**
* Methods * Methods
*/ */
const close = React.useCallback(() => { const close = useCallback(() => {
// clear the timeout, in case this was called imperatively // clear the timeout, in case this was called imperatively
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current) clearTimeout(timeoutRef.current)
@@ -67,7 +74,7 @@ export const ProgressGuideToast = React.forwardRef<
) )
}, [setIsOpen, opacity]) }, [setIsOpen, opacity])
const open = React.useCallback(() => { const open = useCallback(() => {
// set isOpen=true to render // set isOpen=true to render
setIsOpen(true) setIsOpen(true)
@@ -105,7 +112,7 @@ export const ProgressGuideToast = React.forwardRef<
[open, close], [open, close],
) )
const containerStyle = React.useMemo(() => { const containerStyle = useMemo(() => {
let left = 10 let left = 10
let right = 10 let right = 10
if (IS_WEB && winDim.width > 400) { if (IS_WEB && winDim.width > 400) {
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import {type StyleProp, Text as RNText, type TextStyle} from 'react-native' import {type StyleProp, Text as RNText, type TextStyle} 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'
@@ -68,7 +68,7 @@ export function RichTextTag({
/* /*
* Mute word records that exactly match the tag in question. * Mute word records that exactly match the tag in question.
*/ */
const removeableMuteWords = React.useMemo(() => { const removeableMuteWords = useMemo(() => {
return ( return (
preferences?.moderationPrefs.mutedWords?.filter(word => { preferences?.moderationPrefs.mutedWords?.filter(word => {
return word.value === tag return word.value === tag
-1
View File
@@ -6,7 +6,6 @@ import Animated, {
SlideInLeft, SlideInLeft,
SlideInRight, SlideInRight,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import type React from 'react'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native' import {type ListRenderItemInfo, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
@@ -19,9 +19,9 @@ interface ProfilesListProps {
scrollElRef: ListRef scrollElRef: ListRef
} }
export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>( export const FeedsList = forwardRef<SectionRef, ProfilesListProps>(
function FeedsListImpl({feeds, headerHeight, scrollElRef}, ref) { function FeedsListImpl({feeds, headerHeight, scrollElRef}, ref) {
const [initialHeaderHeight] = React.useState(headerHeight) const [initialHeaderHeight] = useState(headerHeight)
const bottomBarOffset = useBottomBarOffset(20) const bottomBarOffset = useBottomBarOffset(20)
const t = useTheme() const t = useTheme()
@@ -32,7 +32,7 @@ export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>(
}) })
}, [scrollElRef, headerHeight]) }, [scrollElRef, headerHeight])
React.useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop, scrollToTop: onScrollToTop,
})) }))
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import {forwardRef, useCallback, useImperativeHandle} from 'react'
import {View} from 'react-native' 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'
@@ -17,7 +17,7 @@ interface ProfilesListProps {
scrollElRef: ListRef scrollElRef: ListRef
} }
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>( export const PostsList = forwardRef<SectionRef, ProfilesListProps>(
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) { function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
const feed: FeedDescriptor = `list|${listUri}` const feed: FeedDescriptor = `list|${listUri}`
const {_} = useLingui() const {_} = useLingui()
@@ -29,7 +29,7 @@ export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
}) })
}, [scrollElRef, headerHeight]) }, [scrollElRef, headerHeight])
React.useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop, scrollToTop: onScrollToTop,
})) }))
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native' import {type ListRenderItemInfo, View} from 'react-native'
import { import {
type AppBskyActorDefs, type AppBskyActorDefs,
@@ -37,7 +37,7 @@ interface ProfilesListProps {
scrollElRef: ListRef scrollElRef: ListRef
} }
export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>( export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
function ProfilesListImpl( function ProfilesListImpl(
{listUri, moderationOpts, headerHeight, scrollElRef}, {listUri, moderationOpts, headerHeight, scrollElRef},
ref, ref,
@@ -48,7 +48,7 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {data, refetch, isError} = useAllListMembersQuery(listUri) const {data, refetch, isError} = useAllListMembersQuery(listUri)
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = useState(false)
// The server returns these sorted by descending creation date, so we want to invert // The server returns these sorted by descending creation date, so we want to invert
@@ -80,7 +80,7 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
}) })
}, [scrollElRef, headerHeight]) }, [scrollElRef, headerHeight])
React.useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop, scrollToTop: onScrollToTop,
})) }))
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {AppBskyGraphStarterpack, AtUri} from '@atproto/api' import {AppBskyGraphStarterpack, AtUri} from '@atproto/api'
@@ -115,7 +115,7 @@ export function useStarterPackLink({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const qc = useQueryClient() const qc = useQueryClient()
const {rkey, handleOrDid} = React.useMemo(() => { const {rkey, handleOrDid} = useMemo(() => {
const rkey = new AtUri(view.uri).rkey const rkey = new AtUri(view.uri).rkey
const {creator} = view const {creator} = view
return {rkey, handleOrDid: creator.handle || creator.did} return {rkey, handleOrDid: creator.handle || creator.did}
@@ -148,7 +148,7 @@ export function Link({
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {record} = starterPack const {record} = starterPack
const {rkey, handleOrDid} = React.useMemo(() => { const {rkey, handleOrDid} = useMemo(() => {
const rkey = new AtUri(starterPack.uri).rkey const rkey = new AtUri(starterPack.uri).rkey
const {creator} = starterPack const {creator} = starterPack
return {rkey, handleOrDid: creator.handle || creator.did} return {rkey, handleOrDid: creator.handle || creator.did}
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {isValidElement} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {toast as sonner, Toaster} from 'sonner-native' import {toast as sonner, Toaster} from 'sonner-native'
@@ -61,7 +61,7 @@ export function show(
duration: options?.duration ?? DURATION, duration: options?.duration ?? DURATION,
}, },
) )
} else if (React.isValidElement(content)) { } else if (isValidElement(content)) {
sonner.custom( sonner.custom(
<ToastConfigProvider id={id} type={type}> <ToastConfigProvider id={id} type={type}>
{content} {content}
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {isValidElement} from 'react'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {toast as sonner, Toaster} from 'sonner' import {toast as sonner, Toaster} from 'sonner'
@@ -60,7 +60,7 @@ export function show(
duration: options?.duration ?? DURATION, duration: options?.duration ?? DURATION,
}, },
) )
} else if (React.isValidElement(content)) { } else if (isValidElement(content)) {
sonner( sonner(
<ToastConfigProvider id={id} type={type}> <ToastConfigProvider id={id} type={type}>
{content} {content}
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type AtUri} from '@atproto/api' import {type AtUri} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -170,7 +170,7 @@ type ParsedTrendingTopic =
export function useTopic(raw: TrendingTopic): ParsedTrendingTopic { export function useTopic(raw: TrendingTopic): ParsedTrendingTopic {
const {_} = useLingui() const {_} = useLingui()
return React.useMemo(() => { return useMemo(() => {
const {topic: displayName, link} = raw const {topic: displayName, link} = raw
if (link.startsWith('/search')) { if (link.startsWith('/search')) {
@@ -1,4 +1,4 @@
import React from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {ToolsOzoneReportDefs} from '@atproto/api' import {ToolsOzoneReportDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -44,7 +44,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
const {gtPhone} = useBreakpoints() const {gtPhone} = useBreakpoints()
const agent = useAgent() const agent = useAgent()
const [details, setDetails] = React.useState('') const [details, setDetails] = useState('')
const isInvalid = details.length > 1000 const isInvalid = details.length > 1000
const {mutate, isPending} = useMutation({ const {mutate, isPending} = useMutation({
+64 -65
View File
@@ -1,4 +1,4 @@
import React from 'react' import {forwardRef, useCallback, useEffect, useImperativeHandle} from 'react'
import Animated, { import Animated, {
Easing, Easing,
useAnimatedProps, useAnimatedProps,
@@ -23,74 +23,73 @@ export interface AnimatedCheckProps extends Props {
playOnMount?: boolean playOnMount?: boolean
} }
export const AnimatedCheck = React.forwardRef< export const AnimatedCheck = forwardRef<AnimatedCheckRef, AnimatedCheckProps>(
AnimatedCheckRef, function AnimatedCheck({playOnMount, ...props}, ref) {
AnimatedCheckProps const {fill, size, style, ...rest} = useCommonSVGProps(props)
>(function AnimatedCheck({playOnMount, ...props}, ref) { const circleAnim = useSharedValue(0)
const {fill, size, style, ...rest} = useCommonSVGProps(props) const checkAnim = useSharedValue(0)
const circleAnim = useSharedValue(0)
const checkAnim = useSharedValue(0)
const circleAnimatedProps = useAnimatedProps(() => ({ const circleAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 166 - circleAnim.get() * 166, strokeDashoffset: 166 - circleAnim.get() * 166,
})) }))
const checkAnimatedProps = useAnimatedProps(() => ({ const checkAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 48 - 48 * checkAnim.get(), strokeDashoffset: 48 - 48 * checkAnim.get(),
})) }))
const play = React.useCallback( const play = useCallback(
(cb?: () => void) => { (cb?: () => void) => {
circleAnim.set(0) circleAnim.set(0)
checkAnim.set(0) checkAnim.set(0)
circleAnim.set(() => circleAnim.set(() =>
withTiming(1, {duration: 500, easing: Easing.linear}), withTiming(1, {duration: 500, easing: Easing.linear}),
) )
checkAnim.set(() => checkAnim.set(() =>
withDelay( withDelay(
500, 500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb), withTiming(1, {duration: 300, easing: Easing.linear}, cb),
), ),
) )
}, },
[circleAnim, checkAnim], [circleAnim, checkAnim],
) )
React.useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
play, play,
})) }))
React.useEffect(() => { useEffect(() => {
if (playOnMount) { if (playOnMount) {
play() play()
} }
}, [play, playOnMount]) }, [play, playOnMount])
return ( return (
<Svg <Svg
fill="none"
{...rest}
viewBox="0 0 52 52"
width={size}
height={size}
style={style}>
<AnimatedCircle
animatedProps={circleAnimatedProps}
cx="26"
cy="26"
r="24"
fill="none" fill="none"
stroke={fill} {...rest}
strokeWidth={4} viewBox="0 0 52 52"
strokeDasharray={166} width={size}
/> height={size}
<AnimatedPath style={style}>
animatedProps={checkAnimatedProps} <AnimatedCircle
stroke={fill} animatedProps={circleAnimatedProps}
d={PATH} cx="26"
strokeWidth={4} cy="26"
strokeDasharray={48} r="24"
/> fill="none"
</Svg> stroke={fill}
) strokeWidth={4}
}) strokeDasharray={166}
/>
<AnimatedPath
animatedProps={checkAnimatedProps}
stroke={fill}
d={PATH}
strokeWidth={4}
strokeDasharray={48}
/>
</Svg>
)
},
)
+4 -6
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native' 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'
@@ -125,12 +125,10 @@ function BirthdayInner({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const cleanError = useCleanError() const cleanError = useCleanError()
const [date, setDate] = React.useState( const [date, setDate] = useState(preferences.birthDate || getDateAgo(18))
preferences.birthDate || getDateAgo(18),
)
const {isPending, error, mutateAsync: setBirthDate} = useBirthdateMutation() const {isPending, error, mutateAsync: setBirthDate} = useBirthdateMutation()
const hasChanged = date !== preferences.birthDate const hasChanged = date !== preferences.birthDate
const errorMessage = React.useMemo(() => { const errorMessage = useMemo(() => {
if (error) { if (error) {
const {raw, clean} = cleanError(error) const {raw, clean} = cleanError(error)
return clean || raw || error.toString() return clean || raw || error.toString()
@@ -141,7 +139,7 @@ function BirthdayInner({
const isUnder13 = age < 13 const isUnder13 = age < 13
const isUnder18 = age >= 13 && age < 18 const isUnder18 = age >= 13 && age < 18
const onSave = React.useCallback(async () => { const onSave = useCallback(async () => {
try { try {
// skip if date is the same // skip if date is the same
if (hasChanged) { if (hasChanged) {
+8 -8
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api' import {type AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -58,13 +58,13 @@ function MutedWordsInner() {
error: preferencesError, error: preferencesError,
} = usePreferencesQuery() } = usePreferencesQuery()
const {isPending, mutateAsync: addMutedWord} = useUpsertMutedWordsMutation() const {isPending, mutateAsync: addMutedWord} = useUpsertMutedWordsMutation()
const [field, setField] = React.useState('') const [field, setField] = useState('')
const [targets, setTargets] = React.useState(['content']) const [targets, setTargets] = useState(['content'])
const [error, setError] = React.useState('') const [error, setError] = useState('')
const [durations, setDurations] = React.useState(['forever']) const [durations, setDurations] = useState(['forever'])
const [excludeFollowing, setExcludeFollowing] = React.useState(false) const [excludeFollowing, setExcludeFollowing] = useState(false)
const submit = React.useCallback(async () => { const submit = useCallback(async () => {
const sanitizedValue = sanitizeMutedWordValue(field) const sanitizedValue = sanitizeMutedWordValue(field)
const surfaces = ['tag', targets.includes('content') && 'content'].filter( const surfaces = ['tag', targets.includes('content') && 'content'].filter(
Boolean, Boolean,
@@ -431,7 +431,7 @@ function MutedWordRow({
const isExpired = expiryDate && expiryDate < new Date() const isExpired = expiryDate && expiryDate < new Date()
const formatDistance = useFormatDistance() const formatDistance = useFormatDistance()
const remove = React.useCallback(async () => { const remove = useCallback(async () => {
control.close() control.close()
removeMutedWord(word) removeMutedWord(word)
}, [removeMutedWord, word, control]) }, [removeMutedWord, word, control])
+3 -3
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {View} from 'react-native' 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'
@@ -32,12 +32,12 @@ function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) {
const {requestSwitchToAccount} = useLoggedOutViewControls() const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
const showSignIn = React.useCallback(() => { const showSignIn = useCallback(() => {
closeAllActiveElements() closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'none'}) requestSwitchToAccount({requestedAccount: 'none'})
}, [requestSwitchToAccount, closeAllActiveElements]) }, [requestSwitchToAccount, closeAllActiveElements])
const showCreateAccount = React.useCallback(() => { const showCreateAccount = useCallback(() => {
closeAllActiveElements() closeAllActiveElements()
requestSwitchToAccount({requestedAccount: 'new'}) requestSwitchToAccount({requestedAccount: 'new'})
}, [requestSwitchToAccount, closeAllActiveElements]) }, [requestSwitchToAccount, closeAllActiveElements])
-1
View File
@@ -5,7 +5,6 @@ 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 {StackActions, useNavigation} from '@react-navigation/native' import {StackActions, useNavigation} from '@react-navigation/native'
import type React from 'react'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
+3 -6
View File
@@ -1,4 +1,4 @@
import React from 'react' import {Fragment} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type ModerationCause} from '@atproto/api' import {type ModerationCause} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -25,7 +25,6 @@ export function BlockedByListDialog({
return ( return (
<Prompt.Outer control={control} testID="blockedByListDialog"> <Prompt.Outer control={control} testID="blockedByListDialog">
<Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText> <Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText>
<View style={[a.gap_sm, a.pb_lg]}> <View style={[a.gap_sm, a.pb_lg]}>
<Text <Text
selectable selectable
@@ -39,7 +38,7 @@ export function BlockedByListDialog({
{_(msg`Lists blocking this user:`)}{' '} {_(msg`Lists blocking this user:`)}{' '}
{listBlocks.map((block, i) => {listBlocks.map((block, i) =>
block.source.type === 'list' ? ( block.source.type === 'list' ? (
<React.Fragment key={block.source.list.uri}> <Fragment key={block.source.list.uri}>
{i === 0 ? null : ', '} {i === 0 ? null : ', '}
<InlineLinkText <InlineLinkText
label={block.source.list.name} label={block.source.list.name}
@@ -47,16 +46,14 @@ export function BlockedByListDialog({
style={[a.text_md, a.leading_snug]}> style={[a.text_md, a.leading_snug]}>
{block.source.list.name} {block.source.list.name}
</InlineLinkText> </InlineLinkText>
</React.Fragment> </Fragment>
) : null, ) : null,
)} )}
</Text> </Text>
</View> </View>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Action cta={_(msg`I understand`)} onPress={() => {}} /> <Prompt.Action cta={_(msg`I understand`)} onPress={() => {}} />
</Prompt.Actions> </Prompt.Actions>
<Dialog.Close /> <Dialog.Close />
</Prompt.Outer> </Prompt.Outer>
) )
+6 -6
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useMemo, useState} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import Animated, { import Animated, {
runOnJS, runOnJS,
@@ -24,11 +24,11 @@ export function ChatEmptyPill() {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const playHaptic = useHaptics() const playHaptic = useHaptics()
const [promptIndex, setPromptIndex] = React.useState(lastIndex) const [promptIndex, setPromptIndex] = useState(lastIndex)
const scale = useSharedValue(1) const scale = useSharedValue(1)
const prompts = React.useMemo(() => { const prompts = useMemo(() => {
return [ return [
_(msg`Say hello!`), _(msg`Say hello!`),
_(msg`Share your favorite feed!`), _(msg`Share your favorite feed!`),
@@ -40,17 +40,17 @@ export function ChatEmptyPill() {
] ]
}, [_]) }, [_])
const onPressIn = React.useCallback(() => { const onPressIn = useCallback(() => {
if (IS_WEB) return if (IS_WEB) return
scale.set(() => withTiming(1.075, {duration: 100})) scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale]) }, [scale])
const onPressOut = React.useCallback(() => { const onPressOut = useCallback(() => {
if (IS_WEB) return if (IS_WEB) return
scale.set(() => withTiming(1, {duration: 100})) scale.set(() => withTiming(1, {duration: 100}))
}, [scale]) }, [scale])
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
runOnJS(playHaptic)() runOnJS(playHaptic)()
let randomPromptIndex = Math.floor(Math.random() * prompts.length) let randomPromptIndex = Math.floor(Math.random() * prompts.length)
while (randomPromptIndex === lastIndex) { while (randomPromptIndex === lastIndex) {
+3 -3
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import {memo, useCallback} from 'react'
import {Keyboard, View} from 'react-native' import {Keyboard, View} from 'react-native'
import {type ChatBskyConvoDefs, type ModerationCause} from '@atproto/api' import {type ChatBskyConvoDefs, type ModerationCause} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -159,7 +159,7 @@ let ConvoMenu = ({
</> </>
) )
} }
ConvoMenu = React.memo(ConvoMenu) ConvoMenu = memo(ConvoMenu)
function MenuContent({ function MenuContent({
convo: initialConvo, convo: initialConvo,
@@ -211,7 +211,7 @@ function MenuContent({
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
const toggleBlock = React.useCallback(() => { const toggleBlock = useCallback(() => {
if (listBlocks.length) { if (listBlocks.length) {
blockedByListControl.open() blockedByListControl.open()
return return
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {memo} from 'react'
import {View} from 'react-native' 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'
@@ -78,5 +78,5 @@ let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => {
</View> </View>
) )
} }
DateDivider = React.memo(DateDivider) DateDivider = memo(DateDivider)
export {DateDivider} export {DateDivider}
+3 -3
View File
@@ -1,6 +1,6 @@
import React from 'react' import {createContext, useContext} from 'react'
const MessageContext = React.createContext(false) const MessageContext = createContext(false)
MessageContext.displayName = 'MessageContext' MessageContext.displayName = 'MessageContext'
export function MessageContextProvider({ export function MessageContextProvider({
@@ -14,5 +14,5 @@ export function MessageContextProvider({
} }
export function useIsWithinMessage() { export function useIsWithinMessage() {
return React.useContext(MessageContext) return useContext(MessageContext)
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useMemo} from 'react' import {memo, useCallback, useMemo} from 'react'
import { import {
type GestureResponderEvent, type GestureResponderEvent,
type StyleProp, type StyleProp,
@@ -233,7 +233,7 @@ let MessageItem = ({
</> </>
) )
} }
MessageItem = React.memo(MessageItem) MessageItem = memo(MessageItem)
export {MessageItem} export {MessageItem}
let MessageItemMetadata = ({ let MessageItemMetadata = ({
@@ -328,5 +328,5 @@ let MessageItemMetadata = ({
</Text> </Text>
) )
} }
MessageItemMetadata = React.memo(MessageItemMetadata) MessageItemMetadata = memo(MessageItemMetadata)
export {MessageItemMetadata} export {MessageItemMetadata}
+3 -4
View File
@@ -1,10 +1,9 @@
import React from 'react' import {memo} from 'react'
import {useWindowDimensions, View} from 'react-native' import {useWindowDimensions, View} from 'react-native'
import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api' import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api'
import {atoms as a, native, tokens, useTheme, web} from '#/alf' import {atoms as a, native, tokens, useTheme, web} from '#/alf'
import {PostEmbedViewContext} from '#/components/Post/Embed' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
import {Embed} from '#/components/Post/Embed'
import {MessageContextProvider} from './MessageContext' import {MessageContextProvider} from './MessageContext'
let MessageItemEmbed = ({ let MessageItemEmbed = ({
@@ -43,5 +42,5 @@ let MessageItemEmbed = ({
</MessageContextProvider> </MessageContextProvider>
) )
} }
MessageItemEmbed = React.memo(MessageItemEmbed) MessageItemEmbed = memo(MessageItemEmbed)
export {MessageItemEmbed} export {MessageItemEmbed}
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type AppBskyActorDefs} from '@atproto/api' import {type AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -39,7 +39,7 @@ export function MessageProfileButton({
}, },
}) })
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
if (!convoAvailability?.canChat) { if (!convoAvailability?.canChat) {
return return
} }
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type ModerationDecision} from '@atproto/api' import {type ModerationDecision} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -38,7 +38,7 @@ export function MessagesListBlockedFooter({
const reportControl = useDialogControl() const reportControl = useDialogControl()
const blockedByListControl = useDialogControl() const blockedByListControl = useDialogControl()
const {listBlocks, userBlock} = React.useMemo(() => { const {listBlocks, userBlock} = useMemo(() => {
const modui = moderation.ui('profileView') const modui = moderation.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking') const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list') const listBlocks = blocks.filter(alert => alert.source.type === 'list')
@@ -51,7 +51,7 @@ export function MessagesListBlockedFooter({
const isBlocking = !!userBlock || !!listBlocks.length const isBlocking = !!userBlock || !!listBlocks.length
const onUnblockPress = React.useCallback(() => { const onUnblockPress = useCallback(() => {
if (listBlocks.length) { if (listBlocks.length) {
blockedByListControl.open() blockedByListControl.open()
} else { } else {
+4 -4
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import Animated, { import Animated, {
runOnJS, runOnJS,
@@ -33,17 +33,17 @@ export function NewMessagesPill({
const scale = useSharedValue(1) const scale = useSharedValue(1)
const onPressIn = React.useCallback(() => { const onPressIn = useCallback(() => {
if (IS_WEB) return if (IS_WEB) return
scale.set(() => withTiming(1.075, {duration: 100})) scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale]) }, [scale])
const onPressOut = React.useCallback(() => { const onPressOut = useCallback(() => {
if (IS_WEB) return if (IS_WEB) return
scale.set(() => withTiming(1, {duration: 100})) scale.set(() => withTiming(1, {duration: 100}))
}, [scale]) }, [scale])
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
runOnJS(playHaptic)() runOnJS(playHaptic)()
onPressInner?.() onPressInner?.()
}, [onPressInner, playHaptic]) }, [onPressInner, playHaptic])
+3 -3
View File
@@ -1,4 +1,4 @@
import React from 'react' import {forwardRef, useCallback} from 'react'
import {StyleSheet, type TextInput, type TextInputProps} from 'react-native' import {StyleSheet, type TextInput, type TextInputProps} from 'react-native'
// @ts-expect-error untyped // @ts-expect-error untyped
import {unstable_createElement} from 'react-native-web' import {unstable_createElement} from 'react-native-web'
@@ -11,7 +11,7 @@ import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const LabelText = TextField.LabelText export const LabelText = TextField.LabelText
const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>( const InputBase = forwardRef<HTMLInputElement, TextInputProps>(
({style, ...props}, ref) => { ({style, ...props}, ref) => {
return unstable_createElement('input', { return unstable_createElement('input', {
...props, ...props,
@@ -42,7 +42,7 @@ export function DateField({
accessibilityHint, accessibilityHint,
maximumDate, maximumDate,
}: DateFieldProps) { }: DateFieldProps) {
const handleOnChange = React.useCallback( const handleOnChange = useCallback(
(e: any) => { (e: any) => {
const date = e.target.valueAsDate || e.target.value const date = e.target.valueAsDate || e.target.value
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {Keyboard, View} from 'react-native' import {Keyboard, 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'
@@ -28,7 +28,7 @@ export function HostingProvider({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const onPressSelectService = React.useCallback(() => { const onPressSelectService = useCallback(() => {
Keyboard.dismiss() Keyboard.dismiss()
serverInputControl.open() serverInputControl.open()
onOpenDialog?.() onOpenDialog?.()
+6 -6
View File
@@ -1,4 +1,4 @@
import React from 'react' import {Children, cloneElement, Fragment, isValidElement} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {atoms, useTheme} from '#/alf' import {atoms, useTheme} from '#/alf'
@@ -8,19 +8,19 @@ import {atoms, useTheme} from '#/alf'
*/ */
export function InputGroup(props: React.PropsWithChildren<{}>) { export function InputGroup(props: React.PropsWithChildren<{}>) {
const t = useTheme() const t = useTheme()
const children = React.Children.toArray(props.children) const children = Children.toArray(props.children)
const total = children.length const total = children.length
return ( return (
<View style={[atoms.w_full]}> <View style={[atoms.w_full]}>
{children.map((child, i) => { {children.map((child, i) => {
return React.isValidElement(child) ? ( return isValidElement(child) ? (
<React.Fragment key={i}> <Fragment key={i}>
{i > 0 ? ( {i > 0 ? (
<View <View
style={[atoms.border_b, {borderColor: t.palette.contrast_500}]} style={[atoms.border_b, {borderColor: t.palette.contrast_500}]}
/> />
) : null} ) : null}
{React.cloneElement(child, { {cloneElement(child, {
// @ts-ignore // @ts-ignore
style: [ style: [
// @ts-ignore // @ts-ignore
@@ -38,7 +38,7 @@ export function InputGroup(props: React.PropsWithChildren<{}>) {
}, },
], ],
})} })}
</React.Fragment> </Fragment>
) : null ) : null
})} })}
</View> </View>
+3 -3
View File
@@ -1,9 +1,9 @@
import React from 'react' import {useEffect, useState} from 'react'
export function useDelayedLoading(delay: number, initialState: boolean = true) { export function useDelayedLoading(delay: number, initialState: boolean = true) {
const [isLoading, setIsLoading] = React.useState(initialState) const [isLoading, setIsLoading] = useState(initialState)
React.useEffect(() => { useEffect(() => {
let timeout: NodeJS.Timeout let timeout: NodeJS.Timeout
// on initial load, show a loading spinner for a hot sec to prevent flash // on initial load, show a loading spinner for a hot sec to prevent flash
if (isLoading) timeout = setTimeout(() => setIsLoading(false), delay) if (isLoading) timeout = setTimeout(() => setIsLoading(false), delay)
+3 -3
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -25,7 +25,7 @@ export function useFollowMethods({
logContext, logContext,
) )
const follow = React.useCallback(() => { const follow = useCallback(() => {
requireAuth(async () => { requireAuth(async () => {
try { try {
await queueFollow() await queueFollow()
@@ -38,7 +38,7 @@ export function useFollowMethods({
}) })
}, [_, queueFollow, requireAuth]) }, [_, queueFollow, requireAuth])
const unfollow = React.useCallback(() => { const unfollow = useCallback(() => {
requireAuth(async () => { requireAuth(async () => {
try { try {
await queueUnfollow() await queueUnfollow()
+5 -5
View File
@@ -1,16 +1,16 @@
import React from 'react' import {useCallback, useMemo, useState} from 'react'
export function useInteractionState() { export function useInteractionState() {
const [state, setState] = React.useState(false) const [state, setState] = useState(false)
const onIn = React.useCallback(() => { const onIn = useCallback(() => {
setState(true) setState(true)
}, []) }, [])
const onOut = React.useCallback(() => { const onOut = useCallback(() => {
setState(false) setState(false)
}, []) }, [])
return React.useMemo( return useMemo(
() => ({ () => ({
state, state,
onIn, onIn,
+5 -5
View File
@@ -1,12 +1,12 @@
import React from 'react' import {useEffect, useState} from 'react'
import {RichText as RichTextAPI} from '@atproto/api' import {RichText as RichTextAPI} from '@atproto/api'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
export function useRichText(text: string): [RichTextAPI, boolean] { export function useRichText(text: string): [RichTextAPI, boolean] {
const [prevText, setPrevText] = React.useState(text) const [prevText, setPrevText] = useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text})) const [rawRT, setRawRT] = useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null) const [resolvedRT, setResolvedRT] = useState<RichTextAPI | null>(null)
const agent = useAgent() const agent = useAgent()
if (text !== prevText) { if (text !== prevText) {
setPrevText(text) setPrevText(text)
@@ -14,7 +14,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
setResolvedRT(null) setResolvedRT(null)
// This will queue an immediate re-render // This will queue an immediate re-render
} }
React.useEffect(() => { useEffect(() => {
let ignore = false let ignore = false
async function resolveRTFacets() { async function resolveRTFacets() {
// new each time // new each time
@@ -1,4 +1,4 @@
import React from 'react' import {useEffect, useState} from 'react'
import { import {
createStarterPackLinkFromAndroidReferrer, createStarterPackLinkFromAndroidReferrer,
@@ -10,11 +10,11 @@ import {IS_ANDROID} from '#/env'
import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
export function useStarterPackEntry() { export function useStarterPackEntry() {
const [ready, setReady] = React.useState(false) const [ready, setReady] = useState(false)
const setActiveStarterPack = useSetActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
const hasCheckedForStarterPack = useHasCheckedForStarterPack() const hasCheckedForStarterPack = useHasCheckedForStarterPack()
React.useEffect(() => { useEffect(() => {
if (ready) return if (ready) return
// On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So, // On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So,
+3 -3
View File
@@ -1,14 +1,14 @@
import React from 'react' import {useEffect, useState} from 'react'
import {httpStarterPackUriToAtUri} from '#/lib/strings/starter-pack' import {httpStarterPackUriToAtUri} from '#/lib/strings/starter-pack'
import {useSetActiveStarterPack} from '#/state/shell/starter-pack' import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
export function useStarterPackEntry() { export function useStarterPackEntry() {
const [ready, setReady] = React.useState(false) const [ready, setReady] = useState(false)
const setActiveStarterPack = useSetActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
React.useEffect(() => { useEffect(() => {
const href = window.location.href const href = window.location.href
const atUri = httpStarterPackUriToAtUri(href) const atUri = httpStarterPackUriToAtUri(href)
+5 -5
View File
@@ -1,9 +1,9 @@
import React from 'react' import {forwardRef} from 'react'
import Svg, {Path} from 'react-native-svg' import Svg, {Path} from 'react-native-svg'
import {type Props, useCommonSVGProps} from '#/components/icons/common' import {type Props, useCommonSVGProps} from '#/components/icons/common'
export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( export const IconTemplate_Stroke2_Corner0_Rounded = forwardRef(
function LogoImpl(props: Props, ref) { function LogoImpl(props: Props, ref) {
const {fill, size, style, ...rest} = useCommonSVGProps(props) const {fill, size, style, ...rest} = useCommonSVGProps(props)
@@ -41,7 +41,7 @@ export function createSinglePathSVG({
strokeLinecap?: 'butt' | 'round' | 'square' strokeLinecap?: 'butt' | 'round' | 'square'
strokeLinejoin?: 'miter' | 'round' | 'bevel' strokeLinejoin?: 'miter' | 'round' | 'bevel'
}) { }) {
return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) { return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
const hasStroke = strokeWidth > 0 const hasStroke = strokeWidth > 0
@@ -72,7 +72,7 @@ export function createSinglePathSVG({
} }
export function createSinglePathSVG2({path}: {path: string}) { export function createSinglePathSVG2({path}: {path: string}) {
return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) { return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
return ( return (
@@ -92,7 +92,7 @@ export function createSinglePathSVG2({path}: {path: string}) {
} }
export function createMultiPathSVG({paths}: {paths: string[]}) { export function createMultiPathSVG({paths}: {paths: string[]}) {
return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) { return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
return ( return (
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react' import {forwardRef} from 'react'
import Svg, {Circle, Path} from 'react-native-svg' import Svg, {Circle, Path} from 'react-native-svg'
import {type Props, useCommonSVGProps} from '#/components/icons/common' import {type Props, useCommonSVGProps} from '#/components/icons/common'
export const VerifiedCheck = React.forwardRef<Svg, Props>( export const VerifiedCheck = forwardRef<Svg, Props>(
function LogoImpl(props, ref) { function LogoImpl(props, ref) {
const {fill, size, style, ...rest} = useCommonSVGProps(props) const {fill, size, style, ...rest} = useCommonSVGProps(props)
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react' import {forwardRef} from 'react'
import Svg, {Path} from 'react-native-svg' import Svg, {Path} from 'react-native-svg'
import {type Props, useCommonSVGProps} from '#/components/icons/common' import {type Props, useCommonSVGProps} from '#/components/icons/common'
export const VerifierCheck = React.forwardRef<Svg, Props>( export const VerifierCheck = forwardRef<Svg, Props>(
function LogoImpl(props, ref) { function LogoImpl(props, ref) {
const {fill, size, style, ...rest} = useCommonSVGProps(props) const {fill, size, style, ...rest} = useCommonSVGProps(props)
+5 -5
View File
@@ -1,4 +1,4 @@
import React from 'react' import {createContext, useContext, useMemo, useState} from 'react'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {type DialogControlProps} from '#/components/Dialog' import {type DialogControlProps} from '#/components/Dialog'
@@ -10,17 +10,17 @@ interface Context {
setVerifyEmailState: (state: {code: string} | undefined) => void setVerifyEmailState: (state: {code: string} | undefined) => void
} }
const Context = React.createContext({} as Context) const Context = createContext({} as Context)
Context.displayName = 'IntentDialogsContext' Context.displayName = 'IntentDialogsContext'
export const useIntentDialogs = () => React.useContext(Context) export const useIntentDialogs = () => useContext(Context)
export function Provider({children}: {children: React.ReactNode}) { export function Provider({children}: {children: React.ReactNode}) {
const verifyEmailDialogControl = Dialog.useDialogControl() const verifyEmailDialogControl = Dialog.useDialogControl()
const [verifyEmailState, setVerifyEmailState] = React.useState< const [verifyEmailState, setVerifyEmailState] = useState<
{code: string} | undefined {code: string} | undefined
>() >()
const value = React.useMemo( const value = useMemo(
() => ({ () => ({
verifyEmailDialogControl, verifyEmailDialogControl,
verifyEmailState, verifyEmailState,
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {ScrollView, View} from 'react-native' import {ScrollView, 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'
@@ -36,7 +36,7 @@ export function Inner() {
const {data: trending, error, isLoading} = useTrendingTopics() const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length const noTopics = !isLoading && !error && !trending?.topics?.length
const onConfirmHide = React.useCallback(() => { const onConfirmHide = useCallback(() => {
ax.metric('trendingTopics:hide', {context: 'interstitial'}) ax.metric('trendingTopics:hide', {context: 'interstitial'})
setTrendingDisabled(true) setTrendingDisabled(true)
}, [ax, setTrendingDisabled]) }, [ax, setTrendingDisabled])
+4 -4
View File
@@ -1,4 +1,4 @@
import React from 'react' import {createContext, useContext, useState} from 'react'
import {type ModerationUI} from '@atproto/api' import {type ModerationUI} from '@atproto/api'
import { import {
@@ -21,10 +21,10 @@ type Context = {
} }
} }
const Context = React.createContext<Context>({} as Context) const Context = createContext<Context>({} as Context)
Context.displayName = 'HiderContext' Context.displayName = 'HiderContext'
export const useHider = () => React.useContext(Context) export const useHider = () => useContext(Context)
export function Outer({ export function Outer({
modui, modui,
@@ -38,7 +38,7 @@ export function Outer({
}>) { }>) {
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
const blur = modui?.blurs[0] const blur = modui?.blurs[0]
const [isContentVisible, setIsContentVisible] = React.useState( const [isContentVisible, setIsContentVisible] = useState(
isContentVisibleInitialState || !blur, isContentVisibleInitialState || !blur,
) )
const info = useModerationCauseDescription(blur) const info = useModerationCauseDescription(blur)
@@ -1,4 +1,4 @@
import React, {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/xrpc'
@@ -47,12 +47,12 @@ export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) {
function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) { function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [appealingLabel, setAppealingLabel] = React.useState< const [appealingLabel, setAppealingLabel] = useState<
ComAtprotoLabelDefs.Label | undefined ComAtprotoLabelDefs.Label | undefined
>(undefined) >(undefined)
const {labels} = props const {labels} = props
const isAccount = props.type === 'account' const isAccount = props.type === 'account'
const containsSelfLabel = React.useMemo( const containsSelfLabel = useMemo(
() => labels.some(l => l.src === currentAccount?.did), () => labels.some(l => l.src === currentAccount?.did),
[currentAccount?.did, labels], [currentAccount?.did, labels],
) )
@@ -224,7 +224,7 @@ function AppealForm({
const {_} = useLingui() const {_} = useLingui()
const {labeler, strings} = useLabelInfo(label) const {labeler, strings} = useLabelInfo(label)
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const [details, setDetails] = React.useState('') const [details, setDetails] = useState('')
const {subject} = useLabelSubject({label}) const {subject} = useLabelSubject({label})
const isAccountReport = 'did' in subject const isAccountReport = 'did' in subject
const agent = useAgent() const agent = useAgent()
@@ -273,7 +273,7 @@ function AppealForm({
}, },
}) })
const onSubmit = React.useCallback(() => mutate(), [mutate]) const onSubmit = useCallback(() => mutate(), [mutate])
return ( return (
<> <>
@@ -1,4 +1,11 @@
import React from 'react' import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react'
import {Pressable, type ScrollView, View} from 'react-native' import {Pressable, type ScrollView, View} from 'react-native'
import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api' import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -66,11 +73,11 @@ export function ReportDialog(
}, },
) { ) {
const ax = useAnalytics() const ax = useAnalytics()
const subject = React.useMemo( const subject = useMemo(
() => (props.subject ? parseReportSubject(props.subject) : undefined), () => (props.subject ? parseReportSubject(props.subject) : undefined),
[props.subject], [props.subject],
) )
const onClose = React.useCallback(() => { const onClose = useCallback(() => {
ax.metric('reportDialog:close', {}) ax.metric('reportDialog:close', {})
}, [ax]) }, [ax])
return ( return (
@@ -108,7 +115,7 @@ function Inner(props: ReportDialogProps) {
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog) const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const ref = React.useRef<ScrollView>(null) const ref = useRef<ScrollView>(null)
const { const {
data: allLabelers, data: allLabelers,
isLoading: isLabelerLoading, isLoading: isLabelerLoading,
@@ -118,14 +125,14 @@ function Inner(props: ReportDialogProps) {
const isLoading = useDelayedLoading(500, isLabelerLoading) const isLoading = useDelayedLoading(500, isLabelerLoading)
const copy = useCopyForSubject(props.subject) const copy = useCopyForSubject(props.subject)
const {categories, getCategory} = useReportOptions() const {categories, getCategory} = useReportOptions()
const [state, dispatch] = React.useReducer(reducer, initialState) const [state, dispatch] = useReducer(reducer, initialState)
/** /**
* Submission handling * Submission handling
*/ */
const {mutateAsync: submitReport} = useSubmitReportMutation() const {mutateAsync: submitReport} = useSubmitReportMutation()
const [isPending, setPending] = React.useState(false) const [isPending, setPending] = useState(false)
const [isSuccess, setSuccess] = React.useState(false) const [isSuccess, setSuccess] = useState(false)
// some reasons ONLY go to Bluesky // some reasons ONLY go to Bluesky
const isBskyOnlyReason = state?.selectedOption?.reason const isBskyOnlyReason = state?.selectedOption?.reason
@@ -139,7 +146,7 @@ function Inner(props: ReportDialogProps) {
/** /**
* Labelers that support this `subject` and its NSID collection * Labelers that support this `subject` and its NSID collection
*/ */
const supportedLabelers = React.useMemo(() => { const supportedLabelers = useMemo(() => {
if (!allLabelers) return [] if (!allLabelers) return []
return allLabelers return allLabelers
.filter(l => { .filter(l => {
@@ -169,7 +176,8 @@ function Inner(props: ReportDialogProps) {
if (supportedReasonTypes === undefined) return true if (supportedReasonTypes === undefined) return true
return ( return (
// supports new reason type // supports new reason type
supportedReasonTypes.includes(state.selectedOption.reason) || // supports old reason type (backwards compat) // supports old reason type (backwards compat)
supportedReasonTypes.includes(state.selectedOption.reason) ||
supportedReasonTypes.includes( supportedReasonTypes.includes(
NEW_TO_OLD_REASONS_MAP[state.selectedOption.reason], NEW_TO_OLD_REASONS_MAP[state.selectedOption.reason],
) )
@@ -194,7 +202,7 @@ function Inner(props: ReportDialogProps) {
const isAlwaysBskyLabeler = const isAlwaysBskyLabeler =
hasSingleSupportedLabeler && (isBskyOnlyReason || isBskyOnlySubject) hasSingleSupportedLabeler && (isBskyOnlyReason || isBskyOnlySubject)
const onSubmit = React.useCallback(async () => { const onSubmit = useCallback(async () => {
dispatch({type: 'clearError'}) dispatch({type: 'clearError'})
logger.info('submitting') logger.info('submitting')
@@ -587,7 +595,7 @@ function ActionOnce({
check: () => boolean check: () => boolean
callback: () => void callback: () => void
}) { }) {
React.useEffect(() => { useEffect(() => {
if (check()) { if (check()) {
callback() callback()
} }
@@ -686,7 +694,7 @@ function CategoryCard({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const gutters = useGutters(['compact']) const gutters = useGutters(['compact'])
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
onSelect?.(option) onSelect?.(option)
}, [onSelect, option]) }, [onSelect, option])
return ( return (
@@ -731,7 +739,7 @@ function OptionCard({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const gutters = useGutters(['compact']) const gutters = useGutters(['compact'])
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
onSelect?.(option) onSelect?.(option)
}, [onSelect, option]) }, [onSelect, option])
return ( return (
@@ -793,7 +801,7 @@ function LabelerCard({
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const onPress = React.useCallback(() => { const onPress = useCallback(() => {
onSelect?.(labeler) onSelect?.(labeler)
}, [onSelect, labeler]) }, [onSelect, labeler])
const title = getLabelingServiceTitle({ const title = getLabelingServiceTitle({
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useState} from 'react'
import { import {
type StyleProp, type StyleProp,
TouchableWithoutFeedback, TouchableWithoutFeedback,
@@ -39,7 +39,7 @@ export function ScreenHider({
}>) { }>) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const [override, setOverride] = React.useState(false) const [override, setOverride] = useState(false)
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const control = useModerationDetailsDialogControl() const control = useModerationDetailsDialogControl()
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import {deviceLocales} from '#/locale/deviceLocales' import {deviceLocales} from '#/locale/deviceLocales'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
@@ -277,7 +277,7 @@ export function useFormatCurrency(
) { ) {
const geolocation = useGeolocation() const geolocation = useGeolocation()
const {appLanguage} = useLanguagePrefs() const {appLanguage} = useLanguagePrefs()
return React.useMemo(() => { return useMemo(() => {
const locale = deviceLocales.at(0) const locale = deviceLocales.at(0)
const languageTag = locale?.languageTag || appLanguage || 'en-US' const languageTag = locale?.languageTag || appLanguage || 'en-US'
const countryCode = ( const countryCode = (
+5 -5
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useEffect, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import Animated, { import Animated, {
Easing, Easing,
@@ -105,14 +105,14 @@ export function CountWheel({
// animation // animation
// The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would // The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
// be unnecessary // be unnecessary
const [key, setKey] = React.useState(0) const [key, setKey] = useState(0)
const [prevCount, setPrevCount] = React.useState(likeCount) const [prevCount, setPrevCount] = useState(likeCount)
const prevIsLiked = React.useRef(isLiked) const prevIsLiked = useRef(isLiked)
const formatPostStatCount = useFormatPostStatCount() const formatPostStatCount = useFormatPostStatCount()
const formattedCount = formatPostStatCount(likeCount) const formattedCount = formatPostStatCount(likeCount)
const formattedPrevCount = formatPostStatCount(prevCount) const formattedPrevCount = formatPostStatCount(prevCount)
React.useEffect(() => { useEffect(() => {
if (isLiked === prevIsLiked.current) { if (isLiked === prevIsLiked.current) {
return return
} }
+6 -6
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useEffect, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated' import {useReducedMotion} from 'react-native-reanimated'
@@ -49,16 +49,16 @@ export function CountWheel({
const shouldAnimate = !useReducedMotion() && hasBeenToggled const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldRoll = decideShouldRoll(isLiked, likeCount) const shouldRoll = decideShouldRoll(isLiked, likeCount)
const countView = React.useRef<HTMLDivElement>(null) const countView = useRef<HTMLDivElement>(null)
const prevCountView = React.useRef<HTMLDivElement>(null) const prevCountView = useRef<HTMLDivElement>(null)
const [prevCount, setPrevCount] = React.useState(likeCount) const [prevCount, setPrevCount] = useState(likeCount)
const prevIsLiked = React.useRef(isLiked) const prevIsLiked = useRef(isLiked)
const formatPostStatCount = useFormatPostStatCount() const formatPostStatCount = useFormatPostStatCount()
const formattedCount = formatPostStatCount(likeCount) const formattedCount = formatPostStatCount(likeCount)
const formattedPrevCount = formatPostStatCount(prevCount) const formattedPrevCount = formatPostStatCount(prevCount)
React.useEffect(() => { useEffect(() => {
if (isLiked === prevIsLiked.current) { if (isLiked === prevIsLiked.current) {
return return
} }
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo, useState} from 'react'
import {type ColorValue, Dimensions, StyleSheet, View} from 'react-native' import {type ColorValue, Dimensions, StyleSheet, View} from 'react-native'
import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import Animated, { import Animated, {
@@ -50,7 +50,7 @@ export function GestureActionView({
) )
} }
const [activeAction, setActiveAction] = React.useState< const [activeAction, setActiveAction] = useState<
'leftFirst' | 'leftSecond' | 'rightFirst' | 'rightSecond' | null 'leftFirst' | 'leftSecond' | 'rightFirst' | 'rightSecond' | null
>(null) >(null)
@@ -231,7 +231,7 @@ export function GestureActionView({
} }
}) })
const leftSideInterpolation = React.useMemo(() => { const leftSideInterpolation = useMemo(() => {
return createInterpolation({ return createInterpolation({
firstColor: actions.leftFirst?.color, firstColor: actions.leftFirst?.color,
secondColor: actions.leftSecond?.color, secondColor: actions.leftSecond?.color,
@@ -241,7 +241,7 @@ export function GestureActionView({
}) })
}, [actions.leftFirst, actions.leftSecond]) }, [actions.leftFirst, actions.leftSecond])
const rightSideInterpolation = React.useMemo(() => { const rightSideInterpolation = useMemo(() => {
return createInterpolation({ return createInterpolation({
firstColor: actions.rightFirst?.color, firstColor: actions.rightFirst?.color,
secondColor: actions.rightSecond?.color, secondColor: actions.rightSecond?.color,
@@ -251,14 +251,14 @@ export function GestureActionView({
}) })
}, [actions.rightFirst, actions.rightSecond]) }, [actions.rightFirst, actions.rightSecond])
const interpolation = React.useMemo<{ const interpolation = useMemo<{
inputRange: number[] inputRange: number[]
outputRange: ColorValue[] outputRange: ColorValue[]
}>(() => { }>(() => {
if (!actions.leftFirst) { if (!actions.leftFirst) {
return rightSideInterpolation! return rightSideInterpolation
} else if (!actions.rightFirst) { } else if (!actions.rightFirst) {
return leftSideInterpolation! return leftSideInterpolation
} else { } else {
return { return {
inputRange: [ inputRange: [
@@ -1,5 +1,3 @@
import type React from 'react'
export function GestureActionView({children}: {children: React.ReactNode}) { export function GestureActionView({children}: {children: React.ReactNode}) {
return children return children
} }
+6 -6
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useEffect, useRef} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated' import {useReducedMotion} from 'react-native-reanimated'
@@ -50,13 +50,13 @@ export function AnimatedLikeIcon({
const t = useTheme() const t = useTheme()
const size = big ? 22 : 18 const size = big ? 22 : 18
const shouldAnimate = !useReducedMotion() && hasBeenToggled const shouldAnimate = !useReducedMotion() && hasBeenToggled
const prevIsLiked = React.useRef(isLiked) const prevIsLiked = useRef(isLiked)
const likeIconRef = React.useRef<HTMLDivElement>(null) const likeIconRef = useRef<HTMLDivElement>(null)
const circle1Ref = React.useRef<HTMLDivElement>(null) const circle1Ref = useRef<HTMLDivElement>(null)
const circle2Ref = React.useRef<HTMLDivElement>(null) const circle2Ref = useRef<HTMLDivElement>(null)
React.useEffect(() => { useEffect(() => {
if (prevIsLiked.current === isLiked) { if (prevIsLiked.current === isLiked) {
return return
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import * as Device from 'expo-device' import * as Device from 'expo-device'
import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics' import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
@@ -8,7 +8,7 @@ import {IS_IOS, IS_WEB} from '#/env'
export function useHaptics() { export function useHaptics() {
const isHapticsDisabled = useHapticsDisabled() const isHapticsDisabled = useHapticsDisabled()
return React.useCallback( return useCallback(
(strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => { (strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => {
if (isHapticsDisabled || IS_WEB) { if (isHapticsDisabled || IS_WEB) {
return return
+4 -4
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useEffect} from 'react'
import {Alert} from 'react-native' import {Alert} from 'react-native'
import * as Linking from 'expo-linking' import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
@@ -28,7 +28,7 @@ export function useIntentHandler() {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {tryApplyUpdate} = useApplyPullRequestOTAUpdate() const {tryApplyUpdate} = useApplyPullRequestOTAUpdate()
React.useEffect(() => { useEffect(() => {
const handleIncomingURL = async (url: string) => { const handleIncomingURL = async (url: string) => {
if (IS_IOS) { if (IS_IOS) {
// Close in-app browser if it's open (iOS only) // Close in-app browser if it's open (iOS only)
@@ -109,7 +109,7 @@ export function useComposeIntent() {
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const {hasSession} = useSession() const {hasSession} = useSession()
return React.useCallback( return useCallback(
({ ({
text, text,
imageUrisStr, imageUrisStr,
@@ -166,7 +166,7 @@ function useVerifyEmailIntent() {
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
const {verifyEmailDialogControl: control, setVerifyEmailState: setState} = const {verifyEmailDialogControl: control, setVerifyEmailState: setState} =
useIntentDialogs() useIntentDialogs()
return React.useCallback( return useCallback(
(code: string) => { (code: string) => {
closeAllActiveElements() closeAllActiveElements()
setState({ setState({
+10 -10
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useEffect, useRef, useState} from 'react'
import {Alert, AppState, type AppStateStatus} from 'react-native' import {Alert, AppState, type AppStateStatus} from 'react-native'
import {nativeBuildVersion} from 'expo-application' import {nativeBuildVersion} from 'expo-application'
import { import {
@@ -67,7 +67,7 @@ async function updateTestflight() {
export function useApplyPullRequestOTAUpdate() { export function useApplyPullRequestOTAUpdate() {
const {currentlyRunning} = useUpdates() const {currentlyRunning} = useUpdates()
const [pending, setPending] = React.useState(false) const [pending, setPending] = useState(false)
const currentChannel = currentlyRunning?.channel const currentChannel = currentlyRunning?.channel
const isCurrentlyRunningPullRequestDeployment = const isCurrentlyRunningPullRequestDeployment =
currentChannel?.startsWith('pull-request') currentChannel?.startsWith('pull-request')
@@ -124,14 +124,14 @@ export function useApplyPullRequestOTAUpdate() {
export function useOTAUpdates() { export function useOTAUpdates() {
const shouldReceiveUpdates = isEnabled && !__DEV__ const shouldReceiveUpdates = isEnabled && !__DEV__
const appState = React.useRef<AppStateStatus>('active') const appState = useRef<AppStateStatus>('active')
const lastMinimize = React.useRef(0) const lastMinimize = useRef(0)
const ranInitialCheck = React.useRef(false) const ranInitialCheck = useRef(false)
const timeout = React.useRef<NodeJS.Timeout>(undefined) const timeout = useRef<NodeJS.Timeout>(undefined)
const {currentlyRunning, isUpdatePending} = useUpdates() const {currentlyRunning, isUpdatePending} = useUpdates()
const currentChannel = currentlyRunning?.channel const currentChannel = currentlyRunning?.channel
const setCheckTimeout = React.useCallback(() => { const setCheckTimeout = useCallback(() => {
timeout.current = setTimeout(async () => { timeout.current = setTimeout(async () => {
try { try {
await setExtraParams() await setExtraParams()
@@ -153,7 +153,7 @@ export function useOTAUpdates() {
}, 10e3) }, 10e3)
}, []) }, [])
const onIsTestFlight = React.useCallback(async () => { const onIsTestFlight = useCallback(async () => {
try { try {
await updateTestflight() await updateTestflight()
} catch (err: any) { } catch (err: any) {
@@ -163,7 +163,7 @@ export function useOTAUpdates() {
} }
}, []) }, [])
React.useEffect(() => { useEffect(() => {
// We don't need to check anything if the current update is a PR update // We don't need to check anything if the current update is a PR update
if (currentChannel?.startsWith('pull-request')) { if (currentChannel?.startsWith('pull-request')) {
return return
@@ -186,7 +186,7 @@ export function useOTAUpdates() {
// After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available // After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available
// or B check for an update again. // or B check for an update again.
React.useEffect(() => { useEffect(() => {
// We also don't start this timeout if the user is on a pull request update // We also don't start this timeout if the user is on a pull request update
if (!isEnabled || currentChannel?.startsWith('pull-request')) { if (!isEnabled || currentChannel?.startsWith('pull-request')) {
return return
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import { import {
type AppBskyLabelerDefs, type AppBskyLabelerDefs,
BskyAgent, BskyAgent,
@@ -122,7 +122,7 @@ export type Subject =
export function useLabelSubject({label}: {label: ComAtprotoLabelDefs.Label}): { export function useLabelSubject({label}: {label: ComAtprotoLabelDefs.Label}): {
subject: Subject subject: Subject
} { } {
return React.useMemo(() => { return useMemo(() => {
const {cid, uri} = label const {cid, uri} = label
if (cid) { if (cid) {
return { return {
@@ -1,4 +1,4 @@
import React from 'react' import {useMemo} from 'react'
import { import {
BSKY_LABELER_DID, BSKY_LABELER_DID,
type ModerationCause, type ModerationCause,
@@ -39,7 +39,7 @@ export function useModerationCauseDescription(
const {labelDefs, labelers} = useLabelDefinitions() const {labelDefs, labelers} = useLabelDefinitions()
const globalLabelStrings = useGlobalLabelStrings() const globalLabelStrings = useGlobalLabelStrings()
return React.useMemo(() => { return useMemo(() => {
if (!cause) { if (!cause) {
return { return {
icon: Warning, icon: Warning,
-1
View File
@@ -2,7 +2,6 @@ import {createContext, useContext} from 'react'
import {i18n} from '@lingui/core' import {i18n} from '@lingui/core'
import {I18nProvider as DefaultI18nProvider} from '@lingui/react' import {I18nProvider as DefaultI18nProvider} from '@lingui/react'
import {type Locale} from 'date-fns' import {type Locale} from 'date-fns'
import type React from 'react'
import {useLocaleLanguage} from './i18n' import {useLocaleLanguage} from './i18n'
+7 -7
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -38,11 +38,11 @@ export function Deactivated() {
const hasOtherAccounts = accounts.length > 1 const hasOtherAccounts = accounts.length > 1
const {logoutCurrentAccount} = useSessionApi() const {logoutCurrentAccount} = useSessionApi()
const agent = useAgent() const agent = useAgent()
const [pending, setPending] = React.useState(false) const [pending, setPending] = useState(false)
const [error, setError] = React.useState<string | undefined>() const [error, setError] = useState<string | undefined>()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const onSelectAccount = React.useCallback( const onSelectAccount = useCallback(
(account: SessionAccount) => { (account: SessionAccount) => {
if (account.did !== currentAccount?.did) { if (account.did !== currentAccount?.did) {
onPressSwitchAccount(account, 'SwitchAccount') onPressSwitchAccount(account, 'SwitchAccount')
@@ -51,11 +51,11 @@ export function Deactivated() {
[currentAccount, onPressSwitchAccount], [currentAccount, onPressSwitchAccount],
) )
const onPressAddAccount = React.useCallback(() => { const onPressAddAccount = useCallback(() => {
setShowLoggedOut(true) setShowLoggedOut(true)
}, [setShowLoggedOut]) }, [setShowLoggedOut])
const onPressLogout = React.useCallback(() => { const onPressLogout = useCallback(() => {
if (IS_WEB) { if (IS_WEB) {
// We're switching accounts, which remounts the entire app. // We're switching accounts, which remounts the entire app.
// On mobile, this gets us Home, but on the web we also need reset the URL. // On mobile, this gets us Home, but on the web we also need reset the URL.
@@ -67,7 +67,7 @@ export function Deactivated() {
logoutCurrentAccount('Deactivated') logoutCurrentAccount('Deactivated')
}, [logoutCurrentAccount]) }, [logoutCurrentAccount])
const handleActivate = React.useCallback(async () => { const handleActivate = useCallback(async () => {
try { try {
setPending(true) setPending(true)
await agent.com.atproto.server.activateAccount() await agent.com.atproto.server.activateAccount()
@@ -1,4 +1,4 @@
import React from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
@@ -9,7 +9,7 @@ import {Text} from '#/components/Typography'
import {SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' import {SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
export function SharedPreferencesTesterScreen() { export function SharedPreferencesTesterScreen() {
const [currentTestOutput, setCurrentTestOutput] = React.useState<string>('') const [currentTestOutput, setCurrentTestOutput] = useState<string>('')
return ( return (
<Layout.Screen> <Layout.Screen>
+15 -15
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useMemo, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native' import {type ListRenderItemInfo, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api' import {type AppBskyFeedDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -47,29 +47,29 @@ export default function HashtagScreen({
const {tag, author} = route.params const {tag, author} = route.params
const {_} = useLingui() const {_} = useLingui()
const decodedTag = React.useMemo(() => { const decodedTag = useMemo(() => {
return decodeURIComponent(tag) return decodeURIComponent(tag)
}, [tag]) }, [tag])
const isCashtag = decodedTag.startsWith('$') const isCashtag = decodedTag.startsWith('$')
const fullTag = React.useMemo(() => { const fullTag = useMemo(() => {
// Cashtags already include the $ prefix, hashtags need # added // Cashtags already include the $ prefix, hashtags need # added
return isCashtag ? decodedTag : `#${decodedTag}` return isCashtag ? decodedTag : `#${decodedTag}`
}, [decodedTag, isCashtag]) }, [decodedTag, isCashtag])
const headerTitle = React.useMemo(() => { const headerTitle = useMemo(() => {
// Keep cashtags uppercase, lowercase hashtags // Keep cashtags uppercase, lowercase hashtags
const displayTag = isCashtag ? fullTag.toUpperCase() : fullTag.toLowerCase() const displayTag = isCashtag ? fullTag.toUpperCase() : fullTag.toLowerCase()
return enforceLen(displayTag, 24, true, 'middle') return enforceLen(displayTag, 24, true, 'middle')
}, [fullTag, isCashtag]) }, [fullTag, isCashtag])
const sanitizedAuthor = React.useMemo(() => { const sanitizedAuthor = useMemo(() => {
if (!author) return '' if (!author) return ''
return sanitizeHandle(author) return sanitizeHandle(author)
}, [author]) }, [author])
const onShare = React.useCallback(() => { const onShare = useCallback(() => {
const url = new URL('https://bsky.app') const url = new URL('https://bsky.app')
url.pathname = `/hashtag/${decodeURIComponent(tag)}` url.pathname = `/hashtag/${decodeURIComponent(tag)}`
if (author) { if (author) {
@@ -78,16 +78,16 @@ export default function HashtagScreen({
shareUrl(url.toString()) shareUrl(url.toString())
}, [tag, author]) }, [tag, author])
const [activeTab, setActiveTab] = React.useState(0) const [activeTab, setActiveTab] = useState(0)
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
useFocusEffect( useFocusEffect(
React.useCallback(() => { useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
const onPageSelected = React.useCallback( const onPageSelected = useCallback(
(index: number) => { (index: number) => {
setMinimalShellMode(false) setMinimalShellMode(false)
setActiveTab(index) setActiveTab(index)
@@ -95,7 +95,7 @@ export default function HashtagScreen({
[setMinimalShellMode], [setMinimalShellMode],
) )
const sections = React.useMemo(() => { const sections = useMemo(() => {
return [ return [
{ {
title: _(msg`Top`), title: _(msg`Top`),
@@ -177,14 +177,14 @@ function HashtagScreenTab({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
const [isPTR, setIsPTR] = React.useState(false) const [isPTR, setIsPTR] = useState(false)
const t = useTheme() const t = useTheme()
const {hasSession} = useSession() const {hasSession} = useSession()
const trackPostView = usePostViewTracking('Hashtag') const trackPostView = usePostViewTracking('Hashtag')
const isCashtag = fullTag.startsWith('$') const isCashtag = fullTag.startsWith('$')
const queryParam = React.useMemo(() => { const queryParam = useMemo(() => {
// Cashtags need # prefix for search: "#$BTC" or "#$BTC from:author" // Cashtags need # prefix for search: "#$BTC" or "#$BTC from:author"
const searchTag = isCashtag ? `#${fullTag}` : fullTag const searchTag = isCashtag ? `#${fullTag}` : fullTag
if (!author) return searchTag if (!author) return searchTag
@@ -203,17 +203,17 @@ function HashtagScreenTab({
hasNextPage, hasNextPage,
} = useSearchPostsQuery({query: queryParam, sort, enabled: active}) } = useSearchPostsQuery({query: queryParam, sort, enabled: active})
const posts = React.useMemo(() => { const posts = useMemo(() => {
return data?.pages.flatMap(page => page.posts) || [] return data?.pages.flatMap(page => page.posts) || []
}, [data]) }, [data])
const onRefresh = React.useCallback(async () => { const onRefresh = useCallback(async () => {
setIsPTR(true) setIsPTR(true)
await refetch() await refetch()
setIsPTR(false) setIsPTR(false)
}, [refetch]) }, [refetch])
const onEndReached = React.useCallback(() => { const onEndReached = useCallback(() => {
if (isFetchingNextPage || !hasNextPage || error) return if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage() fetchNextPage()
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {TID} from '@atproto/common-web' import {TID} from '@atproto/common-web'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -27,7 +27,7 @@ export function NoFeedsPinned({
const {isPending, mutateAsync: overwriteSavedFeeds} = const {isPending, mutateAsync: overwriteSavedFeeds} =
useOverwriteSavedFeedsMutation() useOverwriteSavedFeedsMutation()
const addRecommendedFeeds = React.useCallback(async () => { const addRecommendedFeeds = useCallback(async () => {
let skippedTimeline = false let skippedTimeline = false
let skippedDiscover = false let skippedDiscover = false
let remainingSavedFeeds = [] let remainingSavedFeeds = []
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyGraphDefs} from '@atproto/api' import {AppBskyGraphDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -45,7 +45,7 @@ export function ListHiddenScreen({
const isModList = list.purpose === AppBskyGraphDefs.MODLIST const isModList = list.purpose === AppBskyGraphDefs.MODLIST
const [isProcessing, setIsProcessing] = React.useState(false) const [isProcessing, setIsProcessing] = useState(false)
const listBlockMutation = useListBlockMutation() const listBlockMutation = useListBlockMutation()
const listMuteMutation = useListMuteMutation() const listMuteMutation = useListMuteMutation()
const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation() const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation()
+2 -2
View File
@@ -1,4 +1,4 @@
import React, {useState} from 'react' import {useCallback, useState} from 'react'
import {Keyboard, View} from 'react-native' import {Keyboard, View} from 'react-native'
import {type ComAtprotoServerDescribeServer} from '@atproto/api' import {type ComAtprotoServerDescribeServer} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -44,7 +44,7 @@ export const ForgotPasswordForm = ({
const [email, setEmail] = useState<string>('') const [email, setEmail] = useState<string>('')
const {_} = useLingui() const {_} = useLingui()
const onPressSelectService = React.useCallback(() => { const onPressSelectService = useCallback(() => {
Keyboard.dismiss() Keyboard.dismiss()
}, []) }, [])
-1
View File
@@ -1,5 +1,4 @@
import {type StyleProp, View, type ViewStyle} from 'react-native' import {type StyleProp, View, type ViewStyle} from 'react-native'
import type React from 'react'
import {atoms as a, useBreakpoints, useGutters} from '#/alf' import {atoms as a, useBreakpoints, useGutters} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
+4 -4
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
type AppBskyActorDefs, type AppBskyActorDefs,
@@ -105,7 +105,7 @@ function Inner() {
}) })
const recipient = useMaybeProfileShadow(recipientUnshadowed) const recipient = useMaybeProfileShadow(recipientUnshadowed)
const moderation = React.useMemo(() => { const moderation = useMemo(() => {
if (!recipient || !moderationOpts) return null if (!recipient || !moderationOpts) return null
return moderateProfile(recipient, moderationOpts) return moderateProfile(recipient, moderationOpts)
}, [recipient, moderationOpts]) }, [recipient, moderationOpts])
@@ -113,7 +113,7 @@ function Inner() {
// Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
// we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
// empty. So, we also check for that possible state as well and render once we can. // empty. So, we also check for that possible state as well and render once we can.
const [hasScrolled, setHasScrolled] = React.useState(false) const [hasScrolled, setHasScrolled] = useState(false)
const readyToShow = const readyToShow =
hasScrolled || hasScrolled ||
(isConvoActive(convoState) && (isConvoActive(convoState) &&
@@ -122,7 +122,7 @@ function Inner() {
// Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this // Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this
// state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added. // state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added.
React.useEffect(() => { useEffect(() => {
if (convoState.status === ConvoStatus.Initializing) { if (convoState.status === ConvoStatus.Initializing) {
setHasScrolled(false) setHasScrolled(false)
} }
@@ -1,4 +1,4 @@
import React, {useCallback, useMemo, useState} from 'react' import {memo, useCallback, useMemo, useState} from 'react'
import {type GestureResponderEvent, View} from 'react-native' import {type GestureResponderEvent, View} from 'react-native'
import { import {
AppBskyEmbedRecord, AppBskyEmbedRecord,
@@ -80,7 +80,7 @@ export let ChatListItem = ({
) )
} }
ChatListItem = React.memo(ChatListItem) ChatListItem = memo(ChatListItem)
function ChatListItemReady({ function ChatListItemReady({
convo, convo,
@@ -104,7 +104,7 @@ function ChatListItemReady({
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const profile = useProfileShadow(profileUnshadowed) const profile = useProfileShadow(profileUnshadowed)
const {mutate: markAsRead} = useMarkAsReadMutation() const {mutate: markAsRead} = useMarkAsReadMutation()
const moderation = React.useMemo( const moderation = useMemo(
() => moderateProfile(profile, moderationOpts), () => moderateProfile(profile, moderationOpts),
[profile, moderationOpts], [profile, moderationOpts],
) )
@@ -1,4 +1,4 @@
import React from 'react' import {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, 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'
@@ -43,16 +43,16 @@ export function MessageInput({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {getDraft, clearDraft} = useMessageDraft() const {getDraft, clearDraft} = useMessageDraft()
const [message, setMessage] = React.useState(getDraft) const [message, setMessage] = useState(getDraft)
const inputStyles = useSharedInputStyles() const inputStyles = useSharedInputStyles()
const isComposing = React.useRef(false) const isComposing = useRef(false)
const [isFocused, setIsFocused] = React.useState(false) const [isFocused, setIsFocused] = useState(false)
const [isHovered, setIsHovered] = React.useState(false) const [isHovered, setIsHovered] = useState(false)
const [textAreaHeight, setTextAreaHeight] = React.useState(38) const [textAreaHeight, setTextAreaHeight] = useState(38)
const textAreaRef = React.useRef<HTMLTextAreaElement>(null) const textAreaRef = useRef<HTMLTextAreaElement>(null)
const onSubmit = React.useCallback(() => { const onSubmit = useCallback(() => {
if (!hasEmbed && message.trim() === '') { if (!hasEmbed && message.trim() === '') {
return return
} }
@@ -66,7 +66,7 @@ export function MessageInput({
setEmbed(undefined) setEmbed(undefined)
}, [message, onSendMessage, _, clearDraft, hasEmbed, setEmbed]) }, [message, onSendMessage, _, clearDraft, hasEmbed, setEmbed])
const onKeyDown = React.useCallback( const onKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => { (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Don't submit the form when the Japanese or any other IME is composing // Don't submit the form when the Japanese or any other IME is composing
if (isComposing.current) return if (isComposing.current) return
@@ -98,14 +98,11 @@ export function MessageInput({
[onSubmit], [onSubmit],
) )
const onChange = React.useCallback( const onChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
(e: React.ChangeEvent<HTMLTextAreaElement>) => { setMessage(e.target.value)
setMessage(e.target.value) }, [])
},
[],
)
const onEmojiInserted = React.useCallback( const onEmojiInserted = useCallback(
(emoji: Emoji) => { (emoji: Emoji) => {
if (!textAreaRef.current) { if (!textAreaRef.current) {
return return
@@ -123,7 +120,7 @@ export function MessageInput({
}, },
[setMessage], [setMessage],
) )
React.useEffect(() => { useEffect(() => {
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
return () => { return () => {
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)

Some files were not shown because too many files have changed in this diff Show More