Skip to content

Commit

Permalink
chore(core): update useSeenAnnouncements, handle seen and unseen thro…
Browse files Browse the repository at this point in the history
…ugh rxjs
  • Loading branch information
pedrobonamin committed Sep 23, 2024
1 parent ae0aa6a commit 9f62d16
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {useTelemetry} from '@sanity/telemetry/react'
import {useCallback, useMemo, useState} from 'react'
import {useObservable} from 'react-rx'
import {catchError, map, type Observable, startWith} from 'rxjs'
import {catchError, combineLatest, map, type Observable, startWith} from 'rxjs'
import {StudioAnnouncementContext} from 'sanity/_singletons'

import {useClient} from '../../hooks/useClient'
Expand Down Expand Up @@ -36,12 +36,15 @@ export function StudioAnnouncementsProvider({children}: StudioAnnouncementsProvi
const telemetry = useTelemetry()
const [dialogMode, setDialogMode] = useState<DialogMode | null>(null)
const [isCardDismissed, setIsCardDismissed] = useState(false)
const [seenAnnouncements, setSeenAnnouncements] = useSeenAnnouncements()
const [seenAnnouncements$, setSeenAnnouncements] = useSeenAnnouncements()
const {currentUser} = useWorkspace()
const client = useClient(CLIENT_OPTIONS)

const fetchAnnouncements: Observable<StudioAnnouncementDocument[]> = useMemo(() => {
return client.observable
const getAnnouncements$: Observable<{
unseen: StudioAnnouncementDocument[]
all: StudioAnnouncementDocument[]
}> = useMemo(() => {
const allAnnouncements$ = client.observable
.request<StudioAnnouncementDocument[] | null>({url: '/journey/announcements'})
.pipe(
map((docs) => {
Expand All @@ -58,20 +61,20 @@ export function StudioAnnouncementsProvider({children}: StudioAnnouncementsProvi
catchError(() => []),
startWith([]),
)
}, [client, currentUser?.roles])
const studioAnnouncements = useObservable(fetchAnnouncements, [])

const unseenAnnouncements: StudioAnnouncementDocument[] = useMemo(() => {
// If it's loading or it has errored return an empty array to avoid showing the card
if (seenAnnouncements.loading || seenAnnouncements.error) return []
// If none is seen, return all the announcements
if (!seenAnnouncements.value) return studioAnnouncements

// Filter out the seen announcements
const unseen = studioAnnouncements.filter((doc) => !seenAnnouncements.value?.includes(doc._id))

return unseen
}, [seenAnnouncements, studioAnnouncements])
return combineLatest([allAnnouncements$, seenAnnouncements$]).pipe(
map(([all, seen]) => {
if (seen.loading || seen.error) return {unseen: [], all: all}
if (!seen.value) return {unseen: all, all: all}
const unseen = all.filter((doc) => !seen.value?.includes(doc._id))
return {unseen: unseen, all: all}
}),
)
}, [client.observable, currentUser?.roles, seenAnnouncements$])

const announcements = useObservable(getAnnouncements$, {unseen: [], all: []})
const unseenAnnouncements = announcements.unseen
const studioAnnouncements = announcements.all

const saveSeenAnnouncements = useCallback(() => {
// Mark all the announcements as seen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ describe('StudioAnnouncementsProvider', () => {
describe('if seen announcements is loading', () => {
beforeEach(() => {
jest.clearAllMocks()
seenAnnouncementsMock.mockReturnValue([{loading: true, value: null, error: null}, jest.fn()])
seenAnnouncementsMock.mockReturnValue([
of({loading: true, value: null, error: null}),
jest.fn(),
])
mockClient(mockAnnouncements)
})
test('returns empty unseen announcements ', () => {
Expand All @@ -114,7 +117,7 @@ describe('StudioAnnouncementsProvider', () => {
beforeEach(() => {
jest.clearAllMocks()
seenAnnouncementsMock.mockReturnValue([
{loading: false, value: null, error: new Error('something went wrong')},
of({loading: false, value: null, error: new Error('something went wrong')}),
jest.fn(),
])
mockClient(mockAnnouncements)
Expand All @@ -135,7 +138,10 @@ describe('StudioAnnouncementsProvider', () => {
describe('if seen announcements is not loading and has no values', () => {
beforeEach(() => {
jest.clearAllMocks()
seenAnnouncementsMock.mockReturnValue([{value: [], error: null, loading: false}, jest.fn()])
seenAnnouncementsMock.mockReturnValue([
of({value: [], error: null, loading: false}),
jest.fn(),
])
mockClient(mockAnnouncements)
})
test('returns unseen announcements', () => {
Expand All @@ -157,7 +163,7 @@ describe('StudioAnnouncementsProvider', () => {
jest.clearAllMocks()
// It doesn't show the first element
seenAnnouncementsMock.mockReturnValue([
{value: ['studioAnnouncement-1'], error: null, loading: false},
of({value: ['studioAnnouncement-1'], error: null, loading: false}),
jest.fn(),
])

Expand All @@ -182,7 +188,7 @@ describe('StudioAnnouncementsProvider', () => {
jest.clearAllMocks()
// It doesn't show the first element
seenAnnouncementsMock.mockReturnValue([
{value: ['studioAnnouncement-1'], error: null, loading: false},
of({value: ['studioAnnouncement-1'], error: null, loading: false}),
jest.fn(),
])

Expand Down Expand Up @@ -428,7 +434,10 @@ describe('StudioAnnouncementsProvider', () => {
beforeEach(() => {
jest.clearAllMocks()
// It doesn't show the first element
seenAnnouncementsMock.mockReturnValue([{value: [], error: null, loading: false}, jest.fn()])
seenAnnouncementsMock.mockReturnValue([
of({value: [], error: null, loading: false}),
jest.fn(),
])
})
test('if the audience is everyone, it shows the announcement regardless the version', () => {
const announcements: StudioAnnouncementDocument[] = [
Expand Down Expand Up @@ -664,7 +673,7 @@ describe('StudioAnnouncementsProvider', () => {
test('when the card is dismissed, and only 1 announcement received', () => {
const saveSeenAnnouncementsMock = jest.fn()
seenAnnouncementsMock.mockReturnValue([
{value: [], error: null, loading: false},
of({value: [], error: null, loading: false}),
saveSeenAnnouncementsMock,
])
mockClient([mockAnnouncements[0]])
Expand All @@ -678,7 +687,7 @@ describe('StudioAnnouncementsProvider', () => {
test('when the card is dismissed, and 2 announcements are received', () => {
const saveSeenAnnouncementsMock = jest.fn()
seenAnnouncementsMock.mockReturnValue([
{value: [], error: null, loading: false},
of({value: [], error: null, loading: false}),
saveSeenAnnouncementsMock,
])
mockClient(mockAnnouncements)
Expand All @@ -693,7 +702,7 @@ describe('StudioAnnouncementsProvider', () => {
const saveSeenAnnouncementsMock = jest.fn()
// The id received here is not present anymore in the mock announcements, this id won't be stored in next save.
seenAnnouncementsMock.mockReturnValue([
{value: ['not-to-be-persisted'], error: null, loading: false},
of({value: ['not-to-be-persisted'], error: null, loading: false}),
saveSeenAnnouncementsMock,
])
mockClient(mockAnnouncements)
Expand All @@ -707,7 +716,7 @@ describe('StudioAnnouncementsProvider', () => {
const saveSeenAnnouncementsMock = jest.fn()
// The id received here is present in the mock announcements, this id will be persisted in next save.
seenAnnouncementsMock.mockReturnValue([
{value: [mockAnnouncements[0]._id], error: null, loading: false},
of({value: [mockAnnouncements[0]._id], error: null, loading: false}),
saveSeenAnnouncementsMock,
])
mockClient(mockAnnouncements)
Expand All @@ -721,7 +730,7 @@ describe('StudioAnnouncementsProvider', () => {
test('when the dialog is closed', () => {
const saveSeenAnnouncementsMock = jest.fn()
seenAnnouncementsMock.mockReturnValue([
{value: [], error: null, loading: false},
of({value: [], error: null, loading: false}),
saveSeenAnnouncementsMock,
])
mockClient(mockAnnouncements)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {of, Subject} from 'rxjs'
import {useRouter} from 'sanity/router'

import {useKeyValueStore} from '../../../store/_legacy/datastores'
import {useSeenAnnouncements} from '../useSeenAnnouncements'
import {type SeenAnnouncementsState, useSeenAnnouncements} from '../useSeenAnnouncements'

jest.mock('../../../store/_legacy/datastores', () => ({
useKeyValueStore: jest.fn(),
Expand All @@ -24,20 +24,26 @@ describe('useSeenAnnouncements', () => {
const observable = new Subject<string[]>()
const getKeyMock = jest.fn().mockReturnValue(observable)
const setKeyMock = jest.fn()

useKeyValueStoreMock.mockReturnValue({getKey: getKeyMock, setKey: setKeyMock})

const {result} = renderHook(() => useSeenAnnouncements())
expect(result.current[0]).toEqual({value: null, error: null, loading: true})

const seenAnnouncements$ = result.current[0]
const seenAnnouncements = ['announcement1', 'announcement2']
act(() => {
observable.next(seenAnnouncements)

const expectedStates: SeenAnnouncementsState[] = [
{value: null, error: null, loading: true},
{value: seenAnnouncements, error: null, loading: false},
]
const emissions: SeenAnnouncementsState[] = []

seenAnnouncements$.subscribe((state) => {
emissions.push(state)
})

await waitFor(() => {
expect(result.current[0]).toEqual({value: seenAnnouncements, error: null, loading: false})
act(() => {
observable.next(seenAnnouncements)
})
expect(emissions).toEqual(expectedStates)
})
test('should handle errors on the keyValueStore', async () => {
const observable = new Subject<string[]>()
Expand All @@ -47,20 +53,23 @@ describe('useSeenAnnouncements', () => {
useKeyValueStoreMock.mockReturnValue({getKey: getKeyMock, setKey: setKeyMock})

const {result} = renderHook(() => useSeenAnnouncements())
expect(result.current[0]).toEqual({value: null, error: null, loading: true})
const seenAnnouncements$ = result.current[0]

const emissions: SeenAnnouncementsState[] = []

seenAnnouncements$.subscribe((state) => {
emissions.push(state)
})

const error = new Error('An error occurred')
act(() => {
observable.error(error)
})

await waitFor(() => {
expect(result.current[0]).toEqual({
value: null,
error: error,
loading: false,
})
})
const expectedStates: SeenAnnouncementsState[] = [
{value: null, error: null, loading: true},
{value: null, error: error, loading: false},
]
expect(emissions).toEqual(expectedStates)
})

test('should call the getKey function with the correct key when the hook is called', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {useCallback, useEffect, useMemo} from 'react'
import {useObservable} from 'react-rx'
import {catchError, map, of} from 'rxjs'
import {catchError, map, type Observable, of, startWith} from 'rxjs'
import {useRouter} from 'sanity/router'

import {useKeyValueStore} from '../../store/_legacy/datastores'

const KEY = 'studio.announcement.seen'
const RESET_PARAM = 'reset-announcements'

interface SeenAnnouncementsState {
export interface SeenAnnouncementsState {
value: string[] | null
error: Error | null
loading: boolean
Expand All @@ -19,20 +18,22 @@ const INITIAL_STATE: SeenAnnouncementsState = {
loading: true,
}

export function useSeenAnnouncements(): [SeenAnnouncementsState, (seen: string[]) => void] {
export function useSeenAnnouncements(): [
Observable<SeenAnnouncementsState>,
(seen: string[]) => void,
] {
const router = useRouter()
const keyValueStore = useKeyValueStore()
const seenAnnouncements$ = useMemo(
const seenAnnouncements$: Observable<SeenAnnouncementsState> = useMemo(
() =>
keyValueStore.getKey(KEY).pipe(
map((value) => ({value: value as string[] | null, error: null, loading: false})),
startWith(INITIAL_STATE),
catchError((error) => of({value: null, error: error, loading: false})),
),
[keyValueStore],
)

const seenAnnouncements = useObservable(seenAnnouncements$, INITIAL_STATE)

const setSeenAnnouncements = useCallback(
(seen: string[]) => {
keyValueStore.setKey(KEY, seen)
Expand All @@ -52,5 +53,5 @@ export function useSeenAnnouncements(): [SeenAnnouncementsState, (seen: string[]
}
}, [resetAnnouncementsParams, setSeenAnnouncements])

return [seenAnnouncements, setSeenAnnouncements]
return [seenAnnouncements$, setSeenAnnouncements]
}

0 comments on commit 9f62d16

Please sign in to comment.