diff --git a/src/components/Channel/styling/Channel.scss b/src/components/Channel/styling/Channel.scss index 4f72e3c15..8deae9d06 100644 --- a/src/components/Channel/styling/Channel.scss +++ b/src/components/Channel/styling/Channel.scss @@ -43,7 +43,13 @@ } .str-chat__loading-channel { - $text-height: calc(var(--str-chat__spacing-px) * 20); + $header-bar-height: calc(var(--str-chat__spacing-px) * 16); + $metadata-bar-height: calc(var(--str-chat__spacing-px) * 16); + $composer-control-height: calc(var(--str-chat__spacing-px) * 48); + --str-chat__loading-channel-content-max-width: calc( + var(--str-chat__message-composer-max-width) + + var(--str-chat__message-composer-padding) + ); height: 100%; display: flex; flex-direction: column; @@ -56,8 +62,8 @@ } .str-chat__loading-channel-header-name { - height: $text-height; - width: calc(var(--str-chat__spacing-px) * 112); + height: $header-bar-height; + width: calc(var(--str-chat__spacing-px) * 120); } .str-chat__loading-channel-header-avatar { @@ -74,6 +80,8 @@ .str-chat__message-list-scroll { width: 100%; height: 100%; + max-width: var(--str-chat__loading-channel-content-max-width); + margin-inline: auto; } } @@ -82,18 +90,19 @@ width: 100%; display: flex; flex-direction: column; - padding-block: var(--spacing-lg) var(--spacing-md); + gap: calc(var(--str-chat__spacing-px) * 28); + padding-block: calc(var(--str-chat__spacing-px) * 32) + calc(var(--str-chat__spacing-px) * 24); .str-chat__loading-channel-message { display: flex; width: 100%; column-gap: var(--spacing-sm); - padding-block: var(--spacing-xs); .str-chat__loading-channel-message-avatar { flex-shrink: 0; - width: calc(var(--str-chat__spacing-px) * 32); - height: calc(var(--str-chat__spacing-px) * 32); + width: calc(var(--str-chat__spacing-px) * 36); + height: calc(var(--str-chat__spacing-px) * 36); border-radius: 50%; align-self: end; } @@ -103,43 +112,37 @@ flex-direction: column; width: min(100%, var(--str-chat__message-max-width)); max-width: var(--str-chat__message-max-width); - row-gap: var(--spacing-xxs); + row-gap: calc(var(--str-chat__spacing-px) * 12); min-width: 0; } .str-chat__loading-channel-message-bubble { - min-height: calc(var(--str-chat__spacing-px) * 52); width: min(100%, var(--str-chat__message-max-width)); - border-radius: var(--message-bubble-radius-group-bottom); - } - - .str-chat__loading-channel-message-bubble--sm { - width: min(54%, calc(var(--str-chat__spacing-px) * 216)); + border-radius: var(--radius-3xl); } .str-chat__loading-channel-message-bubble--md { - width: min(68%, calc(var(--str-chat__spacing-px) * 272)); + height: calc(var(--str-chat__spacing-px) * 64); + width: min(58%, calc(var(--str-chat__spacing-px) * 272)); } .str-chat__loading-channel-message-bubble--lg { - width: min(82%, calc(var(--str-chat__spacing-px) * 328)); + height: calc(var(--str-chat__spacing-px) * 84); + width: min(72%, calc(var(--str-chat__spacing-px) * 352)); } .str-chat__loading-channel-message-metadata { - display: flex; - align-items: center; - gap: var(--spacing-xs); - min-width: 0; + height: $metadata-bar-height; + width: min(100%, var(--str-chat__message-max-width)); + border-radius: 999px; } - .str-chat__loading-channel-message-sender { - height: calc(var(--str-chat__spacing-px) * 14); - width: calc(var(--str-chat__spacing-px) * 72); + .str-chat__loading-channel-message-metadata--sm { + width: calc(var(--str-chat__spacing-px) * 88); } - .str-chat__loading-channel-message-date { - height: calc(var(--str-chat__spacing-px) * 14); - width: calc(var(--str-chat__spacing-px) * 48); + .str-chat__loading-channel-message-metadata--md { + width: calc(var(--str-chat__spacing-px) * 124); } &.str-chat__loading-channel-message--outgoing { @@ -148,20 +151,6 @@ .str-chat__loading-channel-message-content { align-items: flex-end; } - - .str-chat__loading-channel-message-bubble { - border-end-end-radius: var(--message-bubble-radius-tail); - } - - .str-chat__loading-channel-message-metadata { - justify-content: flex-end; - } - } - - &.str-chat__loading-channel-message--incoming { - .str-chat__loading-channel-message-bubble { - border-end-start-radius: var(--message-bubble-radius-tail); - } } } } @@ -169,18 +158,22 @@ .str-chat__message-composer-container--loading { pointer-events: none; + .str-chat__message-composer { + align-items: center; + } + .str-chat__loading-channel-message-input-button { flex-shrink: 0; - width: var(--button-visual-height-md); - height: var(--button-visual-height-md); + width: $composer-control-height; + height: $composer-control-height; border-radius: var(--button-radius-full); } .str-chat__loading-channel-message-input-pill { flex: 1; min-width: 0; - height: var(--button-visual-height-md); - border-radius: var(--str-chat__message-input-border-radius); + height: $composer-control-height; + border-radius: 999px; } } } @@ -275,8 +268,7 @@ @include channel-loading-shimmer; } - .str-chat__loading-channel-message-sender, - .str-chat__loading-channel-message-date { + .str-chat__loading-channel-message-metadata { @include channel-loading-faded-bar; } } diff --git a/src/components/Loading/LoadingChannel.tsx b/src/components/Loading/LoadingChannel.tsx index 4bdf6b8f8..ad666f8e9 100644 --- a/src/components/Loading/LoadingChannel.tsx +++ b/src/components/Loading/LoadingChannel.tsx @@ -1,11 +1,16 @@ import React from 'react'; type LoadingMessageProps = { - bubbleSize: 'sm' | 'md' | 'lg'; + bubbleSize: 'md' | 'lg'; + metadataSize: 'sm' | 'md'; outgoing?: boolean; }; -const LoadingMessage = ({ bubbleSize, outgoing = false }: LoadingMessageProps) => ( +const LoadingMessage = ({ + bubbleSize, + metadataSize, + outgoing = false, +}: LoadingMessageProps) => (
-
-
-
-
+
); @@ -50,9 +54,9 @@ export const LoadingChannel = () => (
- - - + + +
diff --git a/src/components/Loading/__tests__/__snapshots__/LoadingChannel.test.js.snap b/src/components/Loading/__tests__/__snapshots__/LoadingChannel.test.js.snap index 492168960..a5b5753e5 100644 --- a/src/components/Loading/__tests__/__snapshots__/LoadingChannel.test.js.snap +++ b/src/components/Loading/__tests__/__snapshots__/LoadingChannel.test.js.snap @@ -41,15 +41,8 @@ exports[`LoadingChannel should render component with default props 1`] = ` class="str-chat__loading-channel-message-bubble str-chat__loading-channel-message-bubble--lg" />
-
-
-
+ class="str-chat__loading-channel-message-metadata str-chat__loading-channel-message-metadata--md" + />
diff --git a/src/components/Threads/ThreadList/ThreadList.tsx b/src/components/Threads/ThreadList/ThreadList.tsx index 422d11754..4bcf4eec8 100644 --- a/src/components/Threads/ThreadList/ThreadList.tsx +++ b/src/components/Threads/ThreadList/ThreadList.tsx @@ -9,11 +9,15 @@ import { ThreadListItem as DefaultThreadListItem } from './ThreadListItem'; import { ThreadListEmptyPlaceholder as DefaultThreadListEmptyPlaceholder } from './ThreadListEmptyPlaceholder'; import { ThreadListUnseenThreadsBanner as DefaultThreadListUnseenThreadsBanner } from './ThreadListUnseenThreadsBanner'; import { ThreadListLoadingIndicator as DefaultThreadListLoadingIndicator } from './ThreadListLoadingIndicator'; +import { LoadingChannels } from '../../Loading'; import { useChatContext, useComponentContext } from '../../../context'; import { useStateStore } from '../../../store'; import { ThreadListHeader } from './ThreadListHeader'; -const selector = (nextValue: ThreadManagerState) => ({ threads: nextValue.threads }); +const selector = (nextValue: ThreadManagerState) => ({ + isLoading: nextValue.pagination.isLoading, + threads: nextValue.threads, +}); const computeItemKey: ComputeItemKey = (_, item) => item.id; @@ -52,10 +56,25 @@ export const ThreadList = ({ virtuosoProps }: ThreadListProps) => { ThreadListLoadingIndicator = DefaultThreadListLoadingIndicator, ThreadListUnseenThreadsBanner = DefaultThreadListUnseenThreadsBanner, } = useComponentContext(); - const { threads } = useStateStore(client.threads.state, selector); + const { isLoading, threads } = useStateStore(client.threads.state, selector); useThreadList(); + if (isLoading && !threads.length) { + return ( +
+ +
+ +
+
+ ); + } + return (
({ + Virtuoso: (props) => { + mockVirtuoso(props); + return
; + }, +})); + +jest.mock('../../../../context', () => ({ + useChatContext: () => mockUseChatContext(), + useComponentContext: () => mockUseComponentContext(), +})); + +jest.mock('../../../../store', () => ({ + useStateStore: (...args) => mockUseStateStore(...args), +})); + +jest.mock('../../../Loading', () => ({ + LoadingChannels: () =>
, +})); + +jest.mock('../ThreadListHeader', () => ({ + ThreadListHeader: () =>
, +})); + +jest.mock('../ThreadListUnseenThreadsBanner', () => ({ + ThreadListUnseenThreadsBanner: () =>
, +})); + +describe('ThreadList', () => { + const mockClient = { + threads: { + activate: jest.fn(), + deactivate: jest.fn(), + loadNextPage: jest.fn(), + state: {}, + }, + }; + + beforeEach(() => { + mockUseChatContext.mockReturnValue({ client: mockClient, navOpen: true }); + mockUseComponentContext.mockReturnValue({}); + mockUseStateStore.mockReturnValue({ isLoading: false, threads: [] }); + }); + + afterEach(() => { + cleanup(); + jest.clearAllMocks(); + }); + + it('renders channel-list skeletons during the initial thread list load', () => { + mockUseStateStore.mockReturnValue({ isLoading: true, threads: [] }); + + render(); + + expect(screen.getByTestId('thread-list-header')).toBeInTheDocument(); + expect(screen.getByTestId('loading-channels')).toBeInTheDocument(); + expect(screen.queryByTestId('thread-list-unseen-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('virtuoso')).not.toBeInTheDocument(); + }); + + it('renders the virtualized thread list once the initial load is complete', () => { + mockUseStateStore.mockReturnValue({ + isLoading: false, + threads: [{ id: 'thread-1' }], + }); + + render(); + + expect(screen.getByTestId('thread-list-header')).toBeInTheDocument(); + expect(screen.getByTestId('thread-list-unseen-banner')).toBeInTheDocument(); + expect(screen.getByTestId('virtuoso')).toBeInTheDocument(); + expect(screen.queryByTestId('loading-channels')).not.toBeInTheDocument(); + }); +});