diff --git a/packages/shared/src/hooks/feed/useAdvancedSettings.ts b/packages/shared/src/hooks/feed/useAdvancedSettings.ts index b2c1986161..ec107493f1 100644 --- a/packages/shared/src/hooks/feed/useAdvancedSettings.ts +++ b/packages/shared/src/hooks/feed/useAdvancedSettings.ts @@ -10,7 +10,7 @@ import useTagAndSource from '../useTagAndSource'; import { Origin } from '../../lib/log'; interface UseAdvancedSettings { - selectedSettings: Record; + selectedSettings: Record; onToggleSettings(id: number, state: boolean): void; onToggleSource(source: Source): void; onUpdateSettings( @@ -30,11 +30,14 @@ export const useAdvancedSettings = ( const selectedSettings = useMemo( () => - feedSettings?.advancedSettings?.reduce((settingsMap, currentSettings) => { - const map = { ...settingsMap }; - map[currentSettings.id] = currentSettings.enabled; - return map; - }, {}) || {}, + feedSettings?.advancedSettings?.reduce>( + (settingsMap, currentSettings) => { + const map = { ...settingsMap }; + map[currentSettings.id] = currentSettings.enabled; + return map; + }, + {}, + ) || {}, [feedSettings?.advancedSettings], ); @@ -61,7 +64,7 @@ export const useAdvancedSettings = ( const onToggleSettings = useCallback( (id: number, defaultEnabledState: boolean) => { if (alerts?.filter && user) { - updateAlerts({ filter: false }); + updateAlerts?.({ filter: false }); } const enabled = !(selectedSettings[id] ?? defaultEnabledState); diff --git a/packages/shared/src/hooks/feed/useAutoRotatingAds.ts b/packages/shared/src/hooks/feed/useAutoRotatingAds.ts index a759e51e5a..3484c88b17 100644 --- a/packages/shared/src/hooks/feed/useAutoRotatingAds.ts +++ b/packages/shared/src/hooks/feed/useAutoRotatingAds.ts @@ -40,7 +40,7 @@ export const useAutoRotatingAds = ( const rotationTime = autorotateAds * 1_000; const refs = useCallback( - (node: HTMLElement) => { + (node?: Element | null) => { ref?.(node); inViewRef(node); }, @@ -49,14 +49,14 @@ export const useAutoRotatingAds = ( const { fetchAd } = useFetchAd(); const queryKey = useMemo( - () => [RequestKey.Ads, ...feedQueryKey], + () => [RequestKey.Ads, ...(feedQueryKey ?? [])], [feedQueryKey], ); const fetchNewAd = useCallback(async (): Promise => { const newAd = await fetchAd({ active: true }); if (!newAd) { - return null; + throw new Error('Unable to fetch replacement ad'); } // End the impression event for the old ad diff --git a/packages/shared/src/hooks/feed/useCustomDefaultFeed.ts b/packages/shared/src/hooks/feed/useCustomDefaultFeed.ts index 27d8c76182..198dd70742 100644 --- a/packages/shared/src/hooks/feed/useCustomDefaultFeed.ts +++ b/packages/shared/src/hooks/feed/useCustomDefaultFeed.ts @@ -9,8 +9,9 @@ const useCustomDefaultFeed = (): UseCustomDefaultFeed => { const { user } = useAuthContext(); return { - isCustomDefaultFeed: user?.defaultFeedId && user.defaultFeedId !== user?.id, - defaultFeedId: user?.defaultFeedId ?? user?.id, + isCustomDefaultFeed: + !!user?.defaultFeedId && user.defaultFeedId !== user?.id, + defaultFeedId: user?.defaultFeedId ?? user?.id ?? '', }; }; diff --git a/packages/shared/src/hooks/feed/useFeedContextMenu.ts b/packages/shared/src/hooks/feed/useFeedContextMenu.ts index 4ad64bfa92..fb725f798e 100644 --- a/packages/shared/src/hooks/feed/useFeedContextMenu.ts +++ b/packages/shared/src/hooks/feed/useFeedContextMenu.ts @@ -23,8 +23,8 @@ type FeedContextMenu = { row: number, column: number, ) => void; - postMenuIndex: number; - postMenuLocation: PostLocation; + postMenuIndex: number | undefined; + postMenuLocation: PostLocation | undefined; setPostMenuIndex: (value: PostLocation | undefined) => void; }; @@ -39,7 +39,7 @@ export default function useFeedContextMenu(): FeedContextMenu { column: number, ) => { if (postMenuIndex === index) { - setPostMenuLocation(null); + setPostMenuLocation(undefined); return; } setPostMenuLocation({ index, row, column }); @@ -53,7 +53,7 @@ export default function useFeedContextMenu(): FeedContextMenu { column: number, ) => { if (postMenuIndex === index) { - setPostMenuLocation(null); + setPostMenuLocation(undefined); return; } setPostMenuLocation({ index, row, column }); diff --git a/packages/shared/src/hooks/feed/useFeeds.spec.tsx b/packages/shared/src/hooks/feed/useFeeds.spec.tsx index 0c9c55964b..7462ca61d7 100644 --- a/packages/shared/src/hooks/feed/useFeeds.spec.tsx +++ b/packages/shared/src/hooks/feed/useFeeds.spec.tsx @@ -4,6 +4,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import defaultUser from '../../../__tests__/fixture/loggedUser'; import { AuthContextProvider } from '../../contexts/AuthContext'; import { useFeeds } from './useFeeds'; +import type { Feed } from '../../graphql/feed'; import { CREATE_FEED_MUTATION, DELETE_FEED_MUTATION, @@ -18,7 +19,7 @@ const client = new QueryClient(); const noop = jest.fn(); let queryCalled = false; -const Wrapper = ({ children }) => { +const Wrapper = ({ children }: React.PropsWithChildren) => { return ( { await waitFor(() => expect(queryCalled).toBe(true)); expect(result.current.feeds).toBeTruthy(); - expect(result.current.feeds.edges).toMatchObject(feeds); + expect(result.current.feeds?.edges).toMatchObject(feeds); }); it('should create a feed', async () => { @@ -193,7 +194,7 @@ describe('useFeeds hook', () => { await waitFor(() => expect(queryCalled).toBe(true)); - let feed; + let feed: Feed | undefined; await act(async () => { feed = await result.current.createFeed({ name: 'New feed' }); @@ -201,9 +202,19 @@ describe('useFeeds hook', () => { rerender(); expect(feed).toBeTruthy(); - expect(feed.flags.name).toBe('New feed'); + if (!feed) { + throw new Error('Created feed is required'); + } + + if (!feed.flags) { + throw new Error('Created feed flags are required'); + } + + const createdFeed = feed; + const createdFeedFlags = feed.flags; + expect(createdFeedFlags.name).toBe('New feed'); expect( - result.current.feeds.edges.find((f) => f.node.id === feed.id), + result.current.feeds?.edges.find((f) => f.node.id === createdFeed.id), ).toBeTruthy(); }); @@ -214,7 +225,7 @@ describe('useFeeds hook', () => { await waitFor(() => expect(queryCalled).toBe(true)); - let feed; + let feed: Feed | undefined; await act(async () => { feed = await result.current.updateFeed({ feedId: 'cf1', @@ -224,9 +235,19 @@ describe('useFeeds hook', () => { rerender(); expect(feed).toBeTruthy(); - expect(feed.flags.name).toBe('Updated feed'); + if (!feed) { + throw new Error('Updated feed is required'); + } + + if (!feed.flags) { + throw new Error('Updated feed flags are required'); + } + + const updatedFeed = feed; + const updatedFeedFlags = feed.flags; + expect(updatedFeedFlags.name).toBe('Updated feed'); expect( - result.current.feeds.edges.find((f) => f.node.id === feed.id), + result.current.feeds?.edges.find((f) => f.node.id === updatedFeed.id), ).toBeTruthy(); }); @@ -244,7 +265,7 @@ describe('useFeeds hook', () => { rerender(); expect( - result.current.feeds.edges.find((f) => f.node.id === 'cf1'), + result.current.feeds?.edges.find((f) => f.node.id === 'cf1'), ).toBeFalsy(); }); }); diff --git a/packages/shared/src/hooks/feed/useFeeds.ts b/packages/shared/src/hooks/feed/useFeeds.ts index 020e449d58..7d90feb2b4 100644 --- a/packages/shared/src/hooks/feed/useFeeds.ts +++ b/packages/shared/src/hooks/feed/useFeeds.ts @@ -22,7 +22,7 @@ export type UpdateFeedProps = { feedId: string } & CreateFeedProps; export type DeleteFeedProps = Pick; export type UseFeeds = { - feeds: FeedList['feedList']; + feeds: FeedList['feedList'] | undefined; createFeed: (props: CreateFeedProps) => Promise; updateFeed: (props: UpdateFeedProps) => Promise; deleteFeed: (props: DeleteFeedProps) => Promise>; @@ -58,6 +58,10 @@ export const useFeeds = (): UseFeeds => { onSuccess: (data) => { queryClient.setQueryData(queryKey, (current) => { + if (!current) { + return current; + } + return { ...current, edges: [ @@ -87,6 +91,10 @@ export const useFeeds = (): UseFeeds => { onSuccess: (data) => { queryClient.setQueryData(queryKey, (current) => { + if (!current) { + return current; + } + return { ...current, edges: (current?.edges || []).map((edge) => { @@ -118,6 +126,10 @@ export const useFeeds = (): UseFeeds => { onSuccess: (data) => { queryClient.setQueryData(queryKey, (current) => { + if (!current) { + return current; + } + return { ...current, edges: (current?.edges || []).filter( diff --git a/packages/shared/src/hooks/feed/useFollowPostTags.ts b/packages/shared/src/hooks/feed/useFollowPostTags.ts index 825bb4c62f..6c324019cc 100644 --- a/packages/shared/src/hooks/feed/useFollowPostTags.ts +++ b/packages/shared/src/hooks/feed/useFollowPostTags.ts @@ -26,17 +26,18 @@ export const useFollowPostTags = ({ const isModerationItem = !post?.permalink; const tags = useMemo(() => { + const all = post?.tags ?? []; + if (!isLoggedIn || isModerationItem) { return { - all: post?.tags, + all, followed: [], - notFollowed: post?.tags, + notFollowed: all, }; } - const all = post?.tags ?? []; const followedTags = new Set(feedSettings?.includeTags || []); - return all.reduce( + return all.reduce>( (acc, tag) => { const isFollowing = followedTags.has(tag); const group = isFollowing ? 'followed' : 'notFollowed'; diff --git a/packages/shared/src/hooks/log/useDeviceId.ts b/packages/shared/src/hooks/log/useDeviceId.ts index 869fa8c83e..2725210a07 100644 --- a/packages/shared/src/hooks/log/useDeviceId.ts +++ b/packages/shared/src/hooks/log/useDeviceId.ts @@ -14,7 +14,7 @@ export const getOrGenerateDeviceId = async (): Promise => { return newDeviceId; }; -export default function useDeviceId(): string { +export default function useDeviceId(): string | undefined { const [deviceId, setDeviceId] = useState(); useEffect(() => { diff --git a/packages/shared/src/hooks/log/useLogContextData.ts b/packages/shared/src/hooks/log/useLogContextData.ts index e23fc7dc4c..75d04a2d09 100644 --- a/packages/shared/src/hooks/log/useLogContextData.ts +++ b/packages/shared/src/hooks/log/useLogContextData.ts @@ -70,8 +70,10 @@ export default function useLogContextData( const event = durationEventsQueue.current.get(id); if (event) { durationEventsQueue.current.delete(id); - event.event_duration = - now.getTime() - event.event_timestamp.getTime(); + const eventTimestamp = event.event_timestamp; + if (eventTimestamp) { + event.event_duration = now.getTime() - eventTimestamp.getTime(); + } if (window.scrollY > 0 && event.event_name !== 'page inactive') { event.page_state = 'active'; } diff --git a/packages/shared/src/hooks/log/useLogLifecycleEvents.ts b/packages/shared/src/hooks/log/useLogLifecycleEvents.ts index c40d3d6b93..bec0c3eb06 100644 --- a/packages/shared/src/hooks/log/useLogLifecycleEvents.ts +++ b/packages/shared/src/hooks/log/useLogLifecycleEvents.ts @@ -55,8 +55,9 @@ export default function useLogLifecycleEvents( useEffect(() => { listenToLifecycleEvents(); - const callback = (event: CustomEvent) => - lifecycleCallbackRef.current(event); + const callback = (event: Event) => { + lifecycleCallbackRef.current?.(event as CustomEvent); + }; window.addEventListener('statechange', callback); return () => window.removeEventListener('statechange', callback); }, []); diff --git a/packages/shared/src/hooks/log/useLogOpportunityNudgeImpression.ts b/packages/shared/src/hooks/log/useLogOpportunityNudgeImpression.ts index d1b87d7443..b10ac34c93 100644 --- a/packages/shared/src/hooks/log/useLogOpportunityNudgeImpression.ts +++ b/packages/shared/src/hooks/log/useLogOpportunityNudgeImpression.ts @@ -23,7 +23,7 @@ export const useLogOpportunityNudgeImpression = ( return; } - logRef.current({ + logRef.current?.({ event_name: LogEvent.ImpressionOpportunityNudge, target_id: targetId, extra: logExtraPayload, diff --git a/packages/shared/src/hooks/log/useLogPageView.ts b/packages/shared/src/hooks/log/useLogPageView.ts index 0ebbf29952..920c597cdd 100644 --- a/packages/shared/src/hooks/log/useLogPageView.ts +++ b/packages/shared/src/hooks/log/useLogPageView.ts @@ -3,7 +3,9 @@ import { useEffect, useRef } from 'react'; import { useRouter } from 'next/router'; import { useLogContext } from '../../contexts/LogContext'; -export default function useLogPageView(): MutableRefObject<() => void> { +export default function useLogPageView(): MutableRefObject< + (() => void) | undefined +> { const router = useRouter(); const { logEventStart, logEventEnd } = useLogContext(); const routeChangedCallbackRef = useRef<() => void>(); @@ -23,11 +25,12 @@ export default function useLogPageView(): MutableRefObject<() => void> { }, [logEventStart, logEventEnd]); useEffect(() => { - const handleRouteChange = () => routeChangedCallbackRef.current(); + const handleRouteChange = () => routeChangedCallbackRef.current?.(); router.events.on('routeChangeComplete', handleRouteChange); - const handleLifecycle = (event: CustomEvent) => - lifecycleCallbackRef.current(event); + const handleLifecycle = (event: Event) => { + lifecycleCallbackRef.current?.(event as CustomEvent); + }; window.addEventListener('statechange', handleLifecycle); return () => { diff --git a/packages/shared/src/hooks/log/useLogSharedProps.ts b/packages/shared/src/hooks/log/useLogSharedProps.ts index ef83b0985f..300e132581 100644 --- a/packages/shared/src/hooks/log/useLogSharedProps.ts +++ b/packages/shared/src/hooks/log/useLogSharedProps.ts @@ -14,7 +14,7 @@ export default function useLogSharedProps( deviceId: string, ): [MutableRefObject>, boolean] { // Use ref instead of state to reduce renders - const sharedPropsRef = useRef>(); + const sharedPropsRef = useRef>({}); const { query } = useRouter(); const { themeMode, spaciness, insaneMode } = useContext(SettingsContext); const { visit, anonymous, tokenRefreshed, user } = useContext(AuthContext);