Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/vite/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

// v3 CSS import
@import url('stream-chat-react/dist/css/index.css') layer(stream-new);
@import url('stream-chat-react/dist/css/emojis.css') layer(stream-new);
@import url('./AppSettings/AppSettings.scss') layer(stream-app-overrides);

:root {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@
"scripts": {
"clean": "rm -rf dist",
"build": "yarn clean && concurrently './scripts/copy-css.sh' 'yarn build-translations' 'vite build' 'tsc --project tsconfig.lib.json' 'yarn build-styling'",
"build-styling": "sass src/styling/index.scss dist/css/index.css && sass src/plugins/Emojis/styling/index.scss dist/css/emojis.css",
"build-styling": "sass src/styling/index.scss dist/css/index.css",
"build-translations": "i18next-cli extract",
"coverage": "jest --collectCoverage && codecov",
"lint": "yarn prettier --list-different && yarn eslint && yarn validate-translations",
Expand Down
10 changes: 9 additions & 1 deletion src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -967,15 +967,23 @@ const ChannelInner = (
};

const retrySendMessage = async (localMessage: LocalMessage) => {
/**
* If type is not checked, and we for example send message.type === 'error',
* then request fails with error: "message.type must be one of ['' regular system]".
* For now, we re-send any other type to prevent breaking behavior.
*/

const type = localMessage.type === 'error' ? 'regular' : localMessage.type;
updateMessage({
...localMessage,
error: undefined,
status: 'sending',
type,
});

await doSendMessage({
localMessage,
message: localMessageToNewMessagePayload(localMessage),
message: localMessageToNewMessagePayload({ ...localMessage, type }),
});
};

Expand Down
88 changes: 85 additions & 3 deletions src/components/Dialog/base/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, {
type ComponentProps,
type ComponentType,
Expand All @@ -10,6 +11,9 @@ import React, {
} from 'react';
import clsx from 'clsx';
import { IconChevronLeft } from '../../Icons';
import { useDialogIsOpen } from '../hooks';
import type { DialogAnchorProps } from '../service/DialogAnchor';
import { DialogAnchor } from '../service/DialogAnchor';

export const ContextMenuBackButton = ({
children,
Expand Down Expand Up @@ -96,7 +100,7 @@ type ContextMenuLevel = {
menuClassName?: string;
};

export type ContextMenuProps = Omit<ComponentProps<'div'>, 'children'> & {
type ContextMenuBaseProps = Omit<ComponentProps<'div'>, 'children'> & {
backLabel?: ReactNode;
items: ContextMenuItemComponent[];
Header?: ContextMenuHeaderComponent;
Expand All @@ -106,7 +110,24 @@ export type ContextMenuProps = Omit<ComponentProps<'div'>, 'children'> & {
onMenuLevelChange?: (level: number) => void;
};

export const ContextMenu = ({
/** When provided, ContextMenu renders inside DialogAnchor and wires menu level for submenu alignment. */
type ContextMenuAnchorProps = Partial<
Pick<
DialogAnchorProps,
| 'id'
| 'dialogManagerId'
| 'placement'
| 'referenceElement'
| 'tabIndex'
| 'trapFocus'
| 'allowFlip'
| 'focus'
>
>;

export type ContextMenuProps = ContextMenuBaseProps & ContextMenuAnchorProps;

function ContextMenuContent({
backLabel = 'Back',
className,
Header,
Expand All @@ -116,7 +137,7 @@ export const ContextMenu = ({
onClose,
onMenuLevelChange,
...props
}: ContextMenuProps) => {
}: ContextMenuBaseProps) {
const rootLevel = useMemo<ContextMenuLevel>(
() => ({
Header,
Expand Down Expand Up @@ -207,4 +228,65 @@ export const ContextMenu = ({
</ContextMenuRoot>
</ContextMenuContext.Provider>
);
}

export const ContextMenu = (props: ContextMenuProps) => {
const {
allowFlip,
dialogManagerId,
focus,
id,
placement,
referenceElement,
tabIndex,
trapFocus,
...menuProps
} = props;

const isAnchored = id != null;

const [menuLevel, setMenuLevel] = useState(1);
const open = useDialogIsOpen(id ?? '', dialogManagerId);

useEffect(() => {
if (isAnchored && !open) setMenuLevel(1);
}, [isAnchored, open]);

const content = (
<ContextMenuContent
{...menuProps}
onMenuLevelChange={isAnchored ? setMenuLevel : menuProps.onMenuLevelChange}
/>
);

if (isAnchored) {
const {
backLabel: _b,
Header: _h,
items: _i,
ItemsWrapper: _w,
menuClassName: _m,
onClose: _c,
onMenuLevelChange: _l,
...anchorDivProps
} = menuProps;
return (
<DialogAnchor
allowFlip={allowFlip}
dialogManagerId={dialogManagerId}
focus={focus}
id={id}
placement={placement}
referenceElement={referenceElement}
tabIndex={tabIndex}
trapFocus={trapFocus}
updateKey={menuLevel}
{...anchorDivProps}
>
{content}
</DialogAnchor>
);
}

return content;
};
24 changes: 21 additions & 3 deletions src/components/Dialog/base/ContextMenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,24 @@ const ContextMenuButtonWithSubmenu = ({

type ContextMenuButtonProps = BaseContextMenuButtonProps;

export const ContextMenuButton = (props: ContextMenuButtonProps) => (
<BaseContextMenuButton {...props} />
);
export const ContextMenuButton = ({
onBlur,
onFocus,
...props
}: ContextMenuButtonProps) => {
const [isFocused, setIsFocused] = useState(false);
return (
<BaseContextMenuButton
{...props}
aria-selected={isFocused ? 'true' : 'false'}
onBlur={(e) => {
setIsFocused(false);
onBlur?.(e);
}}
onFocus={(e) => {
setIsFocused(true);
onFocus?.(e);
}}
/>
);
};
22 changes: 17 additions & 5 deletions src/components/Dialog/service/DialogAnchor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import type { ComponentProps, PropsWithChildren } from 'react';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { FocusScope } from '@react-aria/focus';
import { DialogPortalEntry } from './DialogPortal';
import { useDialog, useDialogIsOpen } from '../hooks';
Expand Down Expand Up @@ -29,22 +29,33 @@ export function useDialogAnchor<T extends HTMLElement>({
placement,
});

// Freeze reference when dialog opens so submenus (e.g. ContextMenu level 2+) stay aligned to the original anchor
const frozenReferenceRef = useRef<HTMLElement | null>(null);
if (open && referenceElement && !frozenReferenceRef.current) {
frozenReferenceRef.current = referenceElement;
}
if (!open) {
frozenReferenceRef.current = null;
}
const effectiveReference = open ? frozenReferenceRef.current : referenceElement;

useEffect(() => {
refs.setReference(referenceElement);
}, [referenceElement, refs]);
refs.setReference(effectiveReference);
}, [effectiveReference, refs]);

useEffect(() => {
refs.setFloating(popperElement);
}, [popperElement, refs]);

useEffect(() => {
if (open && popperElement) {
if (open && popperElement && effectiveReference) {
// Re-run when reference becomes available (e.g. after ref is set) or when updateKey changes (e.g. submenu open)
// Since the popper's reference element might not be (and usually is not) visible
// all the time, it's safer to force popper update before showing it.
// update is non-null only if popperElement is non-null
update?.();
}
}, [open, placement, popperElement, update, updateKey]);
}, [open, placement, popperElement, update, updateKey, effectiveReference]);

if (popperElement && !open) {
setPopperElement(null);
Expand Down Expand Up @@ -83,6 +94,7 @@ export const DialogAnchor = ({
}: DialogAnchorProps) => {
const dialog = useDialog({ dialogManagerId, id });
const open = useDialogIsOpen(id, dialogManagerId);

const { setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
allowFlip,
open,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Form/styling/Form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ input[type='number'] {
.str-chat__form-field-error {
margin-left: 0.5rem;
}
}
}
4 changes: 3 additions & 1 deletion src/components/Form/styling/NumericInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
border-radius: var(--button-radius-full, 9999px);
color: var(--text-tertiary, #687385);
cursor: pointer;
transition: border-color 0.15s ease, color 0.15s ease;
transition:
border-color 0.15s ease,
color 0.15s ease;

&:hover:not(:disabled) {
color: var(--text-primary, #1a1b25);
Expand Down
25 changes: 18 additions & 7 deletions src/components/Form/styling/SwitchField.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,29 @@
// CSS variables aligned with Figma tokens; fallbacks from get_variable_defs.

.str-chat {
--str-chat__switch-field-background-color: var(--input-cards-bg, var(--background-core-surface-subtle));
--str-chat__switch-field-background-color: var(
--input-cards-bg,
var(--background-core-surface-subtle)
);
--str-chat__switch-field-border-radius: var(--radius-md);
--str-chat__switch-field-title-font-size: var(--typography-font-size-sm, 14px);
--str-chat__switch-field-title-font-weight: var(--typography-font-weight-medium, 500);
--str-chat__switch-field-title-line-height: var(--typography-line-height-tight, 16px);
--str-chat__switch-field-title-color: var(--text-primary, #1a1b25);
--str-chat__switch-field-description-font-size: var(--typography-font-size-xs, 12px);
--str-chat__switch-field-description-font-weight: var(--typography-font-weight-regular, 400);
--str-chat__switch-field-description-font-weight: var(
--typography-font-weight-regular,
400
);
--str-chat__switch-field-description-color: var(--text-tertiary, #687385);
--str-chat__switch-field__track-off-bg: var(--control-toggle-switch-bg, var(--border-core-on-surface, #a3acba));
--str-chat__switch-field__track-on-bg: var(--control-toggle-switch-bg-selected, #005fff);
--str-chat__switch-field__track-off-bg: var(
--control-toggle-switch-bg,
var(--border-core-on-surface, #a3acba)
);
--str-chat__switch-field__track-on-bg: var(
--control-toggle-switch-bg-selected,
#005fff
);
--str-chat__switch-field__track-thumb-bg: var(--base-white, #ffffff);
--str-chat__switch-field__track-height: 24px;
--str-chat__switch-field__track-radius: var(--button-radius-full, 9999px);
Expand All @@ -25,12 +37,11 @@
}

.str-chat__form__switch-field {

display: flex;
align-items: center;
gap: var(--spacing-sm);
width: 100%;
padding: var(--spacing-sm ) var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--str-chat__switch-field-background-color);
border-radius: var(--str-chat__switch-field-border-radius);
box-sizing: border-box;
Expand Down Expand Up @@ -137,7 +148,7 @@

.str-chat__form__switch-field__label,
.str-chat__form__switch-field__label__content {
flex: 1
flex: 1;
}

.str-chat__form__switch-field__label--as-error {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Form/styling/TextInputFieldset.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/components/Icons/styling/Icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
width: 1em;
height: 1em;
fill: currentColor;
}
}
2 changes: 1 addition & 1 deletion src/components/Icons/styling/index.scss
Original file line number Diff line number Diff line change
@@ -1 +1 @@
@use 'Icons';
@use 'Icons';
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

.str-chat__audio-recorder__recording-preview {
.str-chat__icon--microphone {
height: var(--icon-size-sm);
width: var(--icon-size-sm);
color: var(--button-destructive-text);
}
Expand Down
9 changes: 4 additions & 5 deletions src/components/Message/MessageSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isMessageBlocked,
isMessageBounced,
isMessageEdited,
isMessageErrorRetryable,
isOnlyEmojis,
messageHasAttachments,
messageHasGiphyAttachment,
Expand Down Expand Up @@ -52,7 +53,6 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
groupedByUser,
handleAction,
handleOpenThread,
handleRetry,
highlighted,
isMessageAIGenerated,
isMyMessage,
Expand Down Expand Up @@ -120,15 +120,14 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => {
const showReplyCountButton = !threadList && !!message.reply_count;
const showIsReplyInChannel =
!threadList && message.show_in_channel && message.parent_id;
const allowRetry = message.status === 'failed' && message.error?.status !== 403;
const allowRetry = isMessageErrorRetryable(message);
const isBounced = isMessageBounced(message);
const isEdited = isMessageEdited(message) && !isAIGenerated;

let handleClick: (() => void) | undefined = undefined;

if (allowRetry) {
handleClick = () => handleRetry(message);
} else if (isBounced) {
// todo: should we keep the behavior with click-on-blubble -> show the MessageBounceModal?
if (isBounced) {
handleClick = () => setIsBounceDialogOpen(true);
} else if (isEdited) {
handleClick = () => setEditedTimestampOpen((prev) => !prev);
Expand Down
Loading
Loading