diff --git a/.cursor/skills/dev-patterns/SKILL.md b/.cursor/skills/dev-patterns/SKILL.md index 38b1e52d5f..0cd2930922 100644 --- a/.cursor/skills/dev-patterns/SKILL.md +++ b/.cursor/skills/dev-patterns/SKILL.md @@ -15,6 +15,7 @@ Apply when generating or modifying UI code in this repo. - **Location:** `src/components//styling/`. - **Required:** Each component styling folder has an `index.scss`. - **Registration:** Each `src/components//styling/index.scss` is imported in `src/styling/index.scss` with an alias. +- **Specificity:** Each component has own `.scss` file in the `src/components//styling` folder **Import order in `src/styling/index.scss`:** @@ -35,6 +36,17 @@ Apply when generating or modifying UI code in this repo. Source: `.ai/DEV_PATTERNS.md`. +## Translating quantities (plurals) + +- **Use plural suffixes only:** `_one`, `_other`, and `_few`, `_many` where the locale requires them. +- **Do not** add a standalone key (e.g. `"{{count}} new messages"`). Only add quantified variants: `"{{count}} new messages_one"`, `"{{count}} new messages_other"`, etc. +- Follow existing patterns in `src/i18n/` (e.g. `{{count}} unread_one`, `unreadMessagesSeparatorText_other`). +- Locale plural rules (CLDR): `en`, `de`, `nl`, `tr`, `hi`, `ko`, `ja` use `_one` + `_other`; `es`, `fr`, `it`, `pt` add `_many`; `ru` uses `_one`, `_few`, `_many`, `_other`. + ## Imports When importing from 'stream-chat' library, always import by library name (from 'stream-chat'), not relative path (from '..path/to/from 'stream-chat-js/src'). + +## React components + +Try to avoid inline `style` attribute and prefer adding styles to `.scss` files. diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index 5655773678..032537e5fb 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -203,7 +203,6 @@ const App = () => { - {/**/} & { + /** Visual variant mapping to design tokens */ + variant?: BadgeVariant; + /** Size preset (typography and padding) */ + size?: BadgeSize; +}; + +/** + * Compact pill/circle badge for counts and labels. + * Uses design tokens: --badge-bg-*, --badge-text-*, --badge-border. + */ +export const Badge = ({ + children, + className, + size = 'md', + variant = 'default', + ...spanProps +}: BadgeProps) => ( + + {children} + +); diff --git a/src/components/Badge/__tests__/Badge.test.tsx b/src/components/Badge/__tests__/Badge.test.tsx new file mode 100644 index 0000000000..0df4dd9386 --- /dev/null +++ b/src/components/Badge/__tests__/Badge.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { Badge } from '../Badge'; + +describe('Badge', () => { + it('renders children', () => { + render(17); + expect(screen.getByText('17')).toBeInTheDocument(); + }); + + it('applies variant class', () => { + const { container } = render(1); + expect(container.firstChild).toHaveClass('str-chat__badge--variant-primary'); + }); + + it('applies size class', () => { + const { container } = render(1); + expect(container.firstChild).toHaveClass('str-chat__badge--size-md'); + }); + + it('passes data-testid', () => { + render(99); + expect(screen.getByTestId('custom-badge')).toHaveTextContent('99'); + }); + + it('merges className', () => { + const { container } = render( + 5, + ); + expect(container.firstChild).toHaveClass('str-chat__badge'); + expect(container.firstChild).toHaveClass('str-chat__jump-to-latest__unread-count'); + }); +}); diff --git a/src/components/Badge/index.ts b/src/components/Badge/index.ts new file mode 100644 index 0000000000..9c8edca28a --- /dev/null +++ b/src/components/Badge/index.ts @@ -0,0 +1 @@ +export * from './Badge'; diff --git a/src/components/Badge/styling/Badge.scss b/src/components/Badge/styling/Badge.scss new file mode 100644 index 0000000000..9740739101 --- /dev/null +++ b/src/components/Badge/styling/Badge.scss @@ -0,0 +1,71 @@ +// Badge: compact pill for counts/labels. Design tokens: badge-bg-*, badge-text-*, badge-border. +// Figma: Badge Notification (scroll-to-bottom unread count) + +.str-chat__badge { + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: var(--typography-font-weight-bold); + border-radius: var(--radius-max); + line-height: 1; + border-style: solid; +} + +// Variants: map to design tokens +.str-chat__badge--variant-default { + background: var(--badge-bg-default); + color: var(--badge-text); + border-color: var(--badge-border); +} + +.str-chat__badge--variant-primary { + background: var(--badge-bg-primary); + color: var(--badge-text-on-accent); + border-color: var(--badge-border); +} + +.str-chat__badge--variant-error { + background: var(--badge-bg-error); + color: var(--badge-text-on-accent); + border-color: var(--badge-border); +} + +.str-chat__badge--variant-neutral { + background: var(--badge-bg-neutral); + color: var(--badge-text-on-accent); + border-color: var(--badge-border); +} + +.str-chat__badge--variant-inverse { + background: var(--badge-bg-inverse); + color: var(--badge-text-inverse); + border-color: var(--badge-border); +} + +// Sizes: caption-numeric equivalents (sm/12px, md/14px, lg/16px) +.str-chat__badge--size-sm { + font-size: var(--typography-font-size-xxs); + min-width: 16px; + min-height: 16px; + padding-inline: var(--spacing-xxxs); + border-width: 1px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14); +} + +.str-chat__badge--size-md { + font-size: var(--typography-font-size-xs); + min-width: 20px; + min-height: 20px; + padding-inline: var(--spacing-xxs); + border-width: 2px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14); +} + +.str-chat__badge--size-lg { + font-size: var(--typography-font-size-sm); + min-width: 24px; + min-height: 24px; + padding-inline: var(--spacing-xs); + border-width: 2px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14); +} diff --git a/src/components/Badge/styling/index.scss b/src/components/Badge/styling/index.scss new file mode 100644 index 0000000000..529a29758b --- /dev/null +++ b/src/components/Badge/styling/index.scss @@ -0,0 +1 @@ +@use 'Badge'; diff --git a/src/components/Button/styling/Button.scss b/src/components/Button/styling/Button.scss index 68082c5a42..4ba95aa313 100644 --- a/src/components/Button/styling/Button.scss +++ b/src/components/Button/styling/Button.scss @@ -5,7 +5,7 @@ @include utils.button-reset; position: relative; /* creates positioning context for pseudo ::after overlay */ overflow: hidden; - + white-space: nowrap; cursor: pointer; display: flex; @@ -111,6 +111,10 @@ box-shadow: var(--light-elevation-2); } + &::after { + border-radius: inherit; + } + &.str-chat__button--size-lg { padding-block: var(--button-padding-y-lg); padding-inline: var(--button-padding-x-with-label-lg); diff --git a/src/components/Button/styling/index.scss b/src/components/Button/styling/index.scss new file mode 100644 index 0000000000..7514bf3556 --- /dev/null +++ b/src/components/Button/styling/index.scss @@ -0,0 +1 @@ +@use 'Button'; diff --git a/src/components/DateSeparator/DateSeparator.tsx b/src/components/DateSeparator/DateSeparator.tsx index 148426bd54..f7bf166c56 100644 --- a/src/components/DateSeparator/DateSeparator.tsx +++ b/src/components/DateSeparator/DateSeparator.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import React from 'react'; import { useTranslationContext } from '../../context/TranslationContext'; @@ -6,8 +7,12 @@ import { getDateString } from '../../i18n/utils'; import type { TimestampFormatterOptions } from '../../i18n/types'; export type DateSeparatorProps = TimestampFormatterOptions & { + /** Optional className for the root element */ + className?: string; /** The date to format */ date: Date; + /** When true, applies floating positioning (fixed at top when scrolling) */ + floating?: boolean; /** Override the default formatting of the date. This is a function that has access to the original date object. */ formatDate?: (date: Date) => string; // todo: position and unread are not necessary anymore @@ -20,7 +25,9 @@ export type DateSeparatorProps = TimestampFormatterOptions & { const UnMemoizedDateSeparator = (props: DateSeparatorProps) => { const { calendar, + className, date: messageCreatedAt, + floating, formatDate, ...restTimestampFormatterOptions } = props; @@ -38,7 +45,15 @@ const UnMemoizedDateSeparator = (props: DateSeparatorProps) => { }); return ( -
+
{formattedDate}
); diff --git a/src/components/DateSeparator/styling/DateSeparator.scss b/src/components/DateSeparator/styling/DateSeparator.scss index 2b83e822fa..ed29b9ef57 100644 --- a/src/components/DateSeparator/styling/DateSeparator.scss +++ b/src/components/DateSeparator/styling/DateSeparator.scss @@ -12,6 +12,17 @@ --str-chat__date-separator-box-shadow: none; } +.str-chat__date-separator--floating { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1; + display: flex; + justify-content: center; + pointer-events: none; +} + .str-chat__date-separator { @include utils.component-layer-overrides('date-separator'); display: flex; diff --git a/src/components/Message/styling/UnreadMessageNotification.scss b/src/components/Message/styling/UnreadMessageNotification.scss deleted file mode 100644 index 8104752f33..0000000000 --- a/src/components/Message/styling/UnreadMessageNotification.scss +++ /dev/null @@ -1,35 +0,0 @@ -.str-chat__unread-messages-notification { - --str-chat-icon-color: var(--str-chat__grey50); - background-color: var(--str-chat__text-low-emphasis-color); - border-radius: 1.125rem; - position: absolute; - top: 0.75rem; - z-index: 2; - display: flex; - align-items: center; - overflow: clip; - - button { - padding-block: var(--str-chat__spacing-2); - height: 100%; - width: 100%; - white-space: nowrap; - cursor: pointer; - color: var(--str-chat__grey50); - border: none; - background-color: transparent; - } - - button:first-of-type { - padding-inline: 0.75rem 0.375rem; - font: var(--str-chat__caption-text); - } - - button:last-of-type { - padding-inline: 0.375rem 0.75rem; - - svg { - width: 0.875rem; - } - } -} diff --git a/src/components/Message/styling/UnreadMessagesSeparator.scss b/src/components/Message/styling/UnreadMessagesSeparator.scss deleted file mode 100644 index 356e1cebfc..0000000000 --- a/src/components/Message/styling/UnreadMessagesSeparator.scss +++ /dev/null @@ -1,15 +0,0 @@ -.str-chat__unread-messages-separator-wrapper { - padding-block: var(--str-chat__spacing-2); - - .str-chat__unread-messages-separator { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - padding: var(--str-chat__spacing-2); - background-color: var(--str-chat__secondary-surface-color); - color: var(--str-chat__text-low-emphasis-color); - text-transform: uppercase; - font: var(--str-chat__caption-strong-text); - } -} diff --git a/src/components/Message/styling/index.scss b/src/components/Message/styling/index.scss index dcf4ae3d8f..026268bbdf 100644 --- a/src/components/Message/styling/index.scss +++ b/src/components/Message/styling/index.scss @@ -7,6 +7,4 @@ @use 'MessageTranslationIndicator'; @use 'QuotedMessage'; @use 'ReminderNotification'; -@use 'UnreadMessageNotification'; -@use 'UnreadMessagesSeparator'; @use 'MessageRepliesCountButton'; diff --git a/src/components/MessageList/FloatingDateSeparator.tsx b/src/components/MessageList/FloatingDateSeparator.tsx new file mode 100644 index 0000000000..cb8ff21452 --- /dev/null +++ b/src/components/MessageList/FloatingDateSeparator.tsx @@ -0,0 +1,68 @@ +import React, { useLayoutEffect } from 'react'; +import type { RefObject } from 'react'; + +import { DateSeparator as DefaultDateSeparator } from '../DateSeparator'; +import { useComponentContext } from '../../context/ComponentContext'; +import { useFloatingDateSeparatorMessageList } from './hooks/MessageList'; +import { useFloatingDateSeparator } from './hooks/VirtualizedMessageList'; +import type { RenderedMessage } from './utils'; + +type BaseProps = { + disableDateSeparator: boolean; + processedMessages: RenderedMessage[]; +}; + +export type FloatingDateSeparatorProps = BaseProps & + ( + | { + /** For VirtualizedMessageList: ref the parent calls with visible items */ + itemsRenderedRef: RefObject<((rendered: RenderedMessage[]) => void) | null>; + } + | { + /** For MessageList: scroll container to query DOM for date separators */ + listElement: HTMLDivElement | null; + } + ); + +/** + * Renders a floating date separator when the user has scrolled past the in-flow date. + * State is internal so MessageList/VirtualizedMessageList do not re-render when it changes. + * Use itemsRenderedRef for Virtuoso, listElement for non-virtualized MessageList. + */ +export const FloatingDateSeparator = (props: FloatingDateSeparatorProps) => { + const { DateSeparator = DefaultDateSeparator } = useComponentContext( + 'FloatingDateSeparator', + ); + const { disableDateSeparator, processedMessages } = props; + + const listElement = 'listElement' in props ? props.listElement : null; + const useDomMode = listElement != null; + + const virtuosoResult = useFloatingDateSeparator({ + disableDateSeparator, + processedMessages, + }); + const domResult = useFloatingDateSeparatorMessageList({ + disableDateSeparator, + listElement, + processedMessages, + }); + + const floatingDate = useDomMode ? domResult.floatingDate : virtuosoResult.floatingDate; + const showFloatingDate = useDomMode + ? domResult.showFloatingDate + : virtuosoResult.showFloatingDate; + + const itemsRenderedRef = 'itemsRenderedRef' in props ? props.itemsRenderedRef : null; + useLayoutEffect(() => { + if (!itemsRenderedRef) return; + itemsRenderedRef.current = virtuosoResult.onItemsRendered; + return () => { + itemsRenderedRef.current = null; + }; + }, [itemsRenderedRef, virtuosoResult.onItemsRendered]); + + if (!showFloatingDate || !floatingDate || !DateSeparator) return null; + + return ; +}; diff --git a/src/components/MessageList/MessageList.tsx b/src/components/MessageList/MessageList.tsx index 750eb0b1d3..d1cf2823f7 100644 --- a/src/components/MessageList/MessageList.tsx +++ b/src/components/MessageList/MessageList.tsx @@ -9,8 +9,8 @@ import { } from './hooks/MessageList'; import { useMarkRead } from './hooks/useMarkRead'; -import { MessageNotification as DefaultMessageNotification } from './MessageNotification'; import { MessageListNotifications as DefaultMessageListNotifications } from './MessageListNotifications'; +import { NewMessageNotification as DefaultNewMessageNotification } from './NewMessageNotification'; import { UnreadMessagesNotification as DefaultUnreadMessagesNotification } from './UnreadMessagesNotification'; import type { ChannelActionContextValue } from '../../context/ChannelActionContext'; @@ -29,6 +29,7 @@ import { defaultPinPermissions, MESSAGE_ACTIONS } from '../Message/utils'; import { TypingIndicator as DefaultTypingIndicator } from '../TypingIndicator'; import { MessageListMainPanel as DefaultMessageListMainPanel } from './MessageListMainPanel'; +import { FloatingDateSeparator } from './FloatingDateSeparator'; import { defaultRenderMessages } from './renderMessages'; import { useStableId } from '../UtilityComponents/useStableId'; @@ -43,6 +44,7 @@ import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE, } from '../../constants/limits'; import { useLastOwnMessage } from './hooks/useLastOwnMessage'; +import { ScrollToLatestMessageButton } from './ScrollToLatestMessageButton'; type MessageListWithContextProps = Omit< ChannelStateContextValue, @@ -97,7 +99,7 @@ const MessageListWithContext = (props: MessageListWithContextProps) => { MessageListMainPanel = DefaultMessageListMainPanel, MessageListNotifications = DefaultMessageListNotifications, MessageListWrapper = 'ul', - MessageNotification = DefaultMessageNotification, + NewMessageNotification = DefaultNewMessageNotification, TypingIndicator = DefaultTypingIndicator, UnreadMessagesNotification = DefaultUnreadMessagesNotification, } = useComponentContext('MessageList'); @@ -119,6 +121,7 @@ const MessageListWithContext = (props: MessageListWithContextProps) => { const { show: showUnreadMessagesNotification } = useUnreadMessagesNotification({ isMessageListScrolledToBottom, + listElement, showAlways: !!showUnreadNotificationAlways, unreadCount: channelUnreadUiState?.unread_messages, }); @@ -246,6 +249,11 @@ const MessageListWithContext = (props: MessageListWithContextProps) => { unreadCount={channelUnreadUiState?.unread_messages} /> )} +
{ )}
+ + - + ); diff --git a/src/components/MessageList/MessageListNotifications.tsx b/src/components/MessageList/MessageListNotifications.tsx index 2abc0106cb..1ade8f98c0 100644 --- a/src/components/MessageList/MessageListNotifications.tsx +++ b/src/components/MessageList/MessageListNotifications.tsx @@ -5,7 +5,6 @@ import { CustomNotification } from './CustomNotification'; import { useTranslationContext } from '../../context/TranslationContext'; import { useNotifications } from '../Notifications/hooks/useNotifications'; -import type { MessageNotificationProps } from './MessageNotification'; import type { ChannelNotifications } from '../../context/ChannelStateContext'; const ClientNotifications = () => { @@ -28,29 +27,11 @@ const ClientNotifications = () => { }; export type MessageListNotificationsProps = { - hasNewMessages: boolean; - isMessageListScrolledToBottom: boolean; - isNotAtLatestMessageSet: boolean; - MessageNotification: React.ComponentType; notifications: ChannelNotifications; - scrollToBottom: () => void; - threadList?: boolean; - unreadCount?: number; }; export const MessageListNotifications = (props: MessageListNotificationsProps) => { - const { - hasNewMessages, - isMessageListScrolledToBottom, - isNotAtLatestMessageSet, - MessageNotification, - notifications, - scrollToBottom, - threadList, - unreadCount, - } = props; - - const { t } = useTranslationContext('MessageListNotifications'); + const { notifications } = props; return (
@@ -61,15 +42,6 @@ export const MessageListNotifications = (props: MessageListNotificationsProps) = ))} - - {isNotAtLatestMessageSet ? t('Latest Messages') : t('New Messages!')} -
); }; diff --git a/src/components/MessageList/MessageNotification.tsx b/src/components/MessageList/MessageNotification.tsx deleted file mode 100644 index a777a7ae3d..0000000000 --- a/src/components/MessageList/MessageNotification.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { PropsWithChildren } from 'react'; -import React from 'react'; - -export type MessageNotificationProps = PropsWithChildren<{ - /** button click event handler */ - onClick: React.MouseEventHandler; - /** signals whether the message list is considered (below a threshold) to be scrolled to the bottom. Prop used only by [ScrollToBottomButton](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/ScrollToBottomButton.tsx) */ - isMessageListScrolledToBottom?: boolean; - /** Whether or not to show notification. Prop used only by [MessageNotification](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/MessageNotification.tsx) */ - showNotification?: boolean; - /** informs the component whether it is rendered inside a thread message list */ - threadList?: boolean; - /** */ - unreadCount?: number; -}>; - -const UnMemoizedMessageNotification = (props: MessageNotificationProps) => { - const { children, onClick, showNotification = true } = props; - - if (!showNotification) return null; - - return ( - - ); -}; - -export const MessageNotification = React.memo( - UnMemoizedMessageNotification, -) as typeof UnMemoizedMessageNotification; diff --git a/src/components/MessageList/NewMessageNotification.tsx b/src/components/MessageList/NewMessageNotification.tsx new file mode 100644 index 0000000000..1169e00b8b --- /dev/null +++ b/src/components/MessageList/NewMessageNotification.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { useComponentContext } from '../../context/ComponentContext'; +import { useTranslationContext } from '../../context/TranslationContext'; + +export type NewMessageNotificationProps = { + /** Pre-computed label text (passed to custom override) */ + label?: string; + /** Controls the notification display */ + showNotification?: boolean; + /** Number of new messages since user scrolled up (optional; when provided shows "{{count}} new messages") */ + newMessageCount?: number; +}; + +const UnMemoizedNewMessageNotification = (props: NewMessageNotificationProps) => { + const { newMessageCount = 0, showNotification } = props; + + const { t } = useTranslationContext(); + + const { NewMessageNotification: CustomNewMessageNotification } = useComponentContext(); + + if (!showNotification) return null; + + const label = + newMessageCount > 0 + ? t('{{count}} new messages', { count: newMessageCount }) + : t('New Messages!'); + + if (CustomNewMessageNotification) { + return ; + } + + return ( +
+
+ {label} +
+
+ ); +}; + +export const NewMessageNotification = React.memo( + UnMemoizedNewMessageNotification, +) as typeof UnMemoizedNewMessageNotification; diff --git a/src/components/MessageList/ScrollToBottomButton.tsx b/src/components/MessageList/ScrollToLatestMessageButton.tsx similarity index 55% rename from src/components/MessageList/ScrollToBottomButton.tsx rename to src/components/MessageList/ScrollToLatestMessageButton.tsx index d8508867c8..25cb47687c 100644 --- a/src/components/MessageList/ScrollToBottomButton.tsx +++ b/src/components/MessageList/ScrollToLatestMessageButton.tsx @@ -1,20 +1,30 @@ import React, { useEffect, useState } from 'react'; import clsx from 'clsx'; -import { ArrowDown } from './icons'; - import { useChannelStateContext, useChatContext } from '../../context'; import type { Event } from 'stream-chat'; -import type { MessageNotificationProps } from './MessageNotification'; +import { Badge } from '../Badge'; +import { Button } from '../Button'; +import { IconArrowDown } from '../Icons'; + +type ScrollToLatestMessageButtonProps = { + /** When true, user has jumped to an older message set and newer messages can be loaded */ + isNotAtLatestMessageSet?: boolean; + isMessageListScrolledToBottom?: boolean; + onClick: React.MouseEventHandler; + threadList?: boolean; +}; -const UnMemoizedScrollToBottomButton = ( - props: Pick< - MessageNotificationProps, - 'isMessageListScrolledToBottom' | 'onClick' | 'threadList' - >, +const UnMemoizedScrollToLatestMessageButton = ( + props: ScrollToLatestMessageButtonProps, ) => { - const { isMessageListScrolledToBottom, onClick, threadList } = props; + const { + isMessageListScrolledToBottom, + isNotAtLatestMessageSet = false, + onClick, + threadList, + } = props; const { channel: activeChannel, client } = useChatContext(); const { thread } = useChannelStateContext(); @@ -54,8 +64,15 @@ const UnMemoizedScrollToBottomButton = ( return () => { client.off(observedEvent, handleEvent); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeChannel, isMessageListScrolledToBottom, observedEvent, replyCount, thread]); + }, [ + activeChannel, + client, + isMessageListScrolledToBottom, + observedEvent, + replyCount, + thread, + threadList, + ]); useEffect(() => { if (isMessageListScrolledToBottom) { @@ -64,36 +81,38 @@ const UnMemoizedScrollToBottomButton = ( } }, [isMessageListScrolledToBottom, thread]); - if (isMessageListScrolledToBottom) return null; + if (isMessageListScrolledToBottom && !isNotAtLatestMessageSet) return null; return (
- + + + {countUnread > 0 && ( + + {countUnread} + + )}
); }; -export const ScrollToBottomButton = React.memo( - UnMemoizedScrollToBottomButton, -) as typeof UnMemoizedScrollToBottomButton; +export const ScrollToLatestMessageButton = React.memo( + UnMemoizedScrollToLatestMessageButton, +) as typeof UnMemoizedScrollToLatestMessageButton; diff --git a/src/components/MessageList/UnreadMessagesNotification.tsx b/src/components/MessageList/UnreadMessagesNotification.tsx index 0e4536820a..c2e675dab1 100644 --- a/src/components/MessageList/UnreadMessagesNotification.tsx +++ b/src/components/MessageList/UnreadMessagesNotification.tsx @@ -1,6 +1,8 @@ import React from 'react'; -import { CloseIcon } from './icons'; import { useChannelActionContext, useTranslationContext } from '../../context'; +import { Button } from '../Button'; +import { IconArrowUp, IconCrossMedium } from '../Icons'; +import clsx from 'clsx'; export type UnreadMessagesNotificationProps = { /** @@ -8,7 +10,7 @@ export type UnreadMessagesNotificationProps = { */ queryMessageLimit?: number; /** - * Configuration parameter to determine, whether the unread count is to be shown on the component. Disabled by default. + * Configuration parameter to determine, whether the unread count is to be shown on the component. Enabled by default. */ showCount?: boolean; /** @@ -19,27 +21,34 @@ export type UnreadMessagesNotificationProps = { export const UnreadMessagesNotification = ({ queryMessageLimit, - showCount, + showCount = true, unreadCount, }: UnreadMessagesNotificationProps) => { - const { jumpToFirstUnreadMessage, markRead } = useChannelActionContext( - 'UnreadMessagesNotification', - ); + const { jumpToFirstUnreadMessage, markRead } = useChannelActionContext(); const { t } = useTranslationContext('UnreadMessagesNotification'); return (
- - + +
); }; diff --git a/src/components/MessageList/UnreadMessagesSeparator.tsx b/src/components/MessageList/UnreadMessagesSeparator.tsx index 0d13e6c54e..245481ed39 100644 --- a/src/components/MessageList/UnreadMessagesSeparator.tsx +++ b/src/components/MessageList/UnreadMessagesSeparator.tsx @@ -1,11 +1,14 @@ import React from 'react'; -import { useTranslationContext } from '../../context'; +import { useChannelActionContext, useTranslationContext } from '../../context'; +import { Button } from '../Button'; +import clsx from 'clsx'; +import { IconCrossMedium } from '../Icons'; export const UNREAD_MESSAGE_SEPARATOR_CLASS = 'str-chat__unread-messages-separator'; export type UnreadMessagesSeparatorProps = { /** - * Configuration parameter to determine, whether the unread count is to be shown on the component. Disabled by default. + * Configuration parameter to determine, whether the unread count is to be shown on the component. Enabled by default. */ showCount?: boolean; /** @@ -15,18 +18,32 @@ export type UnreadMessagesSeparatorProps = { }; export const UnreadMessagesSeparator = ({ - showCount, + showCount = true, unreadCount, }: UnreadMessagesSeparatorProps) => { const { t } = useTranslationContext('UnreadMessagesSeparator'); + const { markRead } = useChannelActionContext(); return (
- {unreadCount && showCount - ? t('unreadMessagesSeparatorText', { count: unreadCount }) - : t('Unread messages')} +
+ {unreadCount && showCount + ? t('{{count}} unread', { count: unreadCount }) + : t('Unread messages')} +
+
); }; diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx index 8c63b16adc..476e7627a4 100644 --- a/src/components/MessageList/VirtualizedMessageList.tsx +++ b/src/components/MessageList/VirtualizedMessageList.tsx @@ -1,5 +1,7 @@ import type { RefObject } from 'react'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { FloatingDateSeparator } from './FloatingDateSeparator'; import type { ComputeItemKey, ScrollSeekConfiguration, @@ -22,8 +24,8 @@ import { } from './hooks/VirtualizedMessageList'; import { useMarkRead } from './hooks/useMarkRead'; -import { MessageNotification as DefaultMessageNotification } from './MessageNotification'; import { MessageListNotifications as DefaultMessageListNotifications } from './MessageListNotifications'; +import { NewMessageNotification as DefaultNewMessageNotification } from './NewMessageNotification'; import { MessageListMainPanel as DefaultMessageListMainPanel } from './MessageListMainPanel'; import type { GroupStyle, ProcessMessagesParams, RenderedMessage } from './utils'; import { getGroupStyles, getLastReceived, processMessages } from './utils'; @@ -40,7 +42,10 @@ import { messageRenderer, } from './VirtualizedMessageListComponents'; -import { UnreadMessagesSeparator as DefaultUnreadMessagesSeparator } from '../MessageList'; +import { + UnreadMessagesSeparator as DefaultUnreadMessagesSeparator, + ScrollToLatestMessageButton, +} from '../MessageList'; import { DateSeparator as DefaultDateSeparator } from '../DateSeparator'; import { EventComponent as DefaultMessageSystem } from '../EventComponent'; @@ -243,8 +248,8 @@ const VirtualizedMessageListWithContext = ( GiphyPreviewMessage = DefaultGiphyPreviewMessage, MessageListMainPanel = DefaultMessageListMainPanel, MessageListNotifications = DefaultMessageListNotifications, - MessageNotification = DefaultMessageNotification, MessageSystem = DefaultMessageSystem, + NewMessageNotification = DefaultNewMessageNotification, TypingIndicator, UnreadMessagesNotification = DefaultUnreadMessagesNotification, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, @@ -303,6 +308,16 @@ const VirtualizedMessageListWithContext = ( client.userID, ]); + /** + * This indirection via a ref is needed because Virtuoso’s itemsRendered is set at render time, + * while FloatingDateSeparator lives in a child component that may run its hooks in a different order. + * Using a ref lets the parent provide the callback to Virtuoso while the child keeps ownership + * of the actual handler implementation. + */ + const floatingDateItemsRenderedRef = useRef< + ((rendered: RenderedMessage[]) => void) | null + >(null); + const lastOwnMessage = useLastOwnMessage({ messages, ownUserId: client.user?.id }); // get the mapping of own messages to array of users who read them @@ -405,7 +420,13 @@ const VirtualizedMessageListWithContext = ( const handleItemsRendered = useMemo( () => - makeItemsRenderedHandler([toggleShowUnreadMessagesNotification], processedMessages), + makeItemsRenderedHandler( + [ + toggleShowUnreadMessagesNotification, + (rendered) => floatingDateItemsRenderedRef.current?.(rendered), + ], + processedMessages, + ), [processedMessages, toggleShowUnreadMessagesNotification], ); const followOutput = (isAtBottom: boolean) => { @@ -479,6 +500,11 @@ const VirtualizedMessageListWithContext = ( customClasses?.virtualizedMessageList || 'str-chat__virtual-list' } > + atBottomStateChange={atBottomStateChange} atBottomThreshold={100} @@ -546,20 +572,21 @@ const VirtualizedMessageListWithContext = ( {...(scrollSeekPlaceHolder ? { scrollSeek: scrollSeekPlaceHolder } : {})} {...(defaultItemHeight ? { defaultItemHeight } : {})} /> +
+ {TypingIndicator && } - + {giphyPreviewMessage && } diff --git a/src/components/MessageList/__tests__/MessageList.test.js b/src/components/MessageList/__tests__/MessageList.test.js index d4dba32932..bc3a81b754 100644 --- a/src/components/MessageList/__tests__/MessageList.test.js +++ b/src/components/MessageList/__tests__/MessageList.test.js @@ -26,8 +26,6 @@ import { WithComponents, } from '../../../context'; import { EmptyStateIndicator as EmptyStateIndicatorMock } from '../../EmptyStateIndicator'; -import { ScrollToBottomButton } from '../ScrollToBottomButton'; -import { MessageListNotifications } from '../MessageListNotifications'; import { mockedApiResponse } from '../../../mock-builders/api/utils'; import { nanoid } from 'nanoid'; @@ -795,14 +793,12 @@ describe('MessageList', () => { }); }); - describe('ScrollToBottomButton', () => { - const BUTTON_TEST_ID = 'message-notification'; + describe('ScrollToLatestMessageButton and NewMessageNotification', () => { + const NEW_MESSAGE_NOTIFICATION_TEST_ID = 'message-notification'; + const SCROLL_TO_LATEST_MESSAGE_TEST_ID = 'scroll-to-latest-message-button'; const NEW_MESSAGE_COUNTER_TEST_ID = 'unread-message-notification-counter'; - const MockMessageListNotifications = (props) => ( - - ); - it('does not reflect the channel unread UI state', async () => { + it('ScrollToLatestMessageButton does not reflect the channel unread UI state', async () => { const { channels: [channel], client, @@ -814,15 +810,15 @@ describe('MessageList', () => { channel, }, chatClient: client, - components: { - MessageListNotifications: MockMessageListNotifications, - MessageNotification: ScrollToBottomButton, - }, msgListProps: { messages }, }); }); - expect(screen.queryByTestId(BUTTON_TEST_ID)).toBeInTheDocument(); + const scrollButton = screen.queryByTestId(SCROLL_TO_LATEST_MESSAGE_TEST_ID); + const newMessageNotification = screen.queryByTestId( + NEW_MESSAGE_NOTIFICATION_TEST_ID, + ); + expect(scrollButton || newMessageNotification).toBeTruthy(); expect(screen.queryByTestId(NEW_MESSAGE_COUNTER_TEST_ID)).not.toBeInTheDocument(); await act(() => { @@ -832,7 +828,7 @@ describe('MessageList', () => { expect(screen.queryByTestId(NEW_MESSAGE_COUNTER_TEST_ID)).not.toBeInTheDocument(); }); - it('does not reflect the channel unread state in a thread', async () => { + it('ScrollToLatestMessageButton does not reflect the channel unread state in a thread', async () => { const { channels: [channel], client, @@ -844,15 +840,15 @@ describe('MessageList', () => { channel, }, chatClient: client, - components: { - MessageListNotifications: MockMessageListNotifications, - MessageNotification: ScrollToBottomButton, - }, msgListProps: { messages, threadList: true }, }); }); - expect(screen.queryByTestId(BUTTON_TEST_ID)).toBeInTheDocument(); + const scrollButton = screen.queryByTestId(SCROLL_TO_LATEST_MESSAGE_TEST_ID); + const newMessageNotification = screen.queryByTestId( + NEW_MESSAGE_NOTIFICATION_TEST_ID, + ); + expect(scrollButton || newMessageNotification).toBeTruthy(); expect(screen.queryByTestId(NEW_MESSAGE_COUNTER_TEST_ID)).not.toBeInTheDocument(); await act(() => { diff --git a/src/components/MessageList/__tests__/ScrollToBottomButton.test.js b/src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.js similarity index 86% rename from src/components/MessageList/__tests__/ScrollToBottomButton.test.js rename to src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.js index 10b18bb074..bbf0005b67 100644 --- a/src/components/MessageList/__tests__/ScrollToBottomButton.test.js +++ b/src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { ScrollToBottomButton } from '../ScrollToBottomButton'; +import { ScrollToLatestMessageButton } from '../ScrollToLatestMessageButton'; import { ChannelStateProvider, ChatProvider } from '../../../context'; import { createClientWithChannel, @@ -11,7 +11,7 @@ import { generateMessage, } from '../../../mock-builders'; -const BUTTON_TEST_ID = 'message-notification'; +const BUTTON_TEST_ID = 'scroll-to-latest-message-button'; const NEW_MESSAGE_COUNTER_TEST_ID = 'unread-message-notification-counter'; const mainList = 'the main message list'; @@ -42,7 +42,7 @@ const dispatchMessageEvents = ({ channel, client, newMessage, parentMsg, user }) describe.each([ [mainList, threadList], [threadList, mainList], -])('ScrollToBottomButton in %s', (containerMsgList, otherMsgList) => { +])('ScrollToLatestMessageButton in %s', (containerMsgList, otherMsgList) => { beforeEach(async () => { const result = await createClientWithChannel(); client = result.client; @@ -64,7 +64,7 @@ describe.each([ const { container } = render( - + , ); @@ -75,7 +75,10 @@ describe.each([ render( - + , ); @@ -86,7 +89,10 @@ describe.each([ render( - + , ); @@ -105,7 +111,7 @@ describe.each([ render( - + , ); @@ -131,7 +137,10 @@ describe.each([ render( - + , ); @@ -158,7 +167,10 @@ describe.each([ render( - + , ); @@ -187,7 +199,10 @@ describe.each([ render( - + , ); @@ -217,7 +232,10 @@ describe.each([ render( - + , ); @@ -241,7 +259,10 @@ describe.each([ render( - + , ); @@ -280,13 +301,13 @@ describe.each([
-
- { + const [state, setState] = useState<{ date: Date | null; visible: boolean }>({ + date: null, + visible: false, + }); + + const update = useCallback(() => { + if (disableDateSeparator || !listElement || processedMessages.length === 0) { + setState({ date: null, visible: false }); + return; + } + + const separators = listElement.querySelectorAll(DATE_SEPARATOR_SELECTOR); + if (separators.length === 0) { + setState({ date: null, visible: false }); + return; + } + + const containerRect = listElement.getBoundingClientRect(); + let bestDate: Date | null = null; + let bestBottom = -Infinity; + let anyVisible = false; + + for (const el of separators) { + const rect = el.getBoundingClientRect(); + const dataDate = el.getAttribute('data-date'); + if (!dataDate) continue; + + const isAboveViewport = rect.bottom < containerRect.top; + const isVisible = + rect.top < containerRect.bottom && rect.bottom > containerRect.top; + + if (isVisible) { + anyVisible = true; + } + + if (isAboveViewport && rect.bottom > bestBottom) { + bestBottom = rect.bottom; + const d = new Date(dataDate); + if (!isNaN(d.getTime())) bestDate = d; + } + } + + setState({ + date: anyVisible ? null : bestDate, + visible: !anyVisible && bestDate !== null, + }); + }, [disableDateSeparator, listElement, processedMessages]); + + useEffect(() => { + if (!listElement) return; + + const throttled = throttle(update, THROTTLE_MS); + + throttled(); + listElement.addEventListener('scroll', throttled); + const resizeObserver = new ResizeObserver(throttled); + resizeObserver.observe(listElement); + + return () => { + listElement.removeEventListener('scroll', throttled); + resizeObserver.disconnect(); + throttled.cancel(); + }; + }, [listElement, update]); + + return { + floatingDate: state.date, + showFloatingDate: state.visible, + }; +}; diff --git a/src/components/MessageList/hooks/MessageList/useUnreadMessagesNotification.ts b/src/components/MessageList/hooks/MessageList/useUnreadMessagesNotification.ts index 466e360999..17dafb7b15 100644 --- a/src/components/MessageList/hooks/MessageList/useUnreadMessagesNotification.ts +++ b/src/components/MessageList/hooks/MessageList/useUnreadMessagesNotification.ts @@ -3,9 +3,13 @@ import { useEffect, useRef, useState } from 'react'; import { MESSAGE_LIST_MAIN_PANEL_CLASS } from '../../MessageListMainPanel'; import { UNREAD_MESSAGE_SEPARATOR_CLASS } from '../../UnreadMessagesSeparator'; -const targetScrolledAboveVisibleContainerArea = (element: Element) => { +const targetScrolledAboveVisibleContainerArea = ( + element: Element, + container?: Element, +) => { const { bottom: targetBottom } = element.getBoundingClientRect(); - return targetBottom < 0; + const containerTop = container?.getBoundingClientRect().top ?? 0; + return targetBottom < containerTop; }; const targetScrolledBelowVisibleContainerArea = ( @@ -13,11 +17,13 @@ const targetScrolledBelowVisibleContainerArea = ( container: Element, ) => { const { top: targetTop } = element.getBoundingClientRect(); - const { top: containerBottom } = container.getBoundingClientRect(); + const { bottom: containerBottom } = container.getBoundingClientRect(); return targetTop > containerBottom; }; export type UseUnreadMessagesNotificationParams = { + /** Scroll container (the element with overflow that actually scrolls). When provided, used as IntersectionObserver root and for initial visibility. */ + listElement: HTMLDivElement | null; isMessageListScrolledToBottom: boolean; showAlways: boolean; unreadCount?: number; @@ -25,6 +31,7 @@ export type UseUnreadMessagesNotificationParams = { export const useUnreadMessagesNotification = ({ isMessageListScrolledToBottom, + listElement, showAlways, unreadCount, }: UseUnreadMessagesNotificationParams) => { @@ -39,8 +46,20 @@ export const useUnreadMessagesNotification = ({ return; } - const [msgListPanel] = document.getElementsByClassName(MESSAGE_LIST_MAIN_PANEL_CLASS); - if (!msgListPanel) return; + const scrollRoot = listElement ?? null; + if (!scrollRoot) { + const [msgListPanel] = document.getElementsByClassName( + MESSAGE_LIST_MAIN_PANEL_CLASS, + ); + if (!msgListPanel) return; + const [observedTarget] = document.getElementsByClassName( + UNREAD_MESSAGE_SEPARATOR_CLASS, + ); + if (!observedTarget) { + setShow(true); + } + return; + } const [observedTarget] = document.getElementsByClassName( UNREAD_MESSAGE_SEPARATOR_CLASS, @@ -50,11 +69,13 @@ export const useUnreadMessagesNotification = ({ return; } - const scrolledBelowSeparator = - targetScrolledAboveVisibleContainerArea(observedTarget); + const scrolledBelowSeparator = targetScrolledAboveVisibleContainerArea( + observedTarget, + scrollRoot, + ); const scrolledAboveSeparator = targetScrolledBelowVisibleContainerArea( observedTarget, - msgListPanel, + scrollRoot, ); setShow( @@ -66,16 +87,18 @@ export const useUnreadMessagesNotification = ({ const observer = new IntersectionObserver( (elements) => { if (!elements.length) return; - const { boundingClientRect, isIntersecting } = elements[0]; + const entry = elements[0]; + const { boundingClientRect, isIntersecting, rootBounds } = entry; if (isIntersecting) { setShow(false); return; } - const separatorIsAboveContainerTop = boundingClientRect.bottom < 0; + const rootTop = rootBounds?.top ?? 0; + const separatorIsAboveContainerTop = boundingClientRect.bottom < rootTop; setShow(showAlways || separatorIsAboveContainerTop); isScrolledAboveTargetTop.current = separatorIsAboveContainerTop; }, - { root: msgListPanel }, + { root: scrollRoot }, ); observer.observe(observedTarget); @@ -84,6 +107,7 @@ export const useUnreadMessagesNotification = ({ }; }, [ intersectionObserverIsSupported, + listElement, isMessageListScrolledToBottom, messages, showAlways, diff --git a/src/components/MessageList/hooks/VirtualizedMessageList/__tests__/useFloatingDateSeparator.test.ts b/src/components/MessageList/hooks/VirtualizedMessageList/__tests__/useFloatingDateSeparator.test.ts new file mode 100644 index 0000000000..049d07e186 --- /dev/null +++ b/src/components/MessageList/hooks/VirtualizedMessageList/__tests__/useFloatingDateSeparator.test.ts @@ -0,0 +1,101 @@ +import { act, renderHook } from '@testing-library/react'; + +import { useFloatingDateSeparator } from '../useFloatingDateSeparator'; +import type { RenderedMessage } from '../../../utils'; +import { CUSTOM_MESSAGE_TYPE } from '../../../../../constants/messageTypes'; + +const makeDateSeparator = (date: Date): RenderedMessage => + ({ + customType: CUSTOM_MESSAGE_TYPE.date, + date, + id: `date-${date.toISOString()}`, + type: 'date', + unread: false, + }) as unknown as RenderedMessage; + +const makeMessage = (id: string, createdAt: Date): RenderedMessage => + ({ + created_at: createdAt, + id, + type: 'regular', + user: { id: 'user' }, + }) as unknown as RenderedMessage; + +describe('useFloatingDateSeparator', () => { + const jan1 = new Date('2025-01-01T12:00:00Z'); + const jan2 = new Date('2025-01-02T12:00:00Z'); + const processedMessages: RenderedMessage[] = [ + makeDateSeparator(jan1), + makeMessage('m1', jan1), + makeMessage('m2', jan1), + makeDateSeparator(jan2), + makeMessage('m3', jan2), + ]; + + it('returns visible false when date separators are disabled', () => { + const { result } = renderHook(() => + useFloatingDateSeparator({ + disableDateSeparator: true, + processedMessages, + }), + ); + + act(() => { + result.current.onItemsRendered([makeMessage('m2', jan1)]); + }); + + expect(result.current.showFloatingDate).toBe(false); + expect(result.current.floatingDate).toBeNull(); + }); + + it('hides floating when first visible item is a date separator', () => { + const { result } = renderHook(() => + useFloatingDateSeparator({ + disableDateSeparator: false, + processedMessages, + }), + ); + + act(() => { + result.current.onItemsRendered([makeDateSeparator(jan1), makeMessage('m1', jan1)]); + }); + + expect(result.current.showFloatingDate).toBe(false); + expect(result.current.floatingDate).toBeNull(); + }); + + it('shows floating with correct date when first visible is a message', () => { + const { result } = renderHook(() => + useFloatingDateSeparator({ + disableDateSeparator: false, + processedMessages, + }), + ); + + act(() => { + result.current.onItemsRendered([makeMessage('m2', jan1), makeMessage('m3', jan2)]); + }); + + expect(result.current.showFloatingDate).toBe(true); + expect(result.current.floatingDate).toEqual(jan1); + }); + + it('hides when any date separator is in visible set', () => { + const { result } = renderHook(() => + useFloatingDateSeparator({ + disableDateSeparator: false, + processedMessages, + }), + ); + + act(() => { + result.current.onItemsRendered([ + makeMessage('m2', jan1), + makeDateSeparator(jan2), + makeMessage('m3', jan2), + ]); + }); + + expect(result.current.showFloatingDate).toBe(false); + }); +}); diff --git a/src/components/MessageList/hooks/VirtualizedMessageList/index.ts b/src/components/MessageList/hooks/VirtualizedMessageList/index.ts index 2574aa4c0c..99d4112e16 100644 --- a/src/components/MessageList/hooks/VirtualizedMessageList/index.ts +++ b/src/components/MessageList/hooks/VirtualizedMessageList/index.ts @@ -1,3 +1,4 @@ +export * from './useFloatingDateSeparator'; export * from './useGiphyPreview'; export * from './useMessageSetKey'; export * from './useNewMessageNotification'; diff --git a/src/components/MessageList/hooks/VirtualizedMessageList/useFloatingDateSeparator.ts b/src/components/MessageList/hooks/VirtualizedMessageList/useFloatingDateSeparator.ts new file mode 100644 index 0000000000..2730a8db81 --- /dev/null +++ b/src/components/MessageList/hooks/VirtualizedMessageList/useFloatingDateSeparator.ts @@ -0,0 +1,114 @@ +import { useCallback, useState } from 'react'; + +import type { RenderedMessage } from '../../utils'; +import { isDateSeparatorMessage, isIntroMessage } from '../../utils'; +import type { LocalMessage } from 'stream-chat'; + +export type UseFloatingDateSeparatorParams = { + disableDateSeparator: boolean; + processedMessages: RenderedMessage[]; +}; + +export type UseFloatingDateSeparatorResult = { + floatingDate: Date | null; + onItemsRendered: (rendered: RenderedMessage[]) => void; + showFloatingDate: boolean; +}; + +/** + * Returns the date to show in the floating date separator based on currently visible messages. + * When the first visible item is a message (not a date separator), we've scrolled past its + * date separator — find that separator's date. + */ +function getFloatingDateForFirstMessage( + firstMessage: RenderedMessage, + processedMessages: RenderedMessage[], + firstMessageIndex: number, +): Date | null { + if (isIntroMessage(firstMessage)) return null; + + // Walk backwards to find the last date separator before this message + for (let i = firstMessageIndex - 1; i >= 0; i -= 1) { + const item = processedMessages[i]; + if (isDateSeparatorMessage(item)) { + return item.date; + } + } + + // No preceding date separator; use message's created_at + const msg = firstMessage as LocalMessage; + const created = msg.created_at; + if (created) { + const d = new Date(created); + return isNaN(d.getTime()) ? null : d; + } + return null; +} + +/** + * Controls when to show the floating date separator (Slack-like: fixed at top when scrolling). + * Show when no in-flow date separator is visible and we've scrolled past one. + */ +const HIDDEN_STATE = { date: null, visible: false } as const; + +export const useFloatingDateSeparator = ({ + disableDateSeparator, + processedMessages, +}: UseFloatingDateSeparatorParams): UseFloatingDateSeparatorResult => { + const [state, setState] = useState<{ + date: Date | null; + visible: boolean; + }>(HIDDEN_STATE); + + const onItemsRendered = useCallback( + (rendered: RenderedMessage[]) => { + if (disableDateSeparator || processedMessages.length === 0) { + setState(HIDDEN_STATE); + return; + } + + const valid = rendered.filter((m): m is RenderedMessage => m != null); + if (valid.length === 0) { + setState(HIDDEN_STATE); + return; + } + + const first = valid[0]; + + // If first visible item is a date separator, it's in view — hide floating + if (isDateSeparatorMessage(first)) { + setState(HIDDEN_STATE); + return; + } + + // Check if any date separator is visible — if so, hide floating + const hasVisibleDateSeparator = valid.some(isDateSeparatorMessage); + if (hasVisibleDateSeparator) { + setState(HIDDEN_STATE); + return; + } + + // First visible is a message; find its date + const firstIndex = processedMessages.findIndex((m) => m.id === first.id); + const date = + firstIndex >= 0 + ? getFloatingDateForFirstMessage(first, processedMessages, firstIndex) + : null; + + const visible = date !== null; + setState((prev) => { + const prevTime = prev.date?.getTime() ?? null; + const nextTime = date?.getTime() ?? null; + if (prev.visible === visible && prevTime === nextTime) return prev; + return { date, visible }; + }); + }, + [disableDateSeparator, processedMessages], + ); + + return { + floatingDate: state.date, + onItemsRendered, + showFloatingDate: !!state.date && state.visible, + }; +}; diff --git a/src/components/MessageList/icons.tsx b/src/components/MessageList/icons.tsx deleted file mode 100644 index 39f56c048b..0000000000 --- a/src/components/MessageList/icons.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; - -interface ArrowProps { - className?: string; - color?: string; -} - -export const ArrowUp = ({ className, color }: ArrowProps) => ( - - - -); - -export const ArrowDown = ({ className, color }: ArrowProps) => ( - - - -); - -export const CloseIcon = () => ( - - - -); diff --git a/src/components/MessageList/index.ts b/src/components/MessageList/index.ts index 745c2d593e..985ddbec2c 100644 --- a/src/components/MessageList/index.ts +++ b/src/components/MessageList/index.ts @@ -2,8 +2,8 @@ export * from './ConnectionStatus'; // TODO: export this under its own folder export * from './GiphyPreviewMessage'; export * from './MessageList'; export * from './MessageListNotifications'; -export * from './MessageNotification'; -export * from './ScrollToBottomButton'; +export * from './NewMessageNotification'; +export * from './ScrollToLatestMessageButton'; export * from './UnreadMessagesNotification'; export * from './UnreadMessagesSeparator'; export * from './VirtualizedMessageList'; diff --git a/src/components/MessageList/styling/MessageList.scss b/src/components/MessageList/styling/MessageList.scss index 446ca30499..35979a3917 100644 --- a/src/components/MessageList/styling/MessageList.scss +++ b/src/components/MessageList/styling/MessageList.scss @@ -10,10 +10,18 @@ align-items: center; } +.str-chat__message-list-main-panel { + --str-chat__message-list-scroll-max-width: calc( + var(--str-chat__message-composer-max-width) + + var(--str-chat__message-composer-padding) + ); +} + .str-chat__message-list { @include utils.scrollable; display: flex; justify-content: center; + position: relative; overscroll-behavior: none; width: 100%; height: 100%; @@ -23,10 +31,7 @@ .str-chat__message-list-scroll { @include utils.message-list-spacing; /* Max container 800px, 16px padding → 768px readable content; matches composer width + padding */ - max-width: calc( - var(--str-chat__message-composer-max-width) + - var(--str-chat__message-composer-padding) - ); + max-width: var(--str-chat__message-list-scroll-max-width); .str-chat__ul { list-style: none; padding: 0; @@ -52,20 +57,6 @@ } } -.str-chat__jump-to-latest-message { - position: absolute; - inset-block-end: var(--str-chat__spacing-4); - inset-inline-end: var(--str-chat__spacing-2); - z-index: 2; - - .str-chat__jump-to-latest-unread-count { - position: absolute; - padding: var(--str-chat__spacing-0_5) var(--str-chat__spacing-2); - left: 50%; - transform: translateX(-50%) translateY(-100%); - } -} - .str-chat__main-panel { .str-chat__ul { .str-chat__li:first-of-type { @@ -104,80 +95,9 @@ /* Right (left in RTL layout) border of the component */ --str-chat__message-list-border-inline-end: none; - /* The border radius used for the borders of the jump to latest message button */ - --str-chat__jump-to-latest-message-border-radius: var( - --str-chat__circle-fab-border-radius - ); - - /* The text/icon color of the jump to latest message button */ - --str-chat__jump-to-latest-message-color: var(--str-chat__circle-fab-color); - - /* The background color of the jump to latest message button */ - --str-chat__jump-to-latest-message-background-color: var( - --str-chat__circle-fab-background-color - ); - - /* The background color of the jump to latest message button in pressed state */ - --str-chat__jump-to-latest-message-pressed-background-color: var( - --str-chat__circle-fab-pressed-background-color - ); - - /* Box shadow applied to the jump to latest message button */ - --str-chat__jump-to-latest-message-box-shadow: var(--str-chat__circle-fab-box-shadow); - - /* Top border of the jump to latest message button */ - --str-chat__jump-to-latest-message-border-block-start: var( - --str-chat__circle-fab-border-block-start - ); - - /* Bottom border of the jump to latest message button */ - --str-chat__jump-to-latest-message-border-block-end: var( - --str-chat__circle-fab-border-block-end - ); - - /* Left (right in RTL layout) border of the jump to latest message button */ - --str-chat__jump-to-latest-message-border-inline-start: var( - --str-chat__circle-fab-border-inline-start - ); - - /* Right (left in RTL layout) border of the jump to latest message button */ - --str-chat__jump-to-latest-message-border-inline-end: var( - --str-chat__circle-fab-border-inline-end - ); - - /* The background color of the unread messages count on the jump to latest message button */ - --str-chat__jump-to-latest-message-unread-count-background-color: var( - --str-chat__jump-to-latest-message-color - ); - - /* The text/icon color of the unread messages count on the jump to latest message button */ - --str-chat__jump-to-latest-message-unread-count-color: var( - --str-chat__jump-to-latest-message-background-color - ); - /* The color used for displaying thread replies and thread separator at the start of a thread */ --str-chat__thread-head-start-color: var(--str-chat__text-low-emphasis-color); /* The color used for the separator below the first message in a thread */ --str-chat__thread-head-start-border-block-end-color: var(--str-chat__surface-color); } - -.str-chat__jump-to-latest-message { - --str-chat-icon-color: var( - --str-chat__jump-to-latest-message-unread-count-background-color - ); - - .str-chat__circle-fab { - @include utils.component-layer-overrides('jump-to-latest-message'); - @include utils.circle-fab-overrides('jump-to-latest-message'); - - .str-chat__jump-to-latest-unread-count { - background-color: var( - --str-chat__jump-to-latest-message-unread-count-background-color - ); - color: var(--str-chat__jump-to-latest-message-unread-count-color); - border-radius: var(--str-chat__jump-to-latest-message-border-radius); - font: var(--str-chat__caption-text); - } - } -} diff --git a/src/components/MessageList/styling/NewMessageNotification.scss b/src/components/MessageList/styling/NewMessageNotification.scss new file mode 100644 index 0000000000..459878fa10 --- /dev/null +++ b/src/components/MessageList/styling/NewMessageNotification.scss @@ -0,0 +1,22 @@ +@use '../../../styling/utils'; + +.str-chat__new-message-notification { + position: absolute; + inset-block-end: 16px; + inset-inline-start: 0; + inset-inline-end: 0; + z-index: 2; + display: flex; + justify-content: center; + + .str-chat__message-notification__label { + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-xxs) var(--spacing-sm); + background-color: var(--background-core-surface-subtle); + border-radius: var(--radius-max); + font: var(--str-chat__metadata-emphasis-text); + color: var(--chat-text-system); + } +} diff --git a/src/components/MessageList/styling/ScrollToLatestMessageButton.scss b/src/components/MessageList/styling/ScrollToLatestMessageButton.scss new file mode 100644 index 0000000000..3c32908c9e --- /dev/null +++ b/src/components/MessageList/styling/ScrollToLatestMessageButton.scss @@ -0,0 +1,32 @@ +.str-chat__jump-to-latest-message { + height: 40px; + width: 40px; + position: absolute; + inset-block-end: var(--spacing-md); + inset-inline-end: max( + var(--spacing-md), + calc((100% - var(--str-chat__message-list-scroll-max-width)) / 2 + var(--spacing-md)) + ); + z-index: 2; + border-radius: var(--radius-max); + background-color: var(--background-elevation-elevation-1); + // todo - we ned to have the shadows in variables that are supported in light and dark mode too + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.05), + 0 2px 4px 0 rgba(0, 0, 0, 0.12), + 0 6px 16px 0 rgba(0, 0, 0, 0.06); + + .str-chat__jump-to-latest-message__button { + height: 40px; + width: 40px; + position: static; + } + + .str-chat__jump-to-latest__unread-count { + position: absolute; + inset-block-end: 100%; + margin-block-end: var(--str-chat__spacing-1); + right: -15%; + top: -15%; + } +} diff --git a/src/components/MessageList/styling/UnreadMessageNotification.scss b/src/components/MessageList/styling/UnreadMessageNotification.scss new file mode 100644 index 0000000000..d8c4c49926 --- /dev/null +++ b/src/components/MessageList/styling/UnreadMessageNotification.scss @@ -0,0 +1,48 @@ +.str-chat { + .str-chat__unread-messages-notification { + display: flex; + align-items: center; + background: var(--background-elevation-elevation-1); + border-radius: var(--button-radius-lg); + border: 1px solid var(--button-secondary-border); + /* shadow/web/light/elevation-2 */ + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.05), + 0 2px 4px 0 rgba(0, 0, 0, 0.12), + 0 6px 16px 0 rgba(0, 0, 0, 0.06); + position: absolute; + top: 40px; + z-index: 2; + overflow: clip; + + &.str-chat__unread-messages-notification--with-count button { + text-transform: lowercase; + } + + button.str-chat__button--secondary, + button.button.str-chat__button--outline { + border: none; + } + + button:first-of-type { + display: flex; + align-items: center; + gap: var(--spacing-xs); + border-radius: var(--button-radius-lg) 0 0 var(--button-radius-lg); + padding: var(--button-padding-y-md) var(--spacing-xxs) var(--button-padding-y-md) + var(--button-padding-x-with-label-md); + font: var(--str-chat__caption-emphasis-text); + } + + button:last-of-type { + border-radius: 0 var(--button-radius-lg) var(--button-radius-lg) 0; + padding: var(--button-padding-y-md) var(--button-padding-x-with-label-md) + var(--button-padding-y-md) var(--spacing-xxs); + + svg { + height: 16px; + width: 16px; + } + } + } +} diff --git a/src/components/MessageList/styling/UnreadMessagesSeparator.scss b/src/components/MessageList/styling/UnreadMessagesSeparator.scss new file mode 100644 index 0000000000..f1306fde7a --- /dev/null +++ b/src/components/MessageList/styling/UnreadMessagesSeparator.scss @@ -0,0 +1,35 @@ +.str-chat__unread-messages-separator-wrapper { + padding-block: var(--spacing-xs); + display: flex; + justify-content: center; + + .str-chat__unread-messages-separator { + display: flex; + align-items: center; + width: fit-content; + padding: var(--spacing-xxs) var(--spacing-xs); + background: var(--background-elevation-elevation-1); + border-radius: var(--button-radius-lg); + border: 1px solid var(--button-secondary-border); + overflow: clip; + + .str-chat__unread-messages-separator__text { + padding-inline: var(--spacing-xs) var(--spacing-xxs); + border-radius: var(--button-radius-lg) 0 0 var(--button-radius-lg); + font: var(--str-chat__caption-emphasis-text); + text-transform: lowercase; + } + + button.str-chat__button--secondary, + button.button.str-chat__button--outline { + border: none; + } + + button { + svg { + height: 16px; + width: 16px; + } + } + } +} diff --git a/src/components/MessageList/styling/VirtualizedMessageList.scss b/src/components/MessageList/styling/VirtualizedMessageList.scss index 34d438c9e3..b82d6433e2 100644 --- a/src/components/MessageList/styling/VirtualizedMessageList.scss +++ b/src/components/MessageList/styling/VirtualizedMessageList.scss @@ -5,6 +5,10 @@ // Layout .str-chat__virtual-list { @include utils.scrollable; + --str-chat__message-list-scroll-max-width: calc( + var(--str-chat__message-composer-max-width) + + var(--str-chat__message-composer-padding) + ); position: relative; flex: 1; -webkit-overflow-scrolling: touch; /* enable smooth scrolling on ios */ diff --git a/src/components/MessageList/styling/index.scss b/src/components/MessageList/styling/index.scss index 71982d251d..927c8d2039 100644 --- a/src/components/MessageList/styling/index.scss +++ b/src/components/MessageList/styling/index.scss @@ -1,2 +1,6 @@ @use 'MessageList'; +@use 'NewMessageNotification'; +@use 'ScrollToLatestMessageButton'; +@use 'UnreadMessageNotification'; +@use 'UnreadMessagesSeparator'; @use 'VirtualizedMessageList'; diff --git a/src/components/Reactions/ReactionsList.tsx b/src/components/Reactions/ReactionsList.tsx index 7f9d436732..66ff15478d 100644 --- a/src/components/Reactions/ReactionsList.tsx +++ b/src/components/Reactions/ReactionsList.tsx @@ -96,7 +96,8 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => { null, ); const { t } = useTranslationContext('ReactionsList'); - const { MessageReactionsDetail = DefaultMessageReactionsDetail } = useComponentContext(); + const { MessageReactionsDetail = DefaultMessageReactionsDetail } = + useComponentContext(); const { isMyMessage, message } = useMessageContext('ReactionsList'); const divRef = useRef>(null); diff --git a/src/components/Reactions/hooks/useProcessReactions.tsx b/src/components/Reactions/hooks/useProcessReactions.tsx index ef14033189..47e8c8c3da 100644 --- a/src/components/Reactions/hooks/useProcessReactions.tsx +++ b/src/components/Reactions/hooks/useProcessReactions.tsx @@ -129,7 +129,8 @@ export const useProcessReactions = (params: UseProcessReactionsParams) => { const hasReactions = existingReactions.length > 0; const totalReactionCount = useMemo( - () => Object.values(reactionGroups ?? {}).reduce((total, { count }) => total + count, 0), + () => + Object.values(reactionGroups ?? {}).reduce((total, { count }) => total + count, 0), [reactionGroups], ); diff --git a/src/components/index.ts b/src/components/index.ts index e8605f9424..59e71bd5f5 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,6 +2,7 @@ export * from './AIStateIndicator'; export * from './Attachment'; export * from './AudioPlayback'; export * from './Avatar'; +export * from './Badge'; export * from './Button'; export * from './Channel'; export * from './ChannelHeader'; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 8c31ea8f75..1466e3b2d5 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -22,7 +22,6 @@ import { type MessageDeletedProps, type MessageInputProps, type MessageListNotificationsProps, - type MessageNotificationProps, type MessageProps, type MessageReactionsDetailProps, type MessageRepliesCountButtonProps, @@ -31,6 +30,7 @@ import { type MessageUIComponentProps, type ModalGalleryProps, type ModalProps, + type NewMessageNotificationProps, type PinIndicatorProps, type PollCreationDialogProps, type PollOptionSelectorProps, @@ -148,8 +148,8 @@ export type ComponentContextValue = { MessageListMainPanel?: React.ComponentType; /** Custom UI component that displays message and connection status notifications in the `MessageList`, defaults to and accepts same props as [DefaultMessageListNotifications](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/MessageListNotifications.tsx) */ MessageListNotifications?: React.ComponentType; - /** Custom UI component to display a notification when scrolled up the list and new messages arrive, defaults to and accepts same props as [MessageNotification](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/MessageNotification.tsx) */ - MessageNotification?: React.ComponentType; + /** Custom UI component to display a notification when scrolled up the list and new messages arrive, defaults to and accepts same props as [NewMessageNotification](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/NewMessageNotification.tsx) */ + NewMessageNotification?: React.ComponentType; /** Custom UI component to display message replies, defaults to and accepts same props as: [MessageRepliesCountButton](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageRepliesCountButton.tsx) */ MessageRepliesCountButton?: React.ComponentType; /** Custom UI component to display message delivery status, defaults to and accepts same props as: [MessageStatus](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageStatus.tsx) */ diff --git a/src/i18n/de.json b/src/i18n/de.json index b8ecd4c229..9ef64503fc 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -5,8 +5,8 @@ "{{ count }} files_other": "{{ count }} Dateien", "{{ count }} photos_one": "{{ count }} Foto", "{{ count }} photos_other": "{{ count }} Fotos", - "{{ count }} reactions_one": "{{ count }} reactions", - "{{ count }} reactions_other": "{{ count }} reactions", + "{{ count }} reactions_one": "{{ count }} Reaktion", + "{{ count }} reactions_other": "{{ count }} Reaktionen", "{{ count }} videos_one": "{{ count }} Video", "{{ count }} videos_other": "{{ count }} Videos", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} und {{ secondUser }}", @@ -18,6 +18,8 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} und {{ user }} tippen...", "{{ users }} and more are typing...": "{{ users }} und mehr tippen...", "{{ watcherCount }} online": "{{ watcherCount }} online", + "{{count}} new messages_one": "{{count}} neue Nachricht", + "{{count}} new messages_other": "{{count}} neue Nachrichten", "{{count}} unread_one": "{{count}} ungelesen", "{{count}} unread_other": "{{count}} ungelesen", "{{count}} votes_one": "{{count}} Stimme", @@ -223,7 +225,6 @@ "language/vi": "Vietnamesisch", "language/zh": "Chinesisch (Vereinfacht)", "language/zh-TW": "Chinesisch (Traditionell)", - "Latest Messages": "Neueste Nachrichten", "Let others add options": "Andere Optionen hinzufügen lassen", "Limit votes per person": "Stimmen pro Person begrenzen", "live": "live", @@ -333,7 +334,7 @@ "Stop sharing": "Teilen beenden", "Submit": "Absenden", "Suggest an option": "Eine Option vorschlagen", - "Tap to remove": "Tap to remove", + "Tap to remove": "Tippen zum Entfernen", "Thinking...": "Denken...", "this content could not be displayed": "Dieser Inhalt konnte nicht angezeigt werden", "This field cannot be empty or contain only spaces": "Dieses Feld darf nicht leer sein oder nur Leerzeichen enthalten", @@ -367,8 +368,6 @@ "unmute-command-description": "Stummschaltung eines Benutzers aufheben", "Unpin": "Anheftung aufheben", "Unread messages": "Ungelesene Nachrichten", - "unreadMessagesSeparatorText_one": "1 ungelesene Nachricht", - "unreadMessagesSeparatorText_other": "{{count}} ungelesene Nachrichten", "Unsupported attachment": "Nicht unterstützter Anhang", "unsupported file type": "Nicht unterstützter Dateityp", "Update your comment": "Ihren Kommentar aktualisieren", diff --git a/src/i18n/en.json b/src/i18n/en.json index 30b99e5dcf..7faa0b27ac 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -18,6 +18,8 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} and {{ user }} are typing...", "{{ users }} and more are typing...": "{{ users }} and more are typing...", "{{ watcherCount }} online": "{{ watcherCount }} online", + "{{count}} new messages_one": "{{count}} new message", + "{{count}} new messages_other": "{{count}} new messages", "{{count}} unread_one": "{{count}} unread", "{{count}} unread_other": "{{count}} unread", "{{count}} votes_one": "{{count}} vote", @@ -223,7 +225,6 @@ "language/vi": "Vietnamese", "language/zh": "Chinese (Simplified)", "language/zh-TW": "Chinese (Traditional)", - "Latest Messages": "Latest Messages", "Let others add options": "Let others add options", "Limit votes per person": "Limit votes per person", "live": "live", @@ -367,8 +368,6 @@ "unmute-command-description": "Unmute a user", "Unpin": "Unpin", "Unread messages": "Unread messages", - "unreadMessagesSeparatorText_one": "1 unread message", - "unreadMessagesSeparatorText_other": "{{count}} unread messages", "Unsupported attachment": "Unsupported attachment", "unsupported file type": "unsupported file type", "Update your comment": "Update your comment", diff --git a/src/i18n/es.json b/src/i18n/es.json index eb37336a3f..1e48f15b82 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -7,9 +7,9 @@ "{{ count }} photos_one": "{{ count }} foto", "{{ count }} photos_many": "{{ count }} fotos", "{{ count }} photos_other": "{{ count }} fotos", - "{{ count }} reactions_one": "", - "{{ count }} reactions_many": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} reacción", + "{{ count }} reactions_many": "{{ count }} reacciones", + "{{ count }} reactions_other": "{{ count }} reacciones", "{{ count }} videos_one": "{{ count }} vídeo", "{{ count }} videos_many": "{{ count }} vídeos", "{{ count }} videos_other": "{{ count }} vídeos", @@ -22,6 +22,9 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} y {{ user }} están escribiendo...", "{{ users }} and more are typing...": "{{ users }} y más están escribiendo...", "{{ watcherCount }} online": "{{ watcherCount }} en línea", + "{{count}} new messages_one": "{{count}} nuevo mensaje", + "{{count}} new messages_many": "{{count}} nuevos mensajes", + "{{count}} new messages_other": "{{count}} nuevos mensajes", "{{count}} unread_one": "{{count}} no leído", "{{count}} unread_many": "{{count}} no leídos", "{{count}} unread_other": "{{count}} no leídos", @@ -229,7 +232,6 @@ "language/vi": "Vietnamita", "language/zh": "Chino (simplificado)", "language/zh-TW": "Chino (tradicional)", - "Latest Messages": "Últimos mensajes", "Let others add options": "Permitir que otros añadan opciones", "Limit votes per person": "Limitar votos por persona", "live": "En vivo", @@ -343,7 +345,7 @@ "Stop sharing": "Dejar de compartir", "Submit": "Enviar", "Suggest an option": "Sugerir una opción", - "Tap to remove": "", + "Tap to remove": "Toca para quitar", "Thinking...": "Pensando...", "this content could not be displayed": "Este contenido no se pudo mostrar", "This field cannot be empty or contain only spaces": "Este campo no puede estar vacío o contener solo espacios", @@ -377,9 +379,6 @@ "unmute-command-description": "Desactivar el silencio de un usuario", "Unpin": "Desfijar", "Unread messages": "Mensajes no leídos", - "unreadMessagesSeparatorText_one": "1 mensaje no leído", - "unreadMessagesSeparatorText_many": "{{count}} mensajes no leídos", - "unreadMessagesSeparatorText_other": "{{count}} mensajes no leídos", "Unsupported attachment": "Adjunto no compatible", "unsupported file type": "tipo de archivo no compatible", "Update your comment": "Actualizar tu comentario", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 4ca18d3c7f..08b04344e8 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -7,9 +7,9 @@ "{{ count }} photos_one": "{{ count }} photo", "{{ count }} photos_many": "{{ count }} photos", "{{ count }} photos_other": "{{ count }} photos", - "{{ count }} reactions_one": "", - "{{ count }} reactions_many": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} réaction", + "{{ count }} reactions_many": "{{ count }} réactions", + "{{ count }} reactions_other": "{{ count }} réactions", "{{ count }} videos_one": "{{ count }} vidéo", "{{ count }} videos_many": "{{ count }} vidéos", "{{ count }} videos_other": "{{ count }} vidéos", @@ -22,6 +22,9 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} et {{ user }} sont en train d'écrire...", "{{ users }} and more are typing...": "{{ users }} et plus sont en train d'écrire...", "{{ watcherCount }} online": "{{ watcherCount }} en ligne", + "{{count}} new messages_one": "{{count}} nouveau message", + "{{count}} new messages_many": "{{count}} nouveaux messages", + "{{count}} new messages_other": "{{count}} nouveaux messages", "{{count}} unread_one": "{{count}} non lu", "{{count}} unread_many": "{{count}} non lus", "{{count}} unread_other": "{{count}} non lus", @@ -229,7 +232,6 @@ "language/vi": "Vietnamien", "language/zh": "Chinois (simplifié)", "language/zh-TW": "Chinois (traditionnel)", - "Latest Messages": "Derniers messages", "Let others add options": "Permettre à d'autres d'ajouter des options", "Limit votes per person": "Limiter les votes par personne", "live": "en direct", @@ -343,7 +345,7 @@ "Stop sharing": "Arrêter de partager", "Submit": "Envoyer", "Suggest an option": "Suggérer une option", - "Tap to remove": "", + "Tap to remove": "Appuyez pour retirer", "Thinking...": "Réflexion...", "this content could not be displayed": "ce contenu n'a pas pu être affiché", "This field cannot be empty or contain only spaces": "Ce champ ne peut pas être vide ou contenir uniquement des espaces", @@ -377,9 +379,6 @@ "unmute-command-description": "Démuter un utilisateur", "Unpin": "Détacher", "Unread messages": "Messages non lus", - "unreadMessagesSeparatorText_one": "1 message non lu", - "unreadMessagesSeparatorText_many": "{{count}} messages non lus", - "unreadMessagesSeparatorText_other": "{{count}} messages non lus", "Unsupported attachment": "Pièce jointe non prise en charge", "unsupported file type": "type de fichier non pris en charge", "Update your comment": "Mettre à jour votre commentaire", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index 5efb57ad67..34f7b5e6e9 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -5,8 +5,8 @@ "{{ count }} files_other": "{{ count }} फ़ाइलें", "{{ count }} photos_one": "{{ count }} फ़ोटो", "{{ count }} photos_other": "{{ count }} फ़ोटो", - "{{ count }} reactions_one": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} प्रतिक्रिया", + "{{ count }} reactions_other": "{{ count }} प्रतिक्रियाएं", "{{ count }} videos_one": "{{ count }} वीडियो", "{{ count }} videos_other": "{{ count }} वीडियो", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} और {{ secondUser }}", @@ -18,6 +18,8 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} और {{ user }} टाइप कर रहे हैं...", "{{ users }} and more are typing...": "{{ users }} और अधिक टाइप कर रहे हैं...", "{{ watcherCount }} online": "{{ watcherCount }} ऑनलाइन", + "{{count}} new messages_one": "{{count}} नया संदेश", + "{{count}} new messages_other": "{{count}} नए संदेश", "{{count}} unread_one": "{{count}} अपठित", "{{count}} unread_other": "{{count}} अपठित", "{{count}} votes_one": "{{count}} वोट", @@ -224,7 +226,6 @@ "language/vi": "वियतनामी", "language/zh": "चीनी (सरलीकृत)", "language/zh-TW": "चीनी (पारंपरिक)", - "Latest Messages": "नवीनतम संदेश", "Let others add options": "दूसरों को विकल्प जोड़ने दें", "Limit votes per person": "प्रति व्यक्ति वोट सीमित करें", "live": "लाइव", @@ -334,7 +335,7 @@ "Stop sharing": "साझा करना बंद करें", "Submit": "जमा करें", "Suggest an option": "एक विकल्प सुझाव दें", - "Tap to remove": "", + "Tap to remove": "हटाने के लिए टैप करें", "Thinking...": "सोच रहा है...", "this content could not be displayed": "यह कॉन्टेंट लोड नहीं हो पाया", "This field cannot be empty or contain only spaces": "यह फ़ील्ड खाली नहीं हो सकता या केवल रिक्त स्थान नहीं रख सकता", @@ -368,8 +369,6 @@ "unmute-command-description": "एक उपयोगकर्ता को अनम्यूट करें", "Unpin": "अनपिन", "Unread messages": "अपठित संदेश", - "unreadMessagesSeparatorText_one": "1 अपठित संदेश", - "unreadMessagesSeparatorText_other": "{{count}} अपठित संदेश", "Unsupported attachment": "असमर्थित अटैचमेंट", "unsupported file type": "असमर्थित फ़ाइल प्रकार", "Update your comment": "अपने टिप्पणी को अपडेट करें", diff --git a/src/i18n/it.json b/src/i18n/it.json index 057049c3d7..7e358f5c65 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -7,9 +7,9 @@ "{{ count }} photos_one": "{{ count }} foto", "{{ count }} photos_many": "{{ count }} foto", "{{ count }} photos_other": "{{ count }} foto", - "{{ count }} reactions_one": "", - "{{ count }} reactions_many": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} reazione", + "{{ count }} reactions_many": "{{ count }} reazioni", + "{{ count }} reactions_other": "{{ count }} reazioni", "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_many": "{{ count }} video", "{{ count }} videos_other": "{{ count }} video", @@ -22,6 +22,9 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} e {{ user }} stanno digitando...", "{{ users }} and more are typing...": "{{ users }} e altri stanno digitando...", "{{ watcherCount }} online": "{{ watcherCount }} online", + "{{count}} new messages_one": "{{count}} nuovo messaggio", + "{{count}} new messages_many": "{{count}} nuovi messaggi", + "{{count}} new messages_other": "{{count}} nuovi messaggi", "{{count}} unread_one": "{{count}} non letto", "{{count}} unread_many": "{{count}} non letti", "{{count}} unread_other": "{{count}} non letti", @@ -229,7 +232,6 @@ "language/vi": "Vietnamita", "language/zh": "Cinese (semplificato)", "language/zh-TW": "Cinese (tradizionale)", - "Latest Messages": "Ultimi messaggi", "Let others add options": "Lascia che altri aggiungano opzioni", "Limit votes per person": "Limita i voti per persona", "live": "live", @@ -343,7 +345,7 @@ "Stop sharing": "Ferma condivisione", "Submit": "Invia", "Suggest an option": "Suggerisci un'opzione", - "Tap to remove": "", + "Tap to remove": "Tocca per rimuovere", "Thinking...": "Pensando...", "this content could not be displayed": "questo contenuto non può essere mostrato", "This field cannot be empty or contain only spaces": "Questo campo non può essere vuoto o contenere solo spazi", @@ -377,9 +379,6 @@ "unmute-command-description": "Togliere il silenzio a un utente", "Unpin": "Sblocca", "Unread messages": "Messaggi non letti", - "unreadMessagesSeparatorText_one": "1 messaggio non letto", - "unreadMessagesSeparatorText_many": "{{count}} messaggi non letti", - "unreadMessagesSeparatorText_other": "{{count}} messaggi non letti", "Unsupported attachment": "Allegato non supportato", "unsupported file type": "tipo di file non supportato", "Update your comment": "Aggiorna il tuo commento", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 3ad22870af..b3d7ff772b 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -5,7 +5,7 @@ "{{ count }} files_other": "{{ count }} ファイル", "{{ count }} photos_one": "{{ count }} 写真", "{{ count }} photos_other": "{{ count }} 写真", - "{{ count }} reactions_other": "", + "{{ count }} reactions_other": "{{ count }}件のリアクション", "{{ count }} videos_one": "{{ count }} 動画", "{{ count }} videos_other": "{{ count }} 動画", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} と {{ secondUser }}", @@ -17,6 +17,8 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} と {{ user }} が入力中...", "{{ users }} and more are typing...": "{{ users }} とその他が入力中...", "{{ watcherCount }} online": "{{ watcherCount }} オンライン", + "{{count}} new messages_one": "{{count}}件の新しいメッセージ", + "{{count}} new messages_other": "{{count}}件の新しいメッセージ", "{{count}} unread_one": "{{count}} 未読", "{{count}} unread_other": "{{count}} 未読", "{{count}} votes_one": "{{count}} 票", @@ -222,7 +224,6 @@ "language/vi": "ベトナム語", "language/zh": "中国語(簡体字)", "language/zh-TW": "中国語(繁体字)", - "Latest Messages": "最新のメッセージ", "Let others add options": "他の人が選択肢を追加できるようにする", "Limit votes per person": "1人あたりの投票数を制限する", "live": "ライブ", @@ -332,7 +333,7 @@ "Stop sharing": "共有を停止", "Submit": "送信", "Suggest an option": "オプションを提案", - "Tap to remove": "", + "Tap to remove": "タップして削除", "Thinking...": "考え中...", "this content could not be displayed": "このコンテンツは表示できませんでした", "This field cannot be empty or contain only spaces": "このフィールドは空にすることはできません。また、空白文字のみを含むこともできません", @@ -366,8 +367,6 @@ "unmute-command-description": "ユーザーのミュートを解除する", "Unpin": "ピンを解除する", "Unread messages": "未読メッセージ", - "unreadMessagesSeparatorText_one": "未読メッセージ 1 件", - "unreadMessagesSeparatorText_other": "未読メッセージ {{count}} 件", "Unsupported attachment": "サポートされていない添付ファイル", "unsupported file type": "サポートされていないファイル形式", "Update your comment": "コメントを更新", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index b9ce92c8d2..ec521d50c3 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -5,7 +5,7 @@ "{{ count }} files_other": "{{ count }}개 파일", "{{ count }} photos_one": "{{ count }}개 사진", "{{ count }} photos_other": "{{ count }}개 사진", - "{{ count }} reactions_other": "", + "{{ count }} reactions_other": "{{ count }}개 반응", "{{ count }} videos_one": "{{ count }}개 동영상", "{{ count }} videos_other": "{{ count }}개 동영상", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} 그리고 {{ secondUser }}", @@ -17,6 +17,8 @@ "{{ users }} and {{ user }} are typing...": "{{ users }}와(과) {{ user }}이(가) 입력 중입니다...", "{{ users }} and more are typing...": "{{ users }}와(과) 더 많은 사람들이 입력 중입니다...", "{{ watcherCount }} online": "{{ watcherCount }} 온라인", + "{{count}} new messages_one": "{{count}}개의 새 메시지", + "{{count}} new messages_other": "{{count}}개의 새 메시지", "{{count}} unread_one": "{{count}} 읽지 않음", "{{count}} unread_other": "{{count}} 읽지 않음", "{{count}} votes_one": "{{count}} 투표", @@ -222,7 +224,6 @@ "language/vi": "베트남어", "language/zh": "중국어(간체)", "language/zh-TW": "중국어(번체)", - "Latest Messages": "최신 메시지", "Let others add options": "다른 사람이 선택지를 추가할 수 있도록 허용", "Limit votes per person": "1인당 투표 수 제한", "live": "라이브", @@ -332,7 +333,7 @@ "Stop sharing": "공유 중지", "Submit": "제출", "Suggest an option": "옵션 제안", - "Tap to remove": "", + "Tap to remove": "제거하려면 탭하세요", "Thinking...": "생각 중...", "this content could not be displayed": "이 콘텐츠를 표시할 수 없습니다", "This field cannot be empty or contain only spaces": "이 필드는 비워둘 수 없으며 공백만 포함할 수도 없습니다", @@ -366,8 +367,6 @@ "unmute-command-description": "사용자 음소거 해제", "Unpin": "핀 해제", "Unread messages": "읽지 않은 메시지", - "unreadMessagesSeparatorText_one": "읽지 않은 메시지 1개", - "unreadMessagesSeparatorText_other": "읽지 않은 메시지 {{count}}개", "Unsupported attachment": "지원되지 않는 첨부 파일", "unsupported file type": "지원되지 않는 파일 형식", "Update your comment": "댓글 업데이트", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index dc0aa7a96d..ed74026add 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -5,8 +5,8 @@ "{{ count }} files_other": "{{ count }} bestanden", "{{ count }} photos_one": "{{ count }} foto", "{{ count }} photos_other": "{{ count }} foto's", - "{{ count }} reactions_one": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} reactie", + "{{ count }} reactions_other": "{{ count }} reacties", "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_other": "{{ count }} video's", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} en {{ secondUser }}", @@ -18,6 +18,8 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} en {{ user }} zijn aan het typen...", "{{ users }} and more are typing...": "{{ users }} en meer zijn aan het typen...", "{{ watcherCount }} online": "{{ watcherCount }} online", + "{{count}} new messages_one": "{{count}} nieuw bericht", + "{{count}} new messages_other": "{{count}} nieuwe berichten", "{{count}} unread_one": "{{count}} ongelezen", "{{count}} unread_other": "{{count}} ongelezen", "{{count}} votes_one": "{{count}} stem", @@ -223,7 +225,6 @@ "language/vi": "Vietnamees", "language/zh": "Chinees (vereenvoudigd)", "language/zh-TW": "Chinees (traditioneel)", - "Latest Messages": "Laatste berichten", "Let others add options": "Laat anderen opties toevoegen", "Limit votes per person": "Stemmen per persoon beperken", "live": "live", @@ -335,7 +336,7 @@ "Stop sharing": "Delen stoppen", "Submit": "Versturen", "Suggest an option": "Stel een optie voor", - "Tap to remove": "", + "Tap to remove": "Tik om te verwijderen", "Thinking...": "Denken...", "this content could not be displayed": "Deze inhoud kan niet weergegeven worden", "This field cannot be empty or contain only spaces": "Dit veld mag niet leeg zijn of alleen spaties bevatten", @@ -369,8 +370,6 @@ "unmute-command-description": "Een gebruiker niet meer dempen", "Unpin": "Losmaken", "Unread messages": "Ongelezen berichten", - "unreadMessagesSeparatorText_one": "1 ongelezen bericht", - "unreadMessagesSeparatorText_other": "{{count}} ongelezen berichten", "Unsupported attachment": "Niet-ondersteunde bijlage", "unsupported file type": "niet-ondersteund bestandstype", "Update your comment": "Werk je opmerking bij", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 1aa9c33582..4bb5d7b7e3 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -7,9 +7,9 @@ "{{ count }} photos_one": "{{ count }} foto", "{{ count }} photos_many": "{{ count }} fotos", "{{ count }} photos_other": "{{ count }} fotos", - "{{ count }} reactions_one": "", - "{{ count }} reactions_many": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} reação", + "{{ count }} reactions_many": "{{ count }} reações", + "{{ count }} reactions_other": "{{ count }} reações", "{{ count }} videos_one": "{{ count }} vídeo", "{{ count }} videos_many": "{{ count }} vídeos", "{{ count }} videos_other": "{{ count }} vídeos", @@ -22,6 +22,9 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} e {{ user }} estão digitando...", "{{ users }} and more are typing...": "{{ users }} e mais estão digitando...", "{{ watcherCount }} online": "{{ watcherCount }} online", + "{{count}} new messages_one": "{{count}} nova mensagem", + "{{count}} new messages_many": "{{count}} novas mensagens", + "{{count}} new messages_other": "{{count}} novas mensagens", "{{count}} unread_one": "{{count}} não lido", "{{count}} unread_many": "{{count}} não lidos", "{{count}} unread_other": "{{count}} não lidos", @@ -229,7 +232,6 @@ "language/vi": "Vietnamita", "language/zh": "Chinês (simplificado)", "language/zh-TW": "Chinês (tradicional)", - "Latest Messages": "Mensagens mais recentes", "Let others add options": "Permitir que outros adicionem opções", "Limit votes per person": "Limitar votos por pessoa", "live": "ao vivo", @@ -343,7 +345,7 @@ "Stop sharing": "Parar de compartilhar", "Submit": "Enviar", "Suggest an option": "Sugerir uma opção", - "Tap to remove": "", + "Tap to remove": "Toque para remover", "Thinking...": "Pensando...", "this content could not be displayed": "este conteúdo não pôde ser exibido", "This field cannot be empty or contain only spaces": "Este campo não pode estar vazio ou conter apenas espaços", @@ -377,9 +379,6 @@ "unmute-command-description": "Retirar o silenciamento de um usuário", "Unpin": "Desfixar", "Unread messages": "Mensagens não lidas", - "unreadMessagesSeparatorText_one": "1 mensagem não lida", - "unreadMessagesSeparatorText_many": "{{count}} mensagens não lidas", - "unreadMessagesSeparatorText_other": "{{count}} mensagens não lidas", "Unsupported attachment": "Anexo não suportado", "unsupported file type": "tipo de arquivo não suportado", "Update your comment": "Atualizar seu comentário", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index ccffcfca4b..e92efa8d54 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -9,10 +9,10 @@ "{{ count }} photos_few": "{{ count }} фото", "{{ count }} photos_many": "{{ count }} фото", "{{ count }} photos_other": "{{ count }} фото", - "{{ count }} reactions_one": "", - "{{ count }} reactions_few": "", - "{{ count }} reactions_many": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} реакция", + "{{ count }} reactions_few": "{{ count }} реакции", + "{{ count }} reactions_many": "{{ count }} реакций", + "{{ count }} reactions_other": "{{ count }} реакций", "{{ count }} videos_one": "{{ count }} видео", "{{ count }} videos_few": "{{ count }} видео", "{{ count }} videos_many": "{{ count }} видео", @@ -26,6 +26,10 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} и {{ user }} печатают...", "{{ users }} and more are typing...": "{{ users }} и другие печатают...", "{{ watcherCount }} online": "{{ watcherCount }} в сети", + "{{count}} new messages_one": "{{count}} новое сообщение", + "{{count}} new messages_few": "{{count}} новых сообщения", + "{{count}} new messages_many": "{{count}} новых сообщений", + "{{count}} new messages_other": "{{count}} новых сообщений", "{{count}} unread_one": "{{count}} непрочитанное", "{{count}} unread_few": "{{count}} непрочитанных", "{{count}} unread_many": "{{count}} непрочитанных", @@ -235,7 +239,6 @@ "language/vi": "Вьетнамский", "language/zh": "Китайский (упрощённый)", "language/zh-TW": "Китайский (традиционный)", - "Latest Messages": "Последние сообщения", "Let others add options": "Разрешить другим добавлять варианты", "Limit votes per person": "Ограничить голоса на человека", "live": "В прямом эфире", @@ -353,7 +356,7 @@ "Stop sharing": "Прекратить делиться", "Submit": "Отправить", "Suggest an option": "Предложить вариант", - "Tap to remove": "", + "Tap to remove": "Нажмите, чтобы удалить", "Thinking...": "Думаю...", "this content could not be displayed": "Этот контент не может быть отображен в данный момент", "This field cannot be empty or contain only spaces": "Это поле не может быть пустым или содержать только пробелы", @@ -387,10 +390,6 @@ "unmute-command-description": "Включить микрофон у пользователя", "Unpin": "Открепить", "Unread messages": "Непрочитанные сообщения", - "unreadMessagesSeparatorText_one": "1 непрочитанное сообщение", - "unreadMessagesSeparatorText_few": "1 непрочитанное сообщения", - "unreadMessagesSeparatorText_many": "{{count}} непрочитанных сообщений", - "unreadMessagesSeparatorText_other": "{{count}} непрочитанных сообщений", "Unsupported attachment": "Неподдерживаемое вложение", "unsupported file type": "неподдерживаемый тип файла", "Update your comment": "Обновите ваш комментарий", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 5cd611673b..007bc03a44 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -5,8 +5,8 @@ "{{ count }} files_other": "{{ count }} dosya", "{{ count }} photos_one": "{{ count }} fotoğraf", "{{ count }} photos_other": "{{ count }} fotoğraf", - "{{ count }} reactions_one": "", - "{{ count }} reactions_other": "", + "{{ count }} reactions_one": "{{ count }} tepki", + "{{ count }} reactions_other": "{{ count }} tepki", "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_other": "{{ count }} video", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} ve {{ secondUser }}", @@ -18,6 +18,8 @@ "{{ users }} and {{ user }} are typing...": "{{ users }} ve {{ user }} yazıyor...", "{{ users }} and more are typing...": "{{ users }} ve diğerleri yazıyor...", "{{ watcherCount }} online": "{{ watcherCount }} çevrimiçi", + "{{count}} new messages_one": "{{count}} yeni mesaj", + "{{count}} new messages_other": "{{count}} yeni mesaj", "{{count}} unread_one": "{{count}} okunmamış", "{{count}} unread_other": "{{count}} okunmamış", "{{count}} votes_one": "{{count}} oy", @@ -223,7 +225,6 @@ "language/vi": "Vietnamca", "language/zh": "Çince (basitleştirilmiş)", "language/zh-TW": "Çince (geleneksel)", - "Latest Messages": "Son Mesajlar", "Let others add options": "Başkalarının seçenek eklemesine izin ver", "Limit votes per person": "Kişi başına oy sınırı", "live": "canlı", @@ -333,7 +334,7 @@ "Stop sharing": "Paylaşımı durdur", "Submit": "Gönder", "Suggest an option": "Bir seçenek önerin", - "Tap to remove": "", + "Tap to remove": "Kaldırmak için dokunun", "Thinking...": "Düşünüyor...", "this content could not be displayed": "bu içerik gösterilemiyor", "This field cannot be empty or contain only spaces": "Bu alan boş olamaz veya sadece boşluk içeremez", @@ -367,8 +368,6 @@ "unmute-command-description": "Bir kullanıcının sesini aç", "Unpin": "Sabitlemeyi kaldır", "Unread messages": "Okunmamış mesajlar", - "unreadMessagesSeparatorText_one": "1 okunmamış mesaj", - "unreadMessagesSeparatorText_other": "{{count}} okunmamış mesaj", "Unsupported attachment": "Desteklenmeyen ek", "unsupported file type": "desteklenmeyen dosya türü", "Update your comment": "Yorumunuzu güncelleyin", diff --git a/src/styling/index.scss b/src/styling/index.scss index db4b0db85c..d754e5b0d8 100644 --- a/src/styling/index.scss +++ b/src/styling/index.scss @@ -14,7 +14,8 @@ @use '../components/FileIcon/styling/FileIcon'; // Base components -@use '../components/Button/styling/Button'; +@use '../components/Badge/styling' as Badge; +@use '../components/Button/styling' as Button; @use '../components/Form/styling' as Form; @use '../components/Dialog/styling' as Dialog; @use '../components/Modal/styling' as Modal;