diff --git a/src/ageAssurance/components/NoAccessScreen.tsx b/src/ageAssurance/components/NoAccessScreen.tsx index 895e57d077..a1014cc0b5 100644 --- a/src/ageAssurance/components/NoAccessScreen.tsx +++ b/src/ageAssurance/components/NoAccessScreen.tsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect} from 'react' +import {useCallback, useEffect, useEffectEvent} from 'react' import {ScrollView, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Trans, useLingui} from '@lingui/react/macro' @@ -76,7 +76,7 @@ export function NoAccessScreen() { const geolocationString = createGeolocationString(geolocation, i18n.locale) const isUsingGPS = !!geolocation.deviceGeolocation?.countryCode && IS_NATIVE - useEffect(() => { + const onShown = useEffectEvent(() => { // just counting overall hits here ax.metric(`blockedGeoOverlay:shown`, {}) ax.metric(`ageAssurance:noAccessScreen:shown`, { @@ -85,8 +85,10 @@ export function NoAccessScreen() { hasDeclaredAge, canUpdateBirthday, }) - // TODO This can be cleaned up with useEffectEvent once we're on 19.2 - // eslint-disable-next-line react-hooks/exhaustive-deps + }) + + useEffect(() => { + onShown() }, []) const onPressLogout = useCallback(() => { diff --git a/src/alf/util/useGutters.ts b/src/alf/util/useGutters.ts index f7dcef51bd..07ee11e658 100644 --- a/src/alf/util/useGutters.ts +++ b/src/alf/util/useGutters.ts @@ -1,5 +1,3 @@ -import {useMemo} from 'react' - import {type Breakpoint, useBreakpoints} from '#/alf/breakpoints' import * as tokens from '#/alf/tokens' @@ -44,23 +42,26 @@ export function useGutters([top, right, bottom, left]: [ Gutter, Gutter, ]): Gutters -export function useGutters([top, right, bottom, left]: Gutter[]) { +export function useGutters(gutter: Gutter[]) { const {activeBreakpoint} = useBreakpoints() + /* + * Destructured in the body rather than the parameter list: these are + * reassigned below to fill in the CSS-shorthand forms, and React Compiler + * cannot lower a reassigned destructured parameter. + */ + let [top, right, bottom, left] = gutter if (right === undefined) { right = bottom = left = top } else if (bottom === undefined) { bottom = top left = right } - return useMemo(() => { - return { - paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], - paddingRight: - right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'], - paddingBottom: - bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'], - paddingLeft: - left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'], - } - }, [activeBreakpoint, top, right, bottom, left]) + return { + paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], + paddingRight: + right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'], + paddingBottom: + bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'], + paddingLeft: left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'], + } } diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 5c36af6ae5..93f741432b 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -42,7 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] { useEffect(() => { activeDialogs.current.set(id, control) return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps activeDialogs.current.delete(id) } }, [id, activeDialogs]) diff --git a/src/components/EmojiPicker/preload.web.ts b/src/components/EmojiPicker/preload.web.ts index 4456153b71..13f52b996b 100644 --- a/src/components/EmojiPicker/preload.web.ts +++ b/src/components/EmojiPicker/preload.web.ts @@ -1,4 +1,3 @@ -import {useCallback} from 'react' import {init} from 'emoji-mart' /** @@ -16,15 +15,16 @@ let loadRequested = false * * @see {@link https://github.com/missive/emoji-mart/blob/16978d04a766eec6455e2e8bb21cd8dc0b3c7436/README.md?plain=1#L194 | emoji-mart preloading docs} */ -export function useWebPreloadEmoji({immediate}: {immediate?: boolean} = {}) { - const preload = useCallback(async () => { - if (loadRequested) return - loadRequested = true - try { - const data = (await import('@emoji-mart/data')).default - init({data}) - } catch (e) {} - }, []) - if (immediate) preload() - return preload +async function loadEmojiData() { + if (loadRequested) return + loadRequested = true + try { + const data = (await import('@emoji-mart/data')).default + init({data}) + } catch (e) {} +} + +export function useWebPreloadEmoji({immediate}: {immediate?: boolean} = {}) { + if (immediate) loadEmojiData() + return loadEmojiData } diff --git a/src/components/InterestTabs.tsx b/src/components/InterestTabs.tsx index 5a99a36992..ca4d035393 100644 --- a/src/components/InterestTabs.tsx +++ b/src/components/InterestTabs.tsx @@ -54,6 +54,16 @@ export function InterestTabs({ const pendingTabOffsets = useRef<{x: number; width: number}[]>([]) const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([]) + function scrollIntoViewIfNeeded(index: number) { + const btnLayout = tabOffsets[index] + if (!btnLayout) return + listRef.current?.scrollTo({ + // centered + x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2), + animated: true, + }) + } + const onInitialLayout = useNonReactiveCallback(() => { const index = interests.indexOf(selectedInterest) scrollIntoViewIfNeeded(index) @@ -65,16 +75,6 @@ export function InterestTabs({ } }, [tabOffsets, onInitialLayout]) - function scrollIntoViewIfNeeded(index: number) { - const btnLayout = tabOffsets[index] - if (!btnLayout) return - listRef.current?.scrollTo({ - // centered - x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2), - animated: true, - }) - } - function handleSelectTab(index: number) { const tab = interests[index] onSelectTab(tab) diff --git a/src/components/LanguageSelect.tsx b/src/components/LanguageSelect.tsx index 6bcaebe539..0dd600feb3 100644 --- a/src/components/LanguageSelect.tsx +++ b/src/components/LanguageSelect.tsx @@ -5,13 +5,15 @@ import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {APP_LANGUAGES} from '#/locale/languages' import * as Select from '#/components/Select' +const DEFAULT_ITEMS = APP_LANGUAGES.map(l => ({ + label: l.name, + value: l.code2, +})) + export function LanguageSelect({ value, onChange, - items = APP_LANGUAGES.map(l => ({ - label: l.name, - value: l.code2, - })), + items = DEFAULT_ITEMS, label, disabledBlueskySupportedLanguageSanitization = false, }: { diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx index 72d39875ab..6227eeb588 100644 --- a/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx +++ b/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx @@ -79,6 +79,11 @@ const ImageItem = ({ // Keep track of when we're entering or leaving scaled rendering. // Note: DO NOT move any logic reading animated values outside this function. + function handleZoom(nextIsScaled: boolean) { + setIsScaled(nextIsScaled) + onZoom(nextIsScaled) + } + useAnimatedReaction( () => { if (pinchScale.get() !== 1) { @@ -100,11 +105,6 @@ const ImageItem = ({ }, ) - function handleZoom(nextIsScaled: boolean) { - setIsScaled(nextIsScaled) - onZoom(nextIsScaled) - } - // On Android, stock apps prevent going "out of bounds" on pan or pinch. You should "bump" into edges. // If the user tried to pan too hard, this function will provide the negative panning to stay in bounds. function getExtraTranslationToStayInBounds( diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index dc394b8a6e..a67495189a 100644 --- a/src/components/Post/Embed/VideoEmbed/index.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -121,10 +121,12 @@ function InnerWrapper({embed}: Props) { setIsActive={active => { setIsActive(active) if (active) { - telemetryRef.current ??= createPlaybackTelemetry({ - surface: 'feed', - presentation: embed.presentation === 'gif' ? 'gif' : 'video', - }) + if (telemetryRef.current == null) { + telemetryRef.current = createPlaybackTelemetry({ + surface: 'feed', + presentation: embed.presentation === 'gif' ? 'gif' : 'video', + }) + } telemetryRef.current.activated() } else { telemetryRef.current?.deactivated() diff --git a/src/components/ProfileBadges.tsx b/src/components/ProfileBadges.tsx index cb877179ac..ff8a3edaba 100644 --- a/src/components/ProfileBadges.tsx +++ b/src/components/ProfileBadges.tsx @@ -86,17 +86,20 @@ export function ProfileBadges({ const gap = isOnTheSmallSide ? a.gap_2xs : a.gap_xs const padding = gap.gap / 2 + const hitSlops = [] let visibleBadgeIndex = 0 - const hitSlops = badgeVisibility.map(isVisible => { - if (!isVisible) return HITSLOP_20 - + for (const isVisible of badgeVisibility) { + if (!isVisible) { + hitSlops.push(HITSLOP_20) + continue + } const index = visibleBadgeIndex++ - return { + hitSlops.push({ ...HITSLOP_20, left: index === 0 ? HITSLOP_20.left : padding, right: index === badgeCount - 1 ? HITSLOP_20.right : padding, - } - }) + }) + } return ( diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index af5849346b..f8b01b5afa 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -122,9 +122,12 @@ export function Trigger({children, hitSlop, label}: TriggerProps) { } } +const defaultValueText: NonNullable = value => + value.label + export function ValueText({ placeholder, - children = value => value.label, + children = defaultValueText, style, }: ValueProps) { const [value] = useContext(ValueTextContext) diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx index 2f99ab0651..9c3def0adb 100644 --- a/src/components/StarterPack/StarterPackCard.tsx +++ b/src/components/StarterPack/StarterPackCard.tsx @@ -62,6 +62,7 @@ export function Card({ const {_} = useLingui() const t = useTheme() const {currentAccount} = useSession() + const isOwnStarterPack = creator?.did === currentAccount?.did if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null @@ -82,7 +83,7 @@ export function Card({ emoji style={[a.leading_snug, t.atoms.text_contrast_medium]} numberOfLines={1}> - {creator?.did === currentAccount?.did + {isOwnStarterPack ? _(msg`Starter pack by you`) : _(msg`Starter pack by ${sanitizeHandle(creator.handle, '@')}`)} diff --git a/src/components/WelcomeModal.tsx b/src/components/WelcomeModal.tsx index d603a0e223..3e76a914e1 100644 --- a/src/components/WelcomeModal.tsx +++ b/src/components/WelcomeModal.tsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react' +import {useEffect, useEffectEvent, useState} from 'react' import {Pressable, View} from 'react-native' import {ImageBackground} from 'expo-image' import {Trans, useLingui} from '@lingui/react/macro' @@ -35,11 +35,14 @@ export function WelcomeModal({control}: WelcomeModalProps) { }, 150) } + const onPresented = useEffectEvent(() => { + ax.metric('welcomeModal:presented', {}) + }) + useEffect(() => { if (control.isOpen) { - ax.metric('welcomeModal:presented', {}) + onPresented() } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [control.isOpen]) const onPressCreateAccount = () => { diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index ac64ed331f..465f3fb77f 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -294,6 +294,37 @@ export function PostInteractionSettingsDialogControlledInner( ) } +/** + * Lives outside the component because the early `return []` inside a `useMemo` + * is memoization React Compiler cannot preserve. + */ +function getToggleGroupValues(settings: ThreadgateAllowUISetting[]): string[] { + const values: string[] = [] + for (const setting of settings) { + switch (setting.type) { + case 'everybody': + case 'nobody': + // no granularity, early return with nothing + return [] + case 'followers': + values.push('followers') + break + case 'following': + values.push('following') + break + case 'mention': + values.push('mention') + break + case 'list': + values.push(`list:${setting.list}`) + break + default: + break + } + } + return values +} + export function PostInteractionSettingsForm({ canSave = true, onSave, @@ -348,32 +379,7 @@ export function PostInteractionSettingsForm({ v => v.type === 'list', ).length - const toggleGroupValues = useMemo(() => { - const values: string[] = [] - for (const setting of threadgateAllowUISettings) { - switch (setting.type) { - case 'everybody': - case 'nobody': - // no granularity, early return with nothing - return [] - case 'followers': - values.push('followers') - break - case 'following': - values.push('following') - break - case 'mention': - values.push('mention') - break - case 'list': - values.push(`list:${setting.list}`) - break - default: - break - } - } - return values - }, [threadgateAllowUISettings]) + const toggleGroupValues = getToggleGroupValues(threadgateAllowUISettings) const toggleGroupOnChange = (values: string[]) => { const settings: ThreadgateAllowUISetting[] = [] diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index 7fa0426577..98ede654c8 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -92,7 +92,7 @@ export function ReportDialog( openCount: number videoTimestampSeconds?: number }>({openCount: 0}) - const onOpen = useCallback(() => { + const onOpen = () => { const seconds = subject?.type === 'post' && subject.attributes.video ? reportDialogMetadata?.current.videoTimestampSeconds @@ -105,7 +105,7 @@ export function ReportDialog( videoTimestampSeconds: seconds !== undefined && seconds >= 1 ? Math.floor(seconds) : undefined, })) - }, [reportDialogMetadata, subject]) + } const propsOnClose = props.onClose const onClose = useCallback(() => { ax.metric('reportDialog:close', {}) diff --git a/src/features/inviteFriends/components/FollowersPromoBanner.tsx b/src/features/inviteFriends/components/FollowersPromoBanner.tsx index 8701181599..05f2382510 100644 --- a/src/features/inviteFriends/components/FollowersPromoBanner.tsx +++ b/src/features/inviteFriends/components/FollowersPromoBanner.tsx @@ -1,4 +1,4 @@ -import {useEffect} from 'react' +import {useEffect, useEffectEvent} from 'react' import {Pressable, View} from 'react-native' import {Image} from 'expo-image' import {useLingui} from '@lingui/react/macro' @@ -19,11 +19,14 @@ export function FollowersPromoBanner({ const t = useTheme() const ax = useAnalytics() - useEffect(() => { + const onSeen = useEffectEvent(() => { ax.metric('invite:followersPromo:seen', {}) + }) + + useEffect(() => { // Fire once per mount - parent unmounts the banner when followers > 0 or // when dismissed, so each mount is a distinct impression. - // eslint-disable-next-line react-hooks/exhaustive-deps + onSeen() }, []) const handlePress = () => { diff --git a/src/lib/hooks/useTLDs.ts b/src/lib/hooks/useTLDs.ts index 8ed872835a..ae00df4314 100644 --- a/src/lib/hooks/useTLDs.ts +++ b/src/lib/hooks/useTLDs.ts @@ -1,14 +1,20 @@ import {useEffect, useState} from 'react' import type tldts from 'tldts' +/** + * Module scope because React Compiler cannot lower an `import()` expression + * inside a component or hook body. + */ +function loadTLDs(): Promise { + // @ts-expect-error - valid path + return import('tldts/dist/index.cjs.min.js') +} + export function useTLDs() { const [tlds, setTlds] = useState() useEffect(() => { - // @ts-expect-error - valid path - import('tldts/dist/index.cjs.min.js').then(tlds => { - setTlds(tlds) - }) + loadTLDs().then(setTlds) }, []) return tlds diff --git a/src/lib/translation/index.tsx b/src/lib/translation/index.tsx index 1f1f8d4e58..d3b25bcac2 100644 --- a/src/lib/translation/index.tsx +++ b/src/lib/translation/index.tsx @@ -29,6 +29,16 @@ const E_SAME_AS_SOURCE_LANGUAGE = const E_EMPTY_RESULT = 'Translation result is empty.' const E_INVALID_SOURCE_LANGUAGE = 'Invalid source language' +/** + * Returns a copy of `obj` without `key`. A computed property in a destructuring + * pattern is syntax React Compiler cannot lower, so this stays out of the hook. + */ +function omitKey>(obj: T, key: string): T { + const next = {...obj} + delete next[key] + return next +} + /** * Attempts on-device translation via @bsky.app/expo-translate-text. * Uses a lazy import to avoid crashing if the native module isn't linked into @@ -199,8 +209,7 @@ export function Provider({children}: React.PropsWithChildren) { setRefCounts(prev => { const newCount = (prev[key] ?? 1) - 1 if (newCount <= 0) { - const {[key]: _, ...rest} = prev - return rest + return omitKey(prev, key) } return {...prev, [key]: newCount} }) diff --git a/src/lib/useGetEmojis/index.ts b/src/lib/useGetEmojis/index.ts index 4c70c724ff..f9622ebf26 100644 --- a/src/lib/useGetEmojis/index.ts +++ b/src/lib/useGetEmojis/index.ts @@ -6,7 +6,9 @@ let emojis: Awaited> | null = null export function useGetEmojis() { return useCallback(async () => { - emojis ??= await getEmojis() + if (emojis == null) { + emojis = await getEmojis() + } return emojis }, []) } diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx index 246100c13d..5aa9237cd8 100644 --- a/src/screens/Search/components/StarterPackCard.tsx +++ b/src/screens/Search/components/StarterPackCard.tsx @@ -34,6 +34,7 @@ export function StarterPackCard({ const {gtPhone} = useBreakpoints() const link = useStarterPackLink({view}) const record = view.record + const isOwnStarterPack = view.creator?.did === currentAccount?.did if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null @@ -99,7 +100,7 @@ export function StarterPackCard({ t.atoms.text_contrast_medium, ]} numberOfLines={1}> - {view.creator?.did === currentAccount?.did + {isOwnStarterPack ? _(msg`By you`) : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)} diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index 0232acfffe..46e1b43377 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -46,6 +46,19 @@ function sanitizeDate(date: Date): Date { return date } +/* + * Module scope because React Compiler cannot lower an `import()` expression + * inside a component or hook body. + */ +function loadTLDs(): Promise { + // @ts-expect-error - valid path + return import('tldts/dist/index.cjs.min.js') +} + +function preloadViewShot() { + return import('react-native-view-shot') +} + export function StepInfo({ onPressBack, isServerError, @@ -90,12 +103,11 @@ export function StepInfo({ const tldtsRef = useRef(undefined) useEffect(() => { - // @ts-expect-error - valid path - void import('tldts/dist/index.cjs.min.js').then(tldts => { + void loadTLDs().then(tldts => { tldtsRef.current = tldts }) // This will get used in the avatar creator a few steps later, so lets preload it now - void import('react-native-view-shot') + void preloadViewShot() }, []) const onNextPress = () => { diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index e6028edd18..63cf084eb8 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -382,11 +382,10 @@ function Feed() { } } - if ( - updatedSources[0]?.source !== currentSources[0]?.source || - updatedSources[1]?.source !== currentSources[1]?.source || - updatedSources[2]?.source !== currentSources[2]?.source - ) { + const sourcesChanged = [0, 1, 2].some( + i => updatedSources[i]?.source !== currentSources[i]?.source, + ) + if (sourcesChanged) { setCurrentSources(updatedSources) } }, @@ -659,10 +658,12 @@ function usePlaybackTelemetry({ useEffect(() => { if (!active) return - telemetryRef.current ??= createPlaybackTelemetry({ - surface: 'immersiveFeed', - presentation: 'video', - }) + if (telemetryRef.current == null) { + telemetryRef.current = createPlaybackTelemetry({ + surface: 'immersiveFeed', + presentation: 'video', + }) + } const telemetry = telemetryRef.current const preloaded = player.status === 'readyToPlay' telemetry.activated({preloaded}) diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index b84b5fabdd..982204d990 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -65,8 +65,8 @@ export function useActorAutocompleteFn() { const client = useAppviewClient() return useCallback( - async ({query, limit = 8}: {query: string; limit?: number}) => { - query = query.toLowerCase() + async ({query: rawQuery, limit = 8}: {query: string; limit?: number}) => { + const query = rawQuery.toLowerCase() let res if (query) { try { diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index 7dcb76e706..41b8dd65d9 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -55,14 +55,15 @@ export function useStarterPackQuery({ return useQuery({ queryKey: RQKEY(uri ? {uri} : {did, rkey}), queryFn: async () => { - if (!uri) { - uri = `at://${did}/app.bsky.graph.starterpack/${rkey}` - } else if (uri && !uri.startsWith('at://')) { - uri = httpStarterPackUriToAtUri(uri) as string + let atUri = uri + if (!atUri) { + atUri = `at://${did}/app.bsky.graph.starterpack/${rkey}` + } else if (!atUri.startsWith('at://')) { + atUri = httpStarterPackUriToAtUri(atUri) as string } const res = await client.call(app.bsky.graph.getStarterPack, { - starterPack: uri as AtUriString, + starterPack: atUri as AtUriString, }) return res.starterPack }, diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 6140fc046e..21185327a0 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -186,12 +186,10 @@ export function TextInput({ }, [t, fonts]) const textDecorated = useMemo(() => { - let i = 0 - - return Array.from(richtext.segments()).map(segment => { + return Array.from(richtext.segments()).map((segment, i) => { return ( ('idle') - useImperativeHandle(ref, () => ({ - setPage: (index: number) => { - onTabBarSelect(index) - }, - })) - const onTabBarSelect = useCallback( (index: number) => { const scrollY = window.scrollY @@ -104,6 +98,12 @@ export function Pager({ [selectedPage, setSelectedPage, onPageSelected, onTabPressed], ) + useImperativeHandle(ref, () => ({ + setPage: (index: number) => { + onTabBarSelect(index) + }, + })) + return ( {renderTabBar({ diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 1ee8ff78ed..9019fd9340 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -541,14 +541,12 @@ function useWebListTelemetry({ return } - let taskCount = 0 - let totalDurationMs = 0 - let maxDurationMs = 0 + const stats = {taskCount: 0, totalDurationMs: 0, maxDurationMs: 0} const observer = new PerformanceObserver(list => { for (const entry of list.getEntries()) { - taskCount++ - totalDurationMs += entry.duration - maxDurationMs = Math.max(maxDurationMs, entry.duration) + stats.taskCount++ + stats.totalDurationMs += entry.duration + stats.maxDurationMs = Math.max(stats.maxDurationMs, entry.duration) } }) @@ -560,18 +558,18 @@ function useWebListTelemetry({ } const report = () => { - if (taskCount === 0) return + if (stats.taskCount === 0) return ax.metric('web:list:longTasks', { itemCount: itemCountRef.current, - taskCount, - totalDurationMs: Math.round(totalDurationMs), - maxDurationMs: Math.round(maxDurationMs), + taskCount: stats.taskCount, + totalDurationMs: Math.round(stats.totalDurationMs), + maxDurationMs: Math.round(stats.maxDurationMs), intervalMs: LONG_TASK_REPORT_INTERVAL, ...getWebListDiagnostics(containerRef, rowNodesRef), }) - taskCount = 0 - totalDurationMs = 0 - maxDurationMs = 0 + stats.taskCount = 0 + stats.totalDurationMs = 0 + stats.maxDurationMs = 0 } const interval = setInterval(report, LONG_TASK_REPORT_INTERVAL) return () => { diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 6122757f71..3250ee3782 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -567,11 +567,11 @@ let PreviewableUserAvatar = ({ unstableCacheProfileView(queryClient, profile) }, [profile, queryClient, onBeforePress]) - const onOpenLiveStatus = useCallback(() => { + const onOpenLiveStatus = () => { playHaptic('Light') ax.metric('live:card:open', {subject: profile.did, from: 'post'}) liveControl.open() - }, [liveControl, playHaptic, profile.did]) + } const avatarEl = (