Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e4646e1a9 | |||
| c2ee0ee910 |
+3
-1
@@ -1,3 +1,5 @@
|
||||
const reactCompilerConfig = require('./react-compiler.config.js')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
@@ -79,7 +81,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
'react-compiler/react-compiler': 'warn',
|
||||
'react-compiler/react-compiler': ['error', reactCompilerConfig],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
const reactCompilerConfig = require('./react-compiler.config.js')
|
||||
|
||||
module.exports = function (api) {
|
||||
api.cache(true)
|
||||
const isTestEnv = process.env.NODE_ENV === 'test'
|
||||
@@ -17,7 +19,7 @@ module.exports = function (api) {
|
||||
],
|
||||
plugins: [
|
||||
'macros',
|
||||
['babel-plugin-react-compiler', {target: '18'}],
|
||||
['babel-plugin-react-compiler', reactCompilerConfig],
|
||||
[
|
||||
'module:react-native-dotenv',
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
target: '18',
|
||||
environment: {
|
||||
enableTreatRefLikeIdentifiersAsRefs: true,
|
||||
validateRefAccessDuringRender: false, // TODO: Make it `true`.
|
||||
},
|
||||
}
|
||||
@@ -56,7 +56,6 @@ type Props = {
|
||||
const AnimatedLogo = Animated.createAnimatedComponent(Logo)
|
||||
|
||||
export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
'use no memo'
|
||||
const insets = useSafeAreaInsets()
|
||||
const intro = useSharedValue(0)
|
||||
const outroLogo = useSharedValue(0)
|
||||
|
||||
@@ -29,10 +29,10 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||
const {activeDialogs} = useDialogStateContext()
|
||||
|
||||
React.useEffect(() => {
|
||||
activeDialogs.current.set(id, control)
|
||||
const map = activeDialogs.current
|
||||
map.set(id, control)
|
||||
return () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
activeDialogs.current.delete(id)
|
||||
map.delete(id)
|
||||
}
|
||||
}, [id, activeDialogs])
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ function Inner({
|
||||
|
||||
if (IS_DEV && typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
window.clearNuxDialog = (id: Nux) => {
|
||||
if (!IS_DEV || !id) return
|
||||
removeNuxs([id])
|
||||
|
||||
@@ -68,6 +68,8 @@ export function ContentHider({
|
||||
if (hasAdultContentLabel) {
|
||||
return false
|
||||
}
|
||||
// https://github.com/facebook/react/issues/31569
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
hasAdultContentLabel = true
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
/**
|
||||
* Helper hook to run persistent timers on views
|
||||
*/
|
||||
export function useTimer(time: number, handler: () => void) {
|
||||
const timer = React.useRef<undefined | NodeJS.Timeout>(undefined)
|
||||
|
||||
// function to restart the timer
|
||||
const reset = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
}
|
||||
timer.current = setTimeout(handler, time)
|
||||
}, [time, timer, handler])
|
||||
|
||||
// function to cancel the timer
|
||||
const cancel = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
}
|
||||
}, [timer])
|
||||
|
||||
// start the timer immediately
|
||||
React.useEffect(() => {
|
||||
reset()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return [reset, cancel]
|
||||
}
|
||||
@@ -9,20 +9,24 @@ if ('scrollRestoration' in history) {
|
||||
|
||||
function createInitialScrollState() {
|
||||
return {
|
||||
scrollYs: new Map(),
|
||||
focusedKey: null as string | null,
|
||||
// Not used for rendering.
|
||||
// Treat it as a ref so that we can mutate it without upsetting the compiler.
|
||||
current: {
|
||||
scrollYs: new Map(),
|
||||
focusedKey: null as string | null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function useWebScrollRestoration() {
|
||||
const [state] = useState(createInitialScrollState)
|
||||
const [ref] = useState(createInitialScrollState)
|
||||
const navigation = useNavigation()
|
||||
|
||||
useEffect(() => {
|
||||
function onDispatch() {
|
||||
if (state.focusedKey) {
|
||||
if (ref.current.focusedKey) {
|
||||
// Remember where we were for later.
|
||||
state.scrollYs.set(state.focusedKey, window.scrollY)
|
||||
ref.current.scrollYs.set(ref.current.focusedKey, window.scrollY)
|
||||
// TODO: Strictly speaking, this is a leak. We never clean up.
|
||||
// This is because I'm not sure when it's appropriate to clean it up.
|
||||
// It doesn't seem like popstate is enough because it can still Forward-Back again.
|
||||
@@ -36,17 +40,17 @@ export function useWebScrollRestoration() {
|
||||
return () => {
|
||||
navigation.removeListener('__unsafe_action__' as any, onDispatch)
|
||||
}
|
||||
}, [state, navigation])
|
||||
}, [ref, navigation])
|
||||
|
||||
const screenListeners = useMemo(
|
||||
() => ({
|
||||
focus(e: EventArg<'focus', boolean | undefined, unknown>) {
|
||||
const scrollY = state.scrollYs.get(e.target) ?? 0
|
||||
const scrollY = ref.current.scrollYs.get(e.target) ?? 0
|
||||
window.scrollTo(0, scrollY)
|
||||
state.focusedKey = e.target ?? null
|
||||
ref.current.focusedKey = e.target ?? null
|
||||
},
|
||||
}),
|
||||
[state],
|
||||
[ref],
|
||||
)
|
||||
return screenListeners
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
React.useEffect(() => {
|
||||
if (state.pendingSubmit) {
|
||||
if (!state.pendingSubmit.mutableProcessed) {
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
state.pendingSubmit.mutableProcessed = true
|
||||
submit(state, dispatch)
|
||||
}
|
||||
|
||||
@@ -173,6 +173,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
return wasActive
|
||||
})
|
||||
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
unstable__openModal = openModal
|
||||
unstable__closeModal = closeModal
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ const RQKEY_ROOT = 'actor-autocomplete'
|
||||
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
|
||||
|
||||
export function useActorAutocompleteQuery(
|
||||
prefix: string,
|
||||
rawPrefix: string,
|
||||
maintainData?: boolean,
|
||||
limit?: number,
|
||||
) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const agent = useAgent()
|
||||
|
||||
prefix = prefix.toLowerCase().trim()
|
||||
let prefix = rawPrefix.toLowerCase().trim()
|
||||
if (prefix.endsWith('.')) {
|
||||
// Going from "foo" to "foo." should not clear matches.
|
||||
prefix = prefix.slice(0, -1)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {resetBadgeCount} from '#/lib/notifications/notifications'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
@@ -50,7 +51,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
const [numUnread, setNumUnread] = React.useState('')
|
||||
|
||||
const checkUnreadRef = React.useRef<ApiContext['checkUnread'] | null>(null)
|
||||
const cacheRef = React.useRef<CachedFeedPage>({
|
||||
usableInFeed: false,
|
||||
syncedAt: new Date(),
|
||||
@@ -70,19 +70,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// periodic sync
|
||||
React.useEffect(() => {
|
||||
if (!hasSession || !checkUnreadRef.current) {
|
||||
return
|
||||
}
|
||||
checkUnreadRef.current() // fire on init
|
||||
const interval = setInterval(
|
||||
() => checkUnreadRef.current?.({isPoll: true}),
|
||||
UPDATE_INTERVAL,
|
||||
)
|
||||
return () => clearInterval(interval)
|
||||
}, [hasSession])
|
||||
|
||||
// listen for broadcasts
|
||||
React.useEffect(() => {
|
||||
const listener = ({data}: MessageEvent) => {
|
||||
@@ -190,7 +177,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
},
|
||||
}
|
||||
}, [setNumUnread, queryClient, moderationOpts, agent])
|
||||
checkUnreadRef.current = api.checkUnread
|
||||
|
||||
const checkUnread = useNonReactiveCallback(api.checkUnread)
|
||||
|
||||
// periodic sync
|
||||
React.useEffect(() => {
|
||||
if (!hasSession) {
|
||||
return
|
||||
}
|
||||
checkUnread() // fire on init
|
||||
const interval = setInterval(
|
||||
() => checkUnread({isPoll: true}),
|
||||
UPDATE_INTERVAL,
|
||||
)
|
||||
return () => clearInterval(interval)
|
||||
}, [hasSession, checkUnread])
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={numUnread}>
|
||||
|
||||
@@ -187,6 +187,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.needsPersist) {
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
state.needsPersist = false
|
||||
const persistedData = {
|
||||
accounts: state.accounts,
|
||||
|
||||
@@ -61,14 +61,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const {mutateAsync, variables, isPending} =
|
||||
useSetActiveProgressGuideMutation()
|
||||
|
||||
const activeProgressGuide = (
|
||||
let activeProgressGuide = (
|
||||
isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
|
||||
) as ProgressGuide
|
||||
|
||||
// ensure the unspecced attributes have the correct types
|
||||
if (activeProgressGuide?.guide === 'like-10-and-follow-7') {
|
||||
activeProgressGuide.numLikes = Number(activeProgressGuide.numLikes) || 0
|
||||
activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0
|
||||
activeProgressGuide = {
|
||||
...activeProgressGuide,
|
||||
numLikes: Number(activeProgressGuide.numLikes) || 0,
|
||||
numFollows: Number(activeProgressGuide.numFollows) || 0,
|
||||
}
|
||||
}
|
||||
|
||||
const [localGuideState, setLocalGuideState] =
|
||||
|
||||
@@ -303,6 +303,8 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
React.useLayoutEffect(() => {
|
||||
let node = editor?.view.dom
|
||||
if (node) {
|
||||
// FIXME
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
node.style.minHeight = webForceMinHeight ? '140px' : ''
|
||||
}
|
||||
}, [editor, webForceMinHeight])
|
||||
|
||||
@@ -87,7 +87,6 @@ export default function ImageViewRoot({
|
||||
onPressSave: (uri: string) => void
|
||||
onPressShare: (uri: string) => void
|
||||
}) {
|
||||
'use no memo'
|
||||
const ref = useAnimatedRef<View>()
|
||||
const [activeLightbox, setActiveLightbox] = useState(nextLightbox)
|
||||
const openProgress = useSharedValue(0)
|
||||
|
||||
+67
-56
@@ -129,15 +129,15 @@ export const Link = memo(function Link({
|
||||
)
|
||||
}
|
||||
|
||||
let dataSet = props.dataSet
|
||||
if (anchorNoUnderline) {
|
||||
// @ts-ignore web only -prf
|
||||
props.dataSet = props.dataSet || {}
|
||||
// @ts-ignore web only -prf
|
||||
props.dataSet.noUnderline = 1
|
||||
dataSet = {...dataSet, noUnderline: 1}
|
||||
}
|
||||
|
||||
if (title && !props.accessibilityLabel) {
|
||||
props.accessibilityLabel = title
|
||||
let accessibilityLabel = props.accessibilityLabel
|
||||
if (title && !accessibilityLabel) {
|
||||
accessibilityLabel = title
|
||||
}
|
||||
|
||||
const Com = props.hoverStyle ? PressableWithHover : Pressable
|
||||
@@ -150,7 +150,11 @@ export const Link = memo(function Link({
|
||||
accessibilityRole="link"
|
||||
// @ts-ignore web only -prf
|
||||
href={anchorHref}
|
||||
{...props}>
|
||||
{...props}
|
||||
// @ts-ignore web only
|
||||
dataSet={dataSet}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityHint={props.accessibilityHint}>
|
||||
{children ? children : <Text>{title || 'link'}</Text>}
|
||||
</Com>
|
||||
)
|
||||
@@ -164,7 +168,7 @@ export const TextLink = memo(function TextLink({
|
||||
text,
|
||||
numberOfLines,
|
||||
lineHeight,
|
||||
dataSet,
|
||||
dataSet: rawDataSet,
|
||||
title,
|
||||
onPress,
|
||||
onBeforePress,
|
||||
@@ -187,7 +191,7 @@ export const TextLink = memo(function TextLink({
|
||||
anchorNoUnderline?: boolean
|
||||
onBeforePress?: () => void
|
||||
} & TextProps) {
|
||||
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||
let {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||
const navigation = useNavigationDeduped()
|
||||
const {openModal, closeModal} = useModalControls()
|
||||
const openLink = useOpenLink()
|
||||
@@ -196,61 +200,68 @@ export const TextLink = memo(function TextLink({
|
||||
console.error('Unable to detect mismatching label')
|
||||
}
|
||||
|
||||
let dataSet = rawDataSet
|
||||
if (anchorNoUnderline) {
|
||||
dataSet = dataSet ?? {}
|
||||
dataSet.noUnderline = 1
|
||||
dataSet = {
|
||||
...dataSet,
|
||||
noUnderline: 1,
|
||||
}
|
||||
}
|
||||
|
||||
props.onPress = React.useCallback(
|
||||
(e?: Event) => {
|
||||
const requiresWarning =
|
||||
!disableMismatchWarning &&
|
||||
linkRequiresWarning(href, typeof text === 'string' ? text : '')
|
||||
if (requiresWarning) {
|
||||
e?.preventDefault?.()
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: typeof text === 'string' ? text : '',
|
||||
href,
|
||||
})
|
||||
}
|
||||
if (
|
||||
isWeb &&
|
||||
href !== '#' &&
|
||||
e != null &&
|
||||
isModifiedEvent(e as React.MouseEvent)
|
||||
) {
|
||||
// Let the browser handle opening in new tab etc.
|
||||
return
|
||||
}
|
||||
onBeforePress?.()
|
||||
if (onPress) {
|
||||
e?.preventDefault?.()
|
||||
// @ts-ignore function signature differs by platform -prf
|
||||
return onPress()
|
||||
}
|
||||
return onPressInner(
|
||||
props = {
|
||||
...props,
|
||||
onPress: React.useCallback(
|
||||
(e?: Event) => {
|
||||
const requiresWarning =
|
||||
!disableMismatchWarning &&
|
||||
linkRequiresWarning(href, typeof text === 'string' ? text : '')
|
||||
if (requiresWarning) {
|
||||
e?.preventDefault?.()
|
||||
openModal({
|
||||
name: 'link-warning',
|
||||
text: typeof text === 'string' ? text : '',
|
||||
href,
|
||||
})
|
||||
}
|
||||
if (
|
||||
isWeb &&
|
||||
href !== '#' &&
|
||||
e != null &&
|
||||
isModifiedEvent(e as React.MouseEvent)
|
||||
) {
|
||||
// Let the browser handle opening in new tab etc.
|
||||
return
|
||||
}
|
||||
onBeforePress?.()
|
||||
if (onPress) {
|
||||
e?.preventDefault?.()
|
||||
// @ts-ignore function signature differs by platform -prf
|
||||
return onPress()
|
||||
}
|
||||
return onPressInner(
|
||||
closeModal,
|
||||
navigation,
|
||||
sanitizeUrl(href),
|
||||
navigationAction,
|
||||
openLink,
|
||||
e,
|
||||
)
|
||||
},
|
||||
[
|
||||
onBeforePress,
|
||||
onPress,
|
||||
closeModal,
|
||||
openModal,
|
||||
navigation,
|
||||
sanitizeUrl(href),
|
||||
href,
|
||||
text,
|
||||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
openLink,
|
||||
e,
|
||||
)
|
||||
},
|
||||
[
|
||||
onBeforePress,
|
||||
onPress,
|
||||
closeModal,
|
||||
openModal,
|
||||
navigation,
|
||||
href,
|
||||
text,
|
||||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
openLink,
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
const hrefAttrs = useMemo(() => {
|
||||
const isExternal = isExternalUrl(href)
|
||||
if (isExternal) {
|
||||
|
||||
@@ -96,6 +96,8 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
paddingTop: Math.abs(contentOffset.y),
|
||||
})
|
||||
}
|
||||
// @ts-ignore web only -prf
|
||||
let dataSet = props.dataSet
|
||||
if (desktopFixedHeight) {
|
||||
if (typeof desktopFixedHeight === 'number') {
|
||||
// @ts-ignore Web only -prf
|
||||
@@ -114,10 +116,10 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
// around this, we set data-stable-gutters which can then be
|
||||
// styled in our external CSS.
|
||||
// -prf
|
||||
// @ts-ignore web only -prf
|
||||
props.dataSet = props.dataSet || {}
|
||||
// @ts-ignore web only -prf
|
||||
props.dataSet.stableGutters = '1'
|
||||
dataSet = {
|
||||
...dataSet,
|
||||
stableGutters: '1',
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -131,6 +133,8 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
style={style}
|
||||
contentOffset={contentOffset}
|
||||
{...props}
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={dataSet}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user