diff --git a/apps/meteor/client/components/VirtualList/VirtuaListContainer.tsx b/apps/meteor/client/components/VirtualList/VirtuaListContainer.tsx new file mode 100644 index 0000000000000..afcffac6ad716 --- /dev/null +++ b/apps/meteor/client/components/VirtualList/VirtuaListContainer.tsx @@ -0,0 +1,24 @@ +import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'; +import { forwardRef } from 'react'; + +const listResetStyle = { + margin: 0, + padding: 0, + listStyle: 'none', +} as const; + +export type VirtuaListContainerProps = { + children: ReactNode; + style: CSSProperties; +} & Omit, 'children' | 'style'>; + +export const VirtuaListContainer = forwardRef(function VirtuaListContainer( + { children, style, ...props }, + ref, +) { + return ( + + ); +}); diff --git a/apps/meteor/client/components/VirtualList/VirtualList.spec.tsx b/apps/meteor/client/components/VirtualList/VirtualList.spec.tsx new file mode 100644 index 0000000000000..332ed5ffb8776 --- /dev/null +++ b/apps/meteor/client/components/VirtualList/VirtualList.spec.tsx @@ -0,0 +1,216 @@ +import type { UseInfiniteQueryResult } from '@tanstack/react-query'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; +import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'; +import * as React from 'react'; +import { Children, forwardRef, isValidElement } from 'react'; + +import PaginatedVirtualList from './VirtualList'; + +const mockVirtualizerHandle = { + scrollToIndex: jest.fn(), + scrollTo: jest.fn(), + findItemIndex: jest.fn((offset: number) => offset), + scrollOffset: 0, + scrollSize: 1000, + viewportSize: 300, +}; + +type MockVListProps = { + children: ReactNode; + bufferSize?: number; + onScroll?: (offset: number) => void; + as?: React.ElementType; + item?: React.ElementType; + style?: CSSProperties; + className?: string; +}; + +jest.mock('virtua', () => { + return { + Virtualizer: React.forwardRef( + ( + { children, bufferSize, onScroll, as: asRoot = 'div', item: asItem = 'div', style, className }: MockVListProps, + ref: React.Ref, + ) => { + React.useImperativeHandle(ref, () => mockVirtualizerHandle); + const Root = asRoot; + const Item = asItem; + const wrapped = Children.map(children, (child, index) => { + const key = isValidElement(child) && child.key != null ? String(child.key) : `row-${index}`; + return {child}; + }); + + return ( + onScroll?.(mockVirtualizerHandle.scrollOffset)} + > + {wrapped} + + ); + }, + ), + }; +}); + +jest.mock('@rocket.chat/ui-client', () => ({ + ...jest.requireActual('@rocket.chat/ui-client'), + CustomVirtuaScrollbars: forwardRef>(function CustomVirtuaScrollbars( + { children, ...props }, + ref, + ) { + // eslint-disable-next-line testing-library/no-node-access + const content = isValidElement<{ children?: ReactNode }>(children) && children.type === 'div' ? children.props.children : children; + + return ( +
+ {content} +
+ ); + }), +})); + +const items = Array.from({ length: 10 }, (_, index) => ({ _id: `${index}` })); + +type VirtualListTestItem = (typeof items)[number]; + +const renderVirtualList = ( + props: Partial<{ + items: VirtualListTestItem[]; + totalCount: number; + renderItem: (item: VirtualListTestItem, index: number) => ReactNode; + overscan?: number; + onEndReached?: UseInfiniteQueryResult['fetchNextPage']; + }> = {}, +) => render(
{item._id}
} {...props} />); + +const advanceDebouncedScroll = async () => { + await act(async () => { + await jest.advanceTimersByTimeAsync(300); + }); +}; + +describe('PaginatedVirtualList', () => { + beforeEach(() => { + mockVirtualizerHandle.scrollOffset = 0; + mockVirtualizerHandle.scrollSize = 1000; + mockVirtualizerHandle.viewportSize = 300; + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('has no accessibility violations', async () => { + const { container } = renderVirtualList(); + expect(await axe(container)).toHaveNoViolations(); + }); + + it('calls onEndReached when scrolled near the bottom', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockResolvedValue(undefined); + + renderVirtualList({ onEndReached }); + expect(onEndReached).not.toHaveBeenCalled(); + + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(1); + }); + + it('does not call onEndReached when all items are loaded', () => { + const onEndReached = jest.fn().mockResolvedValue(undefined); + + renderVirtualList({ onEndReached, totalCount: items.length }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + + expect(onEndReached).not.toHaveBeenCalled(); + }); + + it('does not call onEndReached repeatedly for the same item count', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockResolvedValue(undefined); + + renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(1); + }); + + it('calls onEndReached after a same-size dataset reset', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockResolvedValue(undefined); + const { rerender } = renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + const resetItems = Array.from({ length: 10 }, (_, index) => ({ _id: `reset-${index}` })); + rerender( +
{item._id}
} onEndReached={onEndReached} />, + ); + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(2); + }); + + it('passes overscan through to virtua buffer size', () => { + renderVirtualList({ overscan: 25 }); + + expect(screen.getByTestId('virtual-list')).toHaveAttribute('data-buffer-size', '25'); + }); + + it('allows onEndReached to retry after a failed load', async () => { + jest.useFakeTimers(); + const onEndReached = jest.fn().mockRejectedValue(new Error('failed to load more items')); + + renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(2); + }); + + it('allows onEndReached to retry after a synchronous throw', async () => { + jest.useFakeTimers(); + const onEndReached = jest + .fn() + .mockImplementationOnce(() => { + throw new Error('failed to load more items'); + }) + .mockImplementation(() => undefined); + + renderVirtualList({ onEndReached }); + mockVirtualizerHandle.scrollOffset = 700; + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + fireEvent.scroll(screen.getByTestId('virtual-list')); + await advanceDebouncedScroll(); + + expect(onEndReached).toHaveBeenCalledTimes(2); + }); + + it('calls onEndReached when the viewport is underfilled', () => { + const onEndReached = jest.fn().mockResolvedValue(undefined); + mockVirtualizerHandle.scrollSize = 200; + + renderVirtualList({ onEndReached }); + + expect(onEndReached).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/meteor/client/components/VirtualList/VirtualList.tsx b/apps/meteor/client/components/VirtualList/VirtualList.tsx new file mode 100644 index 0000000000000..e2275824f21a2 --- /dev/null +++ b/apps/meteor/client/components/VirtualList/VirtualList.tsx @@ -0,0 +1,109 @@ +import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; +import { CustomVirtuaScrollbars } from '@rocket.chat/ui-client'; +import type { UseInfiniteQueryResult } from '@tanstack/react-query'; +import type { ReactNode } from 'react'; +import { useCallback, useLayoutEffect, useRef } from 'react'; +import type { VirtualizerHandle } from 'virtua'; +import { Virtualizer } from 'virtua'; + +import { VirtuaListContainer } from './VirtuaListContainer'; + +const NEAR_BOTTOM_THRESHOLD = -20; + +const scrollViewportStyle = { + height: '100%', + width: '100%', + overflow: 'auto', +} as const; + +type PaginatedVirtualListProps = { + items: T[]; + totalCount: number; + renderItem: (item: T, index: number) => ReactNode; + overscan?: number; + onEndReached?: UseInfiniteQueryResult['fetchNextPage']; +}; + +function PaginatedVirtualList({ + items, + totalCount, + renderItem, + overscan, + onEndReached, +}: PaginatedVirtualListProps) { + const virtualizerRef = useRef(null); + const isEndReachedLockedRef = useRef(false); + const firstItemId = items[0]?._id ?? ''; + const lastItemId = items[items.length - 1]?._id ?? ''; + + useLayoutEffect(() => { + isEndReachedLockedRef.current = false; + }, [firstItemId, items.length, lastItemId, totalCount]); + + const checkEndReached = useCallback( + (offset: number) => { + const handle = virtualizerRef.current; + if (!handle || !onEndReached) { + return; + } + + const { scrollSize, viewportSize } = handle; + if (viewportSize <= 0) { + return; + } + + if (items.length >= totalCount) { + return; + } + + const nearBottom = offset - scrollSize + viewportSize >= NEAR_BOTTOM_THRESHOLD; + if (!nearBottom) { + return; + } + + if (isEndReachedLockedRef.current) { + return; + } + isEndReachedLockedRef.current = true; + + try { + void onEndReached().catch(() => { + isEndReachedLockedRef.current = false; + }); + } catch { + isEndReachedLockedRef.current = false; + } + }, + [items.length, onEndReached, totalCount], + ); + + const handleScroll = useDebouncedCallback( + (offset: number) => { + checkEndReached(offset); + }, + 300, + [checkEndReached], + ); + + useLayoutEffect(() => { + const handle = virtualizerRef.current; + if (!handle) { + return; + } + checkEndReached(handle.scrollOffset); + }, [checkEndReached, firstItemId, items.length, lastItemId, totalCount]); + + return ( + +
+ + {items.map((item, index) => ( +
{renderItem(item, index)}
+ ))} +
+
+
+ ); +} + +export default PaginatedVirtualList; diff --git a/apps/meteor/client/components/VirtualList/index.ts b/apps/meteor/client/components/VirtualList/index.ts new file mode 100644 index 0000000000000..4b475317de971 --- /dev/null +++ b/apps/meteor/client/components/VirtualList/index.ts @@ -0,0 +1 @@ +export { default as PaginatedVirtualList } from './VirtualList'; diff --git a/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx b/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx index 6b92270b2ac07..60adcfba4c9a3 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx @@ -18,7 +18,7 @@ const mockVirtualizerHandle = { }; jest.mock('virtua', () => { - const React = require('react'); + const React = jest.requireActual('react'); return { VList: React.forwardRef( diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.spec.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.spec.tsx index 95f4b9bb6db3d..92d28ff999dfa 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.spec.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.spec.tsx @@ -1,6 +1,9 @@ import { composeStories } from '@storybook/react'; -import { render } from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import { axe } from 'jest-axe'; +import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'; +import * as React from 'react'; +import { Children, forwardRef, isValidElement } from 'react'; import * as stories from './DiscussionsList.stories'; @@ -10,8 +13,104 @@ jest.mock('../../../../lib/rooms/roomCoordinator', () => ({ }, })); -const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); -test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { +const mockVirtualizerHandle = { + scrollToIndex: jest.fn(), + scrollTo: jest.fn(), + findItemIndex: jest.fn((offset: number) => offset), + scrollOffset: 0, + scrollSize: 1000, + viewportSize: 300, +}; + +type MockVListProps = { + children: ReactNode; + onScroll?: (offset: number) => void; + as?: React.ElementType; + item?: React.ElementType; + shift?: boolean; + style?: CSSProperties; + className?: string; +}; + +jest.mock('virtua', () => { + return { + Virtualizer: React.forwardRef( + ( + { children, onScroll, as: asRoot = 'div', item: asItem = 'div', shift: _shift, style, className }: MockVListProps, + ref: React.Ref, + ) => { + React.useImperativeHandle(ref, () => mockVirtualizerHandle); + const Root = asRoot; + const Item = asItem; + const wrapped = Children.map(children, (child, index) => { + const key = isValidElement(child) && child.key != null ? String(child.key) : `row-${index}`; + return {child}; + }); + + return ( + onScroll?.(mockVirtualizerHandle.scrollOffset)} + > + {wrapped} + + ); + }, + ), + }; +}); + +jest.mock('@rocket.chat/ui-client', () => ({ + ...jest.requireActual('@rocket.chat/ui-client'), + CustomVirtuaScrollbars: forwardRef>(function CustomVirtuaScrollbars( + { children, ...props }, + ref, + ) { + // eslint-disable-next-line testing-library/no-node-access + const content = isValidElement<{ children?: ReactNode }>(children) && children.type === 'div' ? children.props.children : children; + + return ( +
+ {content} +
+ ); + }), +})); + +const composed = composeStories(stories); +const testCases = Object.values(composed).map((Story) => [Story.storyName || 'Story', Story] as const); + +describe('DiscussionsList', () => { + it('renders Default with virtual list and discussion rows', () => { + const { Default } = composed; + render(); + + const list = screen.getByTestId('discussions-virtual-list'); + expect(list).toBeInTheDocument(); + expect(list.tagName.toLowerCase()).toBe('ul'); + expect(within(list).getAllByRole('listitem')).toHaveLength(10); + expect(within(list).getAllByText('user.name')).toHaveLength(10); + }); + + it('renders Empty without virtual list', () => { + const { Empty } = composed; + render(); + + expect(screen.queryByTestId('discussions-virtual-list')).not.toBeInTheDocument(); + expect(screen.getByText('No_Discussions_found')).toBeInTheDocument(); + }); + + it('renders Loading without virtual list', () => { + const { Loading } = composed; + render(); + + expect(screen.queryByTestId('discussions-virtual-list')).not.toBeInTheDocument(); + }); +}); + +test.each(testCases)('renders %s with stable structure', (_storyname, Story) => { const { baseElement } = render(); expect(baseElement).toMatchSnapshot(); }); diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.stories.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.stories.tsx index df2d0be0d1e01..30e64b52dc6d2 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.stories.tsx @@ -1,9 +1,16 @@ import { Contextualbar } from '@rocket.chat/ui-client'; import { action } from '@storybook/addon-actions'; import type { Meta, StoryFn } from '@storybook/react'; +import type { UseInfiniteQueryResult } from '@tanstack/react-query'; import DiscussionsList from './DiscussionsList'; +const loadMoreItemsAction = action('loadMoreItems'); +const loadMoreItems = (async (options) => { + loadMoreItemsAction(options); + return {} as Awaited>; +}) satisfies UseInfiniteQueryResult['fetchNextPage']; + export default { component: DiscussionsList, parameters: { @@ -13,7 +20,7 @@ export default { decorators: [(fn) => {fn()}], args: { text: '', - loadMoreItems: action('loadMoreItems'), + loadMoreItems, }, } satisfies Meta; diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx index a34b4d015d3b9..bd6c2cba4d196 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx @@ -1,8 +1,7 @@ import type { IDiscussionMessage } from '@rocket.chat/core-typings'; import { Box, Icon, TextInput, Callout, Throbber } from '@rocket.chat/fuselage'; -import { useResizeObserver, useAutoFocus } from '@rocket.chat/fuselage-hooks'; +import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import { - VirtualizedScrollbars, ContextualbarHeader, ContextualbarIcon, ContextualbarContent, @@ -13,19 +12,20 @@ import { ContextualbarDialog, } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; +import type { UseInfiniteQueryResult } from '@tanstack/react-query'; import type { ChangeEvent, MouseEvent, RefObject } from 'react'; import { useCallback, useId } from 'react'; import { useTranslation } from 'react-i18next'; -import { Virtuoso } from 'react-virtuoso'; import DiscussionsListRow from './DiscussionsListRow'; import ResultsLiveRegion from '../../../../components/ResultsLiveRegion'; +import { PaginatedVirtualList } from '../../../../components/VirtualList'; import { useGoToRoom } from '../../hooks/useGoToRoom'; type DiscussionsListProps = { itemCount: number; discussions: Array; - loadMoreItems: (start: number, end: number) => void; + loadMoreItems: UseInfiniteQueryResult['fetchNextPage']; isPending: boolean; isSuccess: boolean; onClose: () => void; @@ -56,15 +56,11 @@ function DiscussionsList({ const onClick = useCallback( (e: MouseEvent) => { const { drid } = e.currentTarget.dataset; - if (drid) goToRoom(drid); + if (drid) void goToRoom(drid); }, [goToRoom], ); - const { ref, contentBoxSize: { inlineSize = 378, blockSize = 1 } = {} } = useResizeObserver({ - debounceDelay: 200, - }); - return ( @@ -83,7 +79,7 @@ function DiscussionsList({ addon={} /> - + {isPending && ( @@ -99,19 +95,17 @@ function DiscussionsList({ {discussions.length === 0 && } {discussions.length > 0 && ( - - + undefined : (start) => loadMoreItems(start, Math.min(50, itemCount - start))} overscan={25} - data={discussions} - itemContent={(_, data) => } + onEndReached={isPending ? undefined : loadMoreItems} + renderItem={(discussion) => ( + + )} /> - + )} )} diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx index 23ae4d6081573..0ef5a69df0c09 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx @@ -44,7 +44,7 @@ const DiscussionListContextBar = () => { itemCount={itemCount} isPending={isPending} isSuccess={isSuccess} - loadMoreItems={() => fetchNextPage()} + loadMoreItems={fetchNextPage} text={text} onChangeFilter={handleTextChange} /> diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/__snapshots__/DiscussionsList.spec.tsx.snap b/apps/meteor/client/views/room/contextualBar/Discussions/__snapshots__/DiscussionsList.spec.tsx.snap index 9171767c3c0c3..add5b58b92ccb 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/__snapshots__/DiscussionsList.spec.tsx.snap +++ b/apps/meteor/client/views/room/contextualBar/Discussions/__snapshots__/DiscussionsList.spec.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`renders Default without crashing 1`] = ` +exports[`renders Default with stable structure 1`] = `
-
-
+
    -
    -
    +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 0 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 1 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 2 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 3 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 4 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 5 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 6 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 7 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 8 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + user.name + + + January 1st, 2024 + +
    +
    +
    +
    + Discussion 9 +
    +
    +
    +
    +
    +
    + +
    + 5 +
    +
    +
    + +
    + January 1st, 2024 +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
@@ -155,7 +1127,7 @@ exports[`renders Default without crashing 1`] = ` `; -exports[`renders Empty without crashing 1`] = ` +exports[`renders Empty with stable structure 1`] = `
`; -exports[`renders Loading without crashing 1`] = ` +exports[`renders Loading with stable structure 1`] = `
date.toISOString(), }; const largeText = { diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/components/DiscussionsListItem.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/components/DiscussionsListItem.tsx index fa1342f782f1f..0eea8986dfc51 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/components/DiscussionsListItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/components/DiscussionsListItem.tsx @@ -41,7 +41,7 @@ const DiscussionListItem = ({ name = username, ts, dcount, - formatDate = (date: any) => date, + formatDate, dlm, className = [], emoji,