diff --git a/examples/vite/src/stream-imports-layout.scss b/examples/vite/src/stream-imports-layout.scss index 2550fc7651..c59905c395 100644 --- a/examples/vite/src/stream-imports-layout.scss +++ b/examples/vite/src/stream-imports-layout.scss @@ -9,7 +9,7 @@ //@use 'stream-chat-react/dist/scss/v2/AudioRecorder/AudioRecorder-layout'; //@use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-layout'; //@use 'stream-chat-react/dist/scss/v2/Channel/Channel-layout'; -@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-layout'; +//@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-layout'; @use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-layout'; @use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-layout'; @use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-layout'; diff --git a/examples/vite/src/stream-imports-theme.scss b/examples/vite/src/stream-imports-theme.scss index 54c3fa97ad..60fd040f51 100644 --- a/examples/vite/src/stream-imports-theme.scss +++ b/examples/vite/src/stream-imports-theme.scss @@ -9,7 +9,7 @@ //@use 'stream-chat-react/dist/scss/v2/Autocomplete/Autocomplete-theme'; //@use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-theme'; //@use 'stream-chat-react/dist/scss/v2/Channel/Channel-theme.scss'; -@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-theme'; +//@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-theme'; @use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-theme'; @use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-theme'; @use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-theme'; diff --git a/src/components/Attachment/AttachmentActions.tsx b/src/components/Attachment/AttachmentActions.tsx index 938cd031ec..ced98d4cd9 100644 --- a/src/components/Attachment/AttachmentActions.tsx +++ b/src/components/Attachment/AttachmentActions.tsx @@ -70,10 +70,9 @@ const UnMemoizedAttachmentActions = (props: AttachmentActionsProps) => { {text} {actions.map((action, index) => ( diff --git a/src/components/Attachment/components/PlaybackRateButton.tsx b/src/components/Attachment/components/PlaybackRateButton.tsx index 008037cb69..503b5aee34 100644 --- a/src/components/Attachment/components/PlaybackRateButton.tsx +++ b/src/components/Attachment/components/PlaybackRateButton.tsx @@ -9,7 +9,6 @@ export const PlaybackRateButton = ({ children, onClick }: PlaybackRateButtonProp className={clsx('str-chat__message_attachment__playback-rate-button')} data-testid='playback-rate-button' onClick={onClick} - type='button' > {children} diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 8637f1d209..f213cf260b 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -2,10 +2,41 @@ import type { ComponentProps } from 'react'; import { forwardRef } from 'react'; import clsx from 'clsx'; -export type ButtonProps = ComponentProps<'button'>; +export type ButtonVariant = 'primary' | 'secondary' | 'danger'; +export type ButtonAppearance = 'solid' | 'outline' | 'ghost'; +export type ButtonSize = 'lg' | 'md' | 'sm'; + +export type ButtonProps = ComponentProps<'button'> & { + /** Semantic variant: primary, secondary, or danger (maps to destructive in styles). */ + variant?: ButtonVariant; + /** Visual style: solid, outline, or ghost. */ + appearance?: ButtonAppearance; + /** When true, uses full border-radius for icon-only/pill shape. */ + circular?: boolean; + /** Size: lg, md, or sm. */ + size?: ButtonSize; +}; + +const variantToClass: Record = { + danger: 'str-chat__button--destructive', + primary: 'str-chat__button--primary', + secondary: 'str-chat__button--secondary', +}; + +const appearanceToClass: Record = { + ghost: 'str-chat__button--ghost', + outline: 'str-chat__button--outline', + solid: 'str-chat__button--solid', +}; + +const sizeToClass: Record = { + lg: 'str-chat__button--size-lg', + md: 'str-chat__button--size-md', + sm: 'str-chat__button--size-sm', +}; export const Button = forwardRef(function Button( - { className, ...props }, + { appearance, circular, className, size, variant, ...props }, ref, ) { return ( @@ -13,7 +44,14 @@ export const Button = forwardRef(function Button ref={ref} type='button' {...props} - className={clsx('str-chat__button', className)} + className={clsx( + 'str-chat__button', + variant != null && variantToClass[variant], + appearance != null && appearanceToClass[appearance], + circular && 'str-chat__button--circular', + size != null && sizeToClass[size], + className, + )} /> ); }); diff --git a/src/components/Button/PlayButton.tsx b/src/components/Button/PlayButton.tsx index 459f79014d..3e467a34f2 100644 --- a/src/components/Button/PlayButton.tsx +++ b/src/components/Button/PlayButton.tsx @@ -9,16 +9,13 @@ export type PlayButtonProps = ComponentProps<'button'> & { export const PlayButton = ({ className, isPlaying, ...props }: PlayButtonProps) => ( diff --git a/src/components/ChannelHeader/ChannelHeader.tsx b/src/components/ChannelHeader/ChannelHeader.tsx index 7915d8eb86..4bfb2e3211 100644 --- a/src/components/ChannelHeader/ChannelHeader.tsx +++ b/src/components/ChannelHeader/ChannelHeader.tsx @@ -1,22 +1,25 @@ import React from 'react'; -import { MenuIcon as DefaultMenuIcon } from './icons'; +import { IconLayoutAlignLeft } from '../Icons/icons'; import { Avatar as DefaultAvatar } from '../Avatar'; +import { useChannelHeaderOnlineStatus } from './hooks/useChannelHeaderOnlineStatus'; import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo'; import { useChannelStateContext } from '../../context/ChannelStateContext'; import { useChatContext } from '../../context/ChatContext'; import { useTranslationContext } from '../../context/TranslationContext'; import type { ChannelAvatarProps } from '../Avatar'; +import { Button } from '../Button'; +import clsx from 'clsx'; export type ChannelHeaderProps = { /** UI component to display an avatar, defaults to [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) component and accepts the same props as: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */ Avatar?: React.ComponentType; /** Manually set the image to render, defaults to the Channel image */ image?: string; - /** Show a little indicator that the Channel is live right now */ - live?: boolean; /** UI component to display menu icon, defaults to [MenuIcon](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelHeader/ChannelHeader.tsx)*/ MenuIcon?: React.ComponentType; + /** When true, shows IconLayoutAlignLeft instead of MenuIcon for sidebar expansion */ + sidebarCollapsed?: boolean; /** Set title manually */ title?: string; }; @@ -28,12 +31,12 @@ export const ChannelHeader = (props: ChannelHeaderProps) => { const { Avatar = DefaultAvatar, image: overrideImage, - live, - MenuIcon = DefaultMenuIcon, + MenuIcon = IconLayoutAlignLeft, + sidebarCollapsed = true, title: overrideTitle, } = props; - const { channel, watcher_count } = useChannelStateContext('ChannelHeader'); + const { channel } = useChannelStateContext(); const { openMobileNav } = useChatContext('ChannelHeader'); const { t } = useTranslationContext('ChannelHeader'); const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({ @@ -41,18 +44,33 @@ export const ChannelHeader = (props: ChannelHeaderProps) => { overrideImage, overrideTitle, }); - - const { member_count, subtitle } = channel?.data || {}; + const onlineStatusText = useChannelHeaderOnlineStatus(); return ( -
- + {sidebarCollapsed && } + +
+
{displayTitle}
+ {onlineStatusText != null && ( +
+ {onlineStatusText} +
+ )} +
{ size='lg' userName={displayTitle} /> -
-

- {displayTitle}{' '} - {live && ( - {t('live')} - )} -

- {subtitle &&

{subtitle}

} -

- {!live && !!member_count && member_count > 0 && ( - <> - {t('{{ memberCount }} members', { - memberCount: member_count, - })} - ,{' '} - - )} - {t('{{ watcherCount }} online', { watcherCount: watcher_count })} -

-
); }; diff --git a/src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts b/src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts new file mode 100644 index 0000000000..fb9cf67d2e --- /dev/null +++ b/src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react'; +import type { ChannelState } from 'stream-chat'; + +import { useChannelStateContext } from '../../../context/ChannelStateContext'; +import { useChatContext } from '../../../context/ChatContext'; +import { useTranslationContext } from '../../../context/TranslationContext'; + +/** + * Returns the channel header online status text (e.g. "Online", "Offline", or "X members, Y online"). + * Returns null when the channel has no members (nothing to show). + */ +export function useChannelHeaderOnlineStatus(): string | null { + const { t } = useTranslationContext(); + const { client } = useChatContext(); + const { channel, watcherCount = 0 } = useChannelStateContext(); + const { member_count: memberCount = 0 } = channel?.data || {}; + + // todo: we need reactive state for watchers in LLC + const [watchers, setWatchers] = useState(() => + Object.assign({}, channel?.state?.watchers ?? {}), + ); + + useEffect(() => { + if (!channel) return; + const subscription = channel.on('user.watching.start', (event) => { + setWatchers((prev) => { + if (!event.user?.id) return prev; + if (prev[event.user.id]) return prev; + return Object.assign({ [event.user.id]: event.user }, prev); + }); + }); + return () => subscription.unsubscribe(); + }, [channel]); + + if (!memberCount) return null; + + const isDmChannel = + memberCount === 1 || + (memberCount === 2 && + Object.values(channel?.state?.members ?? {}).some( + ({ user }) => user?.id === client.user?.id, + )); + + if (isDmChannel) { + const hasWatchers = Object.keys(watchers).length > 0; + return hasWatchers ? t('Online') : t('Offline'); + } + + return `${t('{{ memberCount }} members', { memberCount })}, ${t('{{ watcherCount }} online', { watcherCount })}`; +} diff --git a/src/components/ChannelHeader/icons.tsx b/src/components/ChannelHeader/icons.tsx deleted file mode 100644 index 3c86210338..0000000000 --- a/src/components/ChannelHeader/icons.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -import { useTranslationContext } from '../../context'; - -export const MenuIcon = ({ title }: { title?: string }) => { - const { t } = useTranslationContext('MenuIcon'); - - return ( - - {title ?? t('Menu')} - - - ); -}; diff --git a/src/components/ChannelHeader/plan.md b/src/components/ChannelHeader/plan.md new file mode 100644 index 0000000000..a06fe46dac --- /dev/null +++ b/src/components/ChannelHeader/plan.md @@ -0,0 +1,169 @@ +# Channel Header Figma Implementation + +## Worktree + +| Field | Value | +| ------------------ | ----------------------------------------------------- | +| **Path** | `../stream-chat-react-worktrees/channel-header-figma` | +| **Branch** | `feat/channel-header-figma` | +| **Base branch** | `feat/add-message-translation-indicator` | +| **Preview branch** | `agent/feat/channel-header-figma` (create when ready) | + +**Agent must `cd` into the worktree before any work:** + +```bash +cd ../stream-chat-react-worktrees/channel-header-figma +yarn install +``` + +--- + +## Task Overview + +Tasks are self-contained; styling and component tasks have a dependency order. All work in `src/components/ChannelHeader/`. + +--- + +## Design Reference + +**Figma:** [Chat SDK Design System – Web / Headers](https://www.figma.com/design/Us73erK1xFNcB5EH3hyq6Y/Chat-SDK-Design-System?node-id=1899-32506&m=dev) + +**Key design elements (channel header section):** + +- Layout: `[hamburger or sidebar icon] | [channelName + Online stacked] | [avatar right]` +- Variant: Sidebar collapsed (shows `IconLayoutAlignLeft`) vs expanded (hamburger / MenuIcon) +- Avatar on the right (current impl has it between hamburger and title) + +--- + +## Task 1: Create ChannelHeader Styling Folder and Base Styles + +**File(s) to create:** `src/components/ChannelHeader/styling/ChannelHeader.scss`, `src/components/ChannelHeader/styling/index.scss` + +**Dependencies:** None + +**Status:** pending + +**Owner:** unassigned + +**Scope:** + +- Create `src/components/ChannelHeader/styling/` folder +- `ChannelHeader.scss`: Base layout using `@include utils.header-layout`, full class names per dev-patterns +- Layout: flex, avatar right-aligned (`margin-left: auto` or `justify-content: space-between`) +- Use design tokens: `--str-chat__channel-header-background-color`, typography vars, spacing +- `index.scss`: `@use './ChannelHeader'` (or forward) +- **Do NOT** register in `src/styling/index.scss` yet (Task 3) + +**Acceptance Criteria:** + +- [ ] `ChannelHeader.scss` exists with `.str-chat__channel-header` and child selectors +- [ ] Uses full class names, no `&__suffix`-only blocks for same selector +- [ ] `index.scss` forwards/uses ChannelHeader.scss + +--- + +## Task 2: Add Sidebar-Collapsed Variant Styles + +**File(s) to create/modify:** `src/components/ChannelHeader/styling/ChannelHeader.scss` + +**Dependencies:** Task 1 + +**Status:** pending + +**Owner:** unassigned + +**Scope:** + +- Add modifier: `.str-chat__channel-header--sidebar-collapsed` (compact left icon area if needed) +- Use existing `--str-chat__*` vars from design-system-tokens +- Hamburger (MenuIcon) and sidebar toggle (`IconLayoutAlignLeft`) button styling + +**Acceptance Criteria:** + +- [ ] Sidebar-collapsed modifier applies correctly when class is present + +--- + +## Task 3: Register ChannelHeader Styles and Update Component + +**File(s) to create/modify:** `src/styling/index.scss`, `src/components/ChannelHeader/ChannelHeader.tsx` + +**Dependencies:** Task 1, Task 2 + +**Status:** pending + +**Owner:** unassigned + +**Scope:** + +- Add `@use '../components/ChannelHeader/styling' as ChannelHeader` to `src/styling/index.scss` in the appropriate group (alphabetical, chat components) +- Update `ChannelHeader.tsx`: + - Reorder layout: hamburger/sidebar icon | text block (title + Online) | avatar (right) + - Add optional prop: `sidebarCollapsed?: boolean` + - Render `MenuIcon` (hamburger) when expanded, `IconLayoutAlignLeft` when `sidebarCollapsed` — import from `src/components/Icons/icons.tsx` + - Simplify info line to "Online" (or keep watcher_count: "X online") per design + - Apply modifier class: `str-chat__channel-header--sidebar-collapsed` when `sidebarCollapsed` +- Preserve: `live`, `subtitle`, `member_count`, `Avatar`, `MenuIcon`, `title`, `image` — ensure backward compatibility + +**Acceptance Criteria:** + +- [ ] ChannelHeader styles imported in `src/styling/index.scss` +- [ ] Component layout matches Figma: avatar on right, text in middle +- [ ] New prop `sidebarCollapsed` works and applies modifier +- [ ] Existing tests pass; update tests if needed for new structure + +--- + +## Task 4: Integration and Tests + +**File(s) to create/modify:** `src/components/ChannelHeader/__tests__/ChannelHeader.test.js` + +**Dependencies:** Task 3 + +**Status:** pending + +**Owner:** unassigned + +**Scope:** + +- Run existing tests; fix any failures from layout/class changes +- Add tests for `sidebarCollapsed` when applicable +- Ensure `yarn test`, `yarn types`, `yarn lint-fix` pass + +**Acceptance Criteria:** + +- [ ] All existing tests pass +- [ ] New props covered if feasible +- [ ] `yarn types` and `yarn lint-fix` pass + +--- + +## Execution Order + +| Phase | Tasks | Can run in parallel? | +| ----- | ------ | ------------------------- | +| 1 | Task 1 | — | +| 2 | Task 2 | No (depends on Task 1) | +| 3 | Task 3 | No (depends on Task 1, 2) | +| 4 | Task 4 | No (depends on Task 3) | + +--- + +## File Ownership Summary + +| Task | Creates | Modifies | +| ---- | ------------------------------------------------------------------------------ | ----------------------------------------------------------- | +| 1 | `ChannelHeader/styling/ChannelHeader.scss`, `ChannelHeader/styling/index.scss` | — | +| 2 | — | `ChannelHeader/styling/ChannelHeader.scss` | +| 3 | — | `src/styling/index.scss`, `ChannelHeader/ChannelHeader.tsx` | +| 4 | — | `ChannelHeader/__tests__/ChannelHeader.test.js` | + +--- + +## Notes + +- ChannelHeader currently has no dedicated styling folder; styles may come from stream-chat-css. This plan introduces ChannelHeader/styling per dev-patterns. +- Loading channel header in `Channel/styling/Channel.scss` uses `--str-chat__channel-header-background-color`; keep variable usage consistent. +- Backward compatibility: `sidebarCollapsed` defaults to `false`; existing usage unchanged. +- Sidebar expansion toggle icon: use `IconLayoutAlignLeft` from `src/components/Icons/icons.tsx` when `sidebarCollapsed=true`. diff --git a/src/components/ChannelHeader/styling/ChannelHeader.scss b/src/components/ChannelHeader/styling/ChannelHeader.scss new file mode 100644 index 0000000000..217e2c0b9e --- /dev/null +++ b/src/components/ChannelHeader/styling/ChannelHeader.scss @@ -0,0 +1,65 @@ +@use '../../../styling/utils'; + +.str-chat { + /* The border radius used for the borders of the component */ + --str-chat__channel-header-border-radius: 0; + + /* The text/icon color of the component */ + --str-chat__channel-header-color: 0; + + /* The background color of the component */ + --str-chat__channel-header-background-color: var(--background-elevation-elevation-1); + + /* Top border of the component */ + --str-chat__channel-header-border-block-start: none; + + /* Bottom border of the component */ + --str-chat__channel-header-border-block-end: 1px solid var(--border-core-default); + + /* Left (right in RTL layout) border of the component */ + --str-chat__channel-header-border-inline-start: none; + + /* Right (left in RTL layout) border of the component */ + --str-chat__channel-header-border-inline-end: none; + + /* Box shadow applied to the component */ + --str-chat__channel-header-box-shadow: none; + + /* The text/icon color used to display member information about the channel */ + --str-chat__channel-header__data__subtitle-color: var(--text-secondary); +} + +.str-chat__channel-header { + @include utils.component-layer-overrides('channel-header'); + display: flex; + padding: var(--spacing-md); + column-gap: var(--spacing-sm); + align-items: center; + flex: 1; + min-width: 0; + + .str-chat__channel-header__data { + @include utils.header-text-layout; + min-width: 0; + } + + .str-chat__channel-header__data__title, + .str-chat__channel-header__data__subtitle { + @include utils.ellipsis-text; + } + + .str-chat__channel-header__data__title { + font: var(--str-chat__heading-sm-text); + } + + .str-chat__channel-header__data__subtitle { + font: var(--str-chat__caption-default-text); + color: var(--str-chat__channel-header__data__subtitle-color); + } + + &.str-chat__channel-header--sidebar-collapsed { + .str-chat__header-sidebar-toggle { + // Compact styling when sidebar collapsed + } + } +} diff --git a/src/components/ChannelHeader/styling/index.scss b/src/components/ChannelHeader/styling/index.scss new file mode 100644 index 0000000000..1385a7048d --- /dev/null +++ b/src/components/ChannelHeader/styling/index.scss @@ -0,0 +1 @@ +@forward './ChannelHeader'; diff --git a/src/components/ChatView/ChatView.tsx b/src/components/ChatView/ChatView.tsx index 51053d7126..7e46393cc9 100644 --- a/src/components/ChatView/ChatView.tsx +++ b/src/components/ChatView/ChatView.tsx @@ -142,14 +142,11 @@ export const ChatViewSelectorButton = ({ ...props }: ButtonProps & { Icon?: ComponentType; text?: string }) => ( diff --git a/src/components/Dialog/components/Prompt.tsx b/src/components/Dialog/components/Prompt.tsx index 42ff6870fc..96de6b8b3e 100644 --- a/src/components/Dialog/components/Prompt.tsx +++ b/src/components/Dialog/components/Prompt.tsx @@ -27,14 +27,12 @@ const PromptHeader = ({
{goBack && ( @@ -47,14 +45,12 @@ const PromptHeader = ({
{close && ( @@ -88,27 +84,21 @@ const PromptFooterControls = ({ children, className }: PromptFooterControlsProps const PromptFooterControlsButtonSecondary = ({ className, ...props }: ButtonProps) => ( diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx index c2ccc4d71a..42f07c4efb 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { useMessageInputContext } from '../../../context'; import { isRecording } from './recordingStateIdentity'; import { Button } from '../../Button'; -import clsx from 'clsx'; const ToggleRecordingButton = () => { const { @@ -13,16 +12,14 @@ const ToggleRecordingButton = () => { return ( @@ -41,31 +38,27 @@ export const AudioRecorderRecordingControls = () => {
{!isRecording(recordingState) && ( )} diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx index 47581d3fd1..e7ba6596a5 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx @@ -5,7 +5,6 @@ import { useAttachmentManagerState } from '../../MessageInput'; import { useComponentContext, useMessageInputContext } from '../../../context'; import { Callout, useDialogOnNearestManager } from '../../Dialog'; import { Button } from '../../Button'; -import clsx from 'clsx'; import { IconMicrophone } from '../../Icons'; const dialogId = 'recording-permission-denied-notification'; @@ -66,15 +65,13 @@ export const DefaultStartRecordingAudioButton = forwardRef< >(function StartRecordingAudioButton(props, ref) { return ( diff --git a/src/components/MessageActions/DeleteMessageAlert.tsx b/src/components/MessageActions/DeleteMessageAlert.tsx index 254a72ef8d..deebb4dea0 100644 --- a/src/components/MessageActions/DeleteMessageAlert.tsx +++ b/src/components/MessageActions/DeleteMessageAlert.tsx @@ -1,6 +1,5 @@ import { Alert } from '../Dialog'; import { Button } from '../Button'; -import clsx from 'clsx'; import React from 'react'; import { useTranslationContext } from '../../context'; import type { ModalProps } from '../Modal'; @@ -22,26 +21,22 @@ export const DeleteMessageAlert = ({ onClose, onDelete }: DeleteMessageAlertProp /> diff --git a/src/components/MessageActions/MessageActions.tsx b/src/components/MessageActions/MessageActions.tsx index dab95c2ffd..7e767d812f 100644 --- a/src/components/MessageActions/MessageActions.tsx +++ b/src/components/MessageActions/MessageActions.tsx @@ -98,20 +98,18 @@ export const MessageActions = ({ {dropdownActionSet.length > 0 && ( <> diff --git a/src/components/MessageActions/QuickMessageActionButton.tsx b/src/components/MessageActions/QuickMessageActionButton.tsx index 1cfe3d8195..249da7034b 100644 --- a/src/components/MessageActions/QuickMessageActionButton.tsx +++ b/src/components/MessageActions/QuickMessageActionButton.tsx @@ -4,13 +4,10 @@ import React from 'react'; export const QuickMessageActionsButton = ({ className, ...props }: ButtonProps) => ( diff --git a/src/components/MessageInput/AttachmentPreviewList/MediaAttachmentPreview.tsx b/src/components/MessageInput/AttachmentPreviewList/MediaAttachmentPreview.tsx index 4ace7b6f46..8cead490ea 100644 --- a/src/components/MessageInput/AttachmentPreviewList/MediaAttachmentPreview.tsx +++ b/src/components/MessageInput/AttachmentPreviewList/MediaAttachmentPreview.tsx @@ -109,16 +109,14 @@ export const MediaAttachmentPreview = ({ {hasRetriableError && ( diff --git a/src/components/MessageInput/AttachmentSelector/AttachmentSelector.tsx b/src/components/MessageInput/AttachmentSelector/AttachmentSelector.tsx index f0f71b2180..287972e604 100644 --- a/src/components/MessageInput/AttachmentSelector/AttachmentSelector.tsx +++ b/src/components/MessageInput/AttachmentSelector/AttachmentSelector.tsx @@ -65,15 +65,12 @@ export const AttachmentSelectorButton = forwardRef diff --git a/src/components/MessageInput/RemoveAttachmentPreviewButton.tsx b/src/components/MessageInput/RemoveAttachmentPreviewButton.tsx index aeee2f4d42..ae3964bf72 100644 --- a/src/components/MessageInput/RemoveAttachmentPreviewButton.tsx +++ b/src/components/MessageInput/RemoveAttachmentPreviewButton.tsx @@ -15,17 +15,15 @@ export const RemoveAttachmentPreviewButton = ({ const { t } = useTranslationContext(); return ( diff --git a/src/components/MessageInput/SendButton.tsx b/src/components/MessageInput/SendButton.tsx index b71fbb209d..9c60655b88 100644 --- a/src/components/MessageInput/SendButton.tsx +++ b/src/components/MessageInput/SendButton.tsx @@ -3,7 +3,6 @@ import { useMessageComposerHasSendableData } from './hooks'; import { useTranslationContext } from '../../context'; import { IconPaperPlane } from '../Icons'; import { Button } from '../Button'; -import clsx from 'clsx'; export type SendButtonProps = { sendMessage: (event: React.BaseSyntheticEvent) => void; @@ -14,18 +13,15 @@ export const SendButton = ({ children, sendMessage, ...rest }: SendButtonProps) const hasSendableData = useMessageComposerHasSendableData(); return ( diff --git a/src/components/MessageList/UnreadMessagesNotification.tsx b/src/components/MessageList/UnreadMessagesNotification.tsx index c2e675dab1..c49dcb7d26 100644 --- a/src/components/MessageList/UnreadMessagesNotification.tsx +++ b/src/components/MessageList/UnreadMessagesNotification.tsx @@ -35,18 +35,16 @@ export const UnreadMessagesNotification = ({ data-testid='unread-messages-notification' > -
diff --git a/src/components/MessageList/UnreadMessagesSeparator.tsx b/src/components/MessageList/UnreadMessagesSeparator.tsx index 245481ed39..5dcc158d46 100644 --- a/src/components/MessageList/UnreadMessagesSeparator.tsx +++ b/src/components/MessageList/UnreadMessagesSeparator.tsx @@ -1,7 +1,6 @@ import React from 'react'; 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'; @@ -34,13 +33,11 @@ export const UnreadMessagesSeparator = ({ : t('Unread messages')} diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx index 476e7627a4..201b13c16e 100644 --- a/src/components/MessageList/VirtualizedMessageList.tsx +++ b/src/components/MessageList/VirtualizedMessageList.tsx @@ -576,13 +576,13 @@ const VirtualizedMessageListWithContext = ( newMessageCount={channelUnreadUiState?.unread_messages} showNotification={newMessagesNotification || hasMoreNewer} /> + - {TypingIndicator && } diff --git a/src/components/Modal/CloseButtonOnModalOverlay.tsx b/src/components/Modal/CloseButtonOnModalOverlay.tsx index d7671016dc..164fe4ba51 100644 --- a/src/components/Modal/CloseButtonOnModalOverlay.tsx +++ b/src/components/Modal/CloseButtonOnModalOverlay.tsx @@ -9,13 +9,10 @@ export const CloseButtonOnModalOverlay = ({ ...props }: ComponentProps<'button'>) => ( diff --git a/src/components/Poll/PollActions/PollAction.tsx b/src/components/Poll/PollActions/PollAction.tsx index e84a5096e2..34d5341eb8 100644 --- a/src/components/Poll/PollActions/PollAction.tsx +++ b/src/components/Poll/PollActions/PollAction.tsx @@ -31,14 +31,13 @@ export const PollAction = ({ return ( <> diff --git a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx index afc0b56da1..712ffacb4f 100644 --- a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx +++ b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx @@ -114,13 +114,11 @@ export const OptionFieldSet = () => { const RemoveOptionButton = ({ className, ...props }: ButtonProps) => ( + ); }; diff --git a/src/components/VideoPlayer/VideoThumbnail.tsx b/src/components/VideoPlayer/VideoThumbnail.tsx index 9cef997110..2ae5ec9a16 100644 --- a/src/components/VideoPlayer/VideoThumbnail.tsx +++ b/src/components/VideoPlayer/VideoThumbnail.tsx @@ -24,15 +24,15 @@ export const VideoThumbnail = ({ /> {onPlay ? ( diff --git a/src/i18n/de.json b/src/i18n/de.json index 9ef64503fc..ff8021140c 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -59,6 +59,7 @@ "aria/Download attachment": "Anhang herunterladen", "aria/Edit Message": "Nachricht bearbeiten", "aria/Emoji picker": "Emoji-Auswahl", + "aria/Expand sidebar": "Seitenleiste einblenden", "aria/File input": "Dateieingabe", "aria/File upload": "Datei hochladen", "aria/Flag Message": "Nachricht melden", @@ -257,7 +258,9 @@ "No results found": "Keine Ergebnisse gefunden", "Nobody will be able to vote in this poll anymore.": "Niemand kann mehr in dieser Umfrage abstimmen.", "Nothing yet...": "Noch nichts...", + "Offline": "Offline", "Ok": "OK", + "Online": "Online", "Only numbers are allowed": "Nur Zahlen sind erlaubt", "Only visible to you": "Nur für dich sichtbar", "Open emoji picker": "Emoji-Auswahl öffnen", diff --git a/src/i18n/en.json b/src/i18n/en.json index 7faa0b27ac..13225a7103 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -59,6 +59,7 @@ "aria/Download attachment": "Download attachment", "aria/Edit Message": "Edit Message", "aria/Emoji picker": "Emoji picker", + "aria/Expand sidebar": "Expand sidebar", "aria/File input": "File input", "aria/File upload": "File upload", "aria/Flag Message": "Flag Message", @@ -257,7 +258,9 @@ "No results found": "No results found", "Nobody will be able to vote in this poll anymore.": "Nobody will be able to vote in this poll anymore.", "Nothing yet...": "Nothing yet...", + "Offline": "Offline", "Ok": "Ok", + "Online": "Online", "Only numbers are allowed": "Only numbers are allowed", "Only visible to you": "Only visible to you", "Open emoji picker": "Open emoji picker", diff --git a/src/i18n/es.json b/src/i18n/es.json index 1e48f15b82..462e973213 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -66,6 +66,7 @@ "aria/Download attachment": "Descargar adjunto", "aria/Edit Message": "Editar mensaje", "aria/Emoji picker": "Selector de emojis", + "aria/Expand sidebar": "Expandir barra lateral", "aria/File input": "Entrada de archivo", "aria/File upload": "Carga de archivo", "aria/Flag Message": "Marcar mensaje", @@ -264,7 +265,9 @@ "No results found": "No se han encontrado resultados", "Nobody will be able to vote in this poll anymore.": "Nadie podrá votar en esta encuesta.", "Nothing yet...": "Nada aún...", + "Offline": "Desconectado", "Ok": "Aceptar", + "Online": "En línea", "Only numbers are allowed": "Solo se permiten números", "Only visible to you": "Solo visible para ti", "Open emoji picker": "Abrir el selector de emojis", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 08b04344e8..1f31bc9ed2 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -66,6 +66,7 @@ "aria/Download attachment": "Télécharger la pièce jointe", "aria/Edit Message": "Éditer un message", "aria/Emoji picker": "Sélecteur d'émojis", + "aria/Expand sidebar": "Développer la barre latérale", "aria/File input": "Entrée de fichier", "aria/File upload": "Téléchargement de fichier", "aria/Flag Message": "Signaler le message", @@ -264,7 +265,9 @@ "No results found": "Aucun résultat trouvé", "Nobody will be able to vote in this poll anymore.": "Personne ne pourra plus voter dans ce sondage.", "Nothing yet...": "Rien pour l'instant...", + "Offline": "Hors ligne", "Ok": "D'accord", + "Online": "En ligne", "Only numbers are allowed": "Seuls les chiffres sont autorisés", "Only visible to you": "Visible uniquement pour vous", "Open emoji picker": "Ouvrir le sélecteur d'émojis", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index 34f7b5e6e9..f94b9130af 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -59,6 +59,7 @@ "aria/Download attachment": "अनुलग्नक डाउनलोड करें", "aria/Edit Message": "मैसेज में बदलाव करे", "aria/Emoji picker": "इमोजी चुनने वाला", + "aria/Expand sidebar": "साइडबार विस्तारित करें", "aria/File input": "फ़ाइल इनपुट", "aria/File upload": "फ़ाइल अपलोड", "aria/Flag Message": "संदेश फ्लैग करें", @@ -258,7 +259,9 @@ "No results found": "कोई परिणाम नहीं मिला", "Nobody will be able to vote in this poll anymore.": "अब कोई भी इस मतदान में मतदान नहीं कर सकेगा।", "Nothing yet...": "कोई मैसेज नहीं है", + "Offline": "ऑफलाइन", "Ok": "ठीक है", + "Online": "ऑनलाइन", "Only numbers are allowed": "केवल संख्याएँ अनुमत हैं", "Only visible to you": "केवल आपको दिखाई देता है", "Open emoji picker": "इमोजी पिकर खोलिये", diff --git a/src/i18n/it.json b/src/i18n/it.json index 7e358f5c65..115da68263 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -66,6 +66,7 @@ "aria/Download attachment": "Scarica l'allegato", "aria/Edit Message": "Modifica messaggio", "aria/Emoji picker": "Selettore di emoji", + "aria/Expand sidebar": "Espandi barra laterale", "aria/File input": "Input di file", "aria/File upload": "Caricamento di file", "aria/Flag Message": "Segnala messaggio", @@ -264,7 +265,9 @@ "No results found": "Nessun risultato trovato", "Nobody will be able to vote in this poll anymore.": "Nessuno potrà più votare in questo sondaggio.", "Nothing yet...": "Ancora niente...", + "Offline": "Offline", "Ok": "OK", + "Online": "Online", "Only numbers are allowed": "Sono consentiti solo numeri", "Only visible to you": "Visibile solo per te", "Open emoji picker": "Apri il selettore di emoji", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index b3d7ff772b..16afa582c1 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -58,6 +58,7 @@ "aria/Download attachment": "添付ファイルをダウンロード", "aria/Edit Message": "メッセージを編集", "aria/Emoji picker": "絵文字ピッカー", + "aria/Expand sidebar": "サイドバーを展開", "aria/File input": "ファイル入力", "aria/File upload": "ファイルアップロード", "aria/Flag Message": "メッセージをフラグ", @@ -256,7 +257,9 @@ "No results found": "結果が見つかりません", "Nobody will be able to vote in this poll anymore.": "この投票では、誰も投票できなくなります。", "Nothing yet...": "まだ何もありません...", + "Offline": "オフライン", "Ok": "OK", + "Online": "オンライン", "Only numbers are allowed": "数字のみ許可されています", "Only visible to you": "あなただけに表示", "Open emoji picker": "絵文字ピッカーを開く", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index ec521d50c3..5ff3cf5548 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -58,6 +58,7 @@ "aria/Download attachment": "첨부 파일 다운로드", "aria/Edit Message": "메시지 수정", "aria/Emoji picker": "이모지 선택기", + "aria/Expand sidebar": "사이드바 확장", "aria/File input": "파일 입력", "aria/File upload": "파일 업로드", "aria/Flag Message": "메시지 신고", @@ -256,7 +257,9 @@ "No results found": "검색 결과가 없습니다", "Nobody will be able to vote in this poll anymore.": "이 투표에 더 이상 아무도 투표할 수 없습니다.", "Nothing yet...": "아직 아무것도...", + "Offline": "오프라인", "Ok": "확인", + "Online": "온라인", "Only numbers are allowed": "숫자만 입력 가능합니다", "Only visible to you": "당신에게만 표시됨", "Open emoji picker": "이모지 선택기 열기", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index ed74026add..d3f41074df 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -59,6 +59,7 @@ "aria/Download attachment": "Bijlage downloaden", "aria/Edit Message": "Bericht bewerken", "aria/Emoji picker": "Emoji kiezer", + "aria/Expand sidebar": "Zijbalken uitvouwen", "aria/File input": "Bestandsinvoer", "aria/File upload": "Bestand uploaden", "aria/Flag Message": "Bericht markeren", @@ -257,7 +258,9 @@ "No results found": "Geen resultaten gevonden", "Nobody will be able to vote in this poll anymore.": "Niemand kan meer stemmen in deze peiling.", "Nothing yet...": "Nog niets ...", + "Offline": "Offline", "Ok": "Oké", + "Online": "Online", "Only numbers are allowed": "Alleen nummers zijn toegestaan", "Only visible to you": "Alleen zichtbaar voor jou", "Open emoji picker": "Emoji-kiezer openen", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 4bb5d7b7e3..8daa14c495 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -66,6 +66,7 @@ "aria/Download attachment": "Baixar anexo", "aria/Edit Message": "Editar Mensagem", "aria/Emoji picker": "Seletor de emojis", + "aria/Expand sidebar": "Expandir barra lateral", "aria/File input": "Entrada de arquivo", "aria/File upload": "Carregar arquivo", "aria/Flag Message": "Reportar mensagem", @@ -264,7 +265,9 @@ "No results found": "Nenhum resultado encontrado", "Nobody will be able to vote in this poll anymore.": "Ninguém mais poderá votar nesta pesquisa.", "Nothing yet...": "Nada ainda...", + "Offline": "Offline", "Ok": "OK", + "Online": "Online", "Only numbers are allowed": "Apenas números são permitidos", "Only visible to you": "Visível apenas para você", "Open emoji picker": "Abrir seletor de emoji", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index e92efa8d54..743cea85ab 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -73,6 +73,7 @@ "aria/Download attachment": "Скачать вложение", "aria/Edit Message": "Редактировать сообщение", "aria/Emoji picker": "Выбор эмодзи", + "aria/Expand sidebar": "Развернуть боковую панель", "aria/File input": "Ввод файла", "aria/File upload": "Загрузка файла", "aria/Flag Message": "Пожаловаться на сообщение", @@ -271,7 +272,9 @@ "No results found": "Результаты не найдены", "Nobody will be able to vote in this poll anymore.": "Никто больше не сможет голосовать в этом опросе.", "Nothing yet...": "Пока ничего нет...", + "Offline": "Не в сети", "Ok": "Ок", + "Online": "В сети", "Only numbers are allowed": "Разрешены только цифры", "Only visible to you": "Видно только вам", "Open emoji picker": "Открыть выбор смайлов", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 007bc03a44..bfa0f166f2 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -59,6 +59,7 @@ "aria/Download attachment": "Ek indir", "aria/Edit Message": "Mesajı Düzenle", "aria/Emoji picker": "Emoji seçici", + "aria/Expand sidebar": "Kenar çubuğunu genişlet", "aria/File input": "Dosya girişi", "aria/File upload": "Dosya yükleme", "aria/Flag Message": "Mesajı bayrakla", @@ -257,7 +258,9 @@ "No results found": "Sonuç bulunamadı", "Nobody will be able to vote in this poll anymore.": "Artık bu ankette kimse oy kullanamayacak.", "Nothing yet...": "Şimdilik hiçbir şey...", + "Offline": "Çevrimdışı", "Ok": "Tamam", + "Online": "Çevrimiçi", "Only numbers are allowed": "Sadece sayılar kullanılabilir", "Only visible to you": "Sadece sana görünür", "Open emoji picker": "Emoji klavyesini aç", diff --git a/src/plugins/Emojis/EmojiPicker.tsx b/src/plugins/Emojis/EmojiPicker.tsx index 3af1815c4c..974e74b9b6 100644 --- a/src/plugins/Emojis/EmojiPicker.tsx +++ b/src/plugins/Emojis/EmojiPicker.tsx @@ -9,7 +9,6 @@ import { useMessageComposer, } from '../../components'; import { usePopoverPosition } from '../../components/Dialog/hooks/usePopoverPosition'; -import clsx from 'clsx'; import { useIsCooldownActive } from '../../components/MessageInput/hooks/useIsCooldownActive'; const isShadowRoot = (node: Node): node is ShadowRoot => !!(node as ShadowRoot).host; @@ -35,14 +34,12 @@ export type EmojiPickerProps = { popperOptions?: Partial<{ placement: PopperLikePlacement }>; }; -const classNames: EmojiPickerProps = { - buttonClassName: clsx( - 'str-chat__emoji-picker-button', - 'str-chat__button--ghost', - 'str-chat__button--secondary', - 'str-chat__button--size-sm', - 'str-chat__button--circular', - ), +const defaultButtonClassName = 'str-chat__emoji-picker-button'; + +const classNames: Pick< + EmojiPickerProps, + 'pickerContainerClassName' | 'wrapperClassName' +> = { pickerContainerClassName: 'str-chat__message-textarea-emoji-picker-container', wrapperClassName: 'str-chat__message-textarea-emoji-picker', }; @@ -68,7 +65,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => { refs.setFloating(popperElement); }, [popperElement, refs]); - const { buttonClassName, pickerContainerClassName, wrapperClassName } = classNames; + const { pickerContainerClassName, wrapperClassName } = classNames; const { ButtonIconComponent = IconEmojiSmile } = props; @@ -118,13 +115,17 @@ export const EmojiPicker = (props: EmojiPickerProps) => { )} diff --git a/src/styling/_utils.scss b/src/styling/_utils.scss index 298868634e..34c72b717d 100644 --- a/src/styling/_utils.scss +++ b/src/styling/_utils.scss @@ -99,6 +99,7 @@ @mixin header-text-layout { display: flex; flex-direction: column; + align-items: center; overflow-y: hidden; // for Edge overflow-x: hidden; // for ellipsis text flex: 1; diff --git a/src/styling/index.scss b/src/styling/index.scss index d754e5b0d8..7e780797b5 100644 --- a/src/styling/index.scss +++ b/src/styling/index.scss @@ -27,6 +27,7 @@ @use '../components/Avatar/styling/AvatarStack' as AvatarStack; @use '../components/Avatar/styling/GroupAvatar' as GroupAvatar; @use '../components/Channel/styling' as Channel; +@use '../components/ChannelHeader/styling' as ChannelHeader; @use '../components/ChatView/styling' as ChatView; @use '../components/DateSeparator/styling' as DateSeparator; @use '../components/DragAndDrop/styling' as DragAndDrop;