Skip to content

Commit da7170b

Browse files
Temporary demo commit
1 parent b52c20b commit da7170b

5 files changed

Lines changed: 193 additions & 0 deletions

File tree

examples/vite/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
SegmentedReactionsList,
6969
} from './CustomMessageUi';
7070
import { ConfigurableMessageActions } from './CustomMessageActions';
71+
import { InlineEditableMessage } from './InlineEditMessage';
7172
import { SidebarToggle } from './Sidebar/SidebarToggle.tsx';
7273
import { CommandModeAttachmentSelector } from './CommandModeAttachmentSelector.tsx';
7374

@@ -424,6 +425,7 @@ const App = () => {
424425
HeaderStartContent: SidebarToggle,
425426
MessageActions: ConfigurableMessageActions,
426427
AttachmentSelector: CommandModeAttachmentSelector,
428+
Message: InlineEditableMessage,
427429
...messageUiOverrides,
428430
}}
429431
>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.app__inline-edit-message {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 0.5rem;
5+
padding: 0.5rem 0;
6+
width: 100%;
7+
}
8+
9+
.app__inline-edit-message__cancel {
10+
align-self: flex-end;
11+
background: transparent;
12+
border: 1px solid var(--str-chat__secondary-surface-color, #dbdde1);
13+
border-radius: 999px;
14+
color: var(--str-chat__text-color, inherit);
15+
cursor: pointer;
16+
font-size: 0.85rem;
17+
padding: 0.25rem 0.75rem;
18+
19+
&:hover {
20+
background: var(--str-chat__secondary-surface-color, #f7f7f8);
21+
}
22+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import {
2+
type ComponentProps,
3+
createContext,
4+
useCallback,
5+
useContext,
6+
useMemo,
7+
useState,
8+
} from 'react';
9+
import { MessageComposer as MessageComposerController } from 'stream-chat';
10+
import type { MessageComposerState } from 'stream-chat';
11+
import { useChannelStateContext } from 'stream-chat-react';
12+
import {
13+
ContextMenuButton,
14+
defaultMessageActionSet,
15+
IconEdit,
16+
MessageActions,
17+
type MessageActionSetItem,
18+
MessageComposer,
19+
MessageComposerControllerProvider,
20+
MessageUI,
21+
type MessageUIComponentProps,
22+
useChatContext,
23+
useComponentContext,
24+
useContextMenuContext,
25+
useMessageContext,
26+
useStateStore,
27+
useTranslationContext,
28+
WithComponents,
29+
} from 'stream-chat-react';
30+
31+
type InlineEditContextValue = {
32+
isEditing: boolean;
33+
startEditing: () => void;
34+
stopEditing: () => void;
35+
};
36+
37+
const InlineEditContext = createContext<InlineEditContextValue | undefined>(undefined);
38+
39+
const useInlineEditContext = () => {
40+
const value = useContext(InlineEditContext);
41+
if (!value) {
42+
throw new Error('useInlineEditContext must be used within an InlineEditableMessage');
43+
}
44+
return value;
45+
};
46+
47+
const InlineEditAction = () => {
48+
const { closeMenu } = useContextMenuContext();
49+
const { startEditing } = useInlineEditContext();
50+
const { t } = useTranslationContext();
51+
52+
return (
53+
<ContextMenuButton
54+
aria-label={t('aria/Edit Message Inline')}
55+
className='str-chat__message-actions-list-item-button'
56+
Icon={IconEdit}
57+
onClick={() => {
58+
startEditing();
59+
closeMenu();
60+
}}
61+
>
62+
{t('Edit inline')}
63+
</ContextMenuButton>
64+
);
65+
};
66+
67+
const inlineEditActionSetItem: MessageActionSetItem = {
68+
Component: InlineEditAction,
69+
placement: 'dropdown',
70+
type: 'editInline',
71+
};
72+
73+
const insertInlineEditAction = (
74+
actionSet: MessageActionSetItem[],
75+
): MessageActionSetItem[] => {
76+
const editIndex = actionSet.findIndex((item) => 'type' in item && item.type === 'edit');
77+
78+
if (editIndex < 0) return [...actionSet, inlineEditActionSetItem];
79+
80+
return [
81+
...actionSet.slice(0, editIndex),
82+
inlineEditActionSetItem,
83+
...actionSet.slice(editIndex),
84+
];
85+
};
86+
87+
const InlineEditComposer = ({ onExit }: { onExit: () => void }) => {
88+
const { t } = useTranslationContext();
89+
90+
return (
91+
<div className='app__inline-edit-message'>
92+
<MessageComposer preventClearingOnUnmount />
93+
<button className='app__inline-edit-message__cancel' onClick={onExit} type='button'>
94+
{t('Cancel')}
95+
</button>
96+
</div>
97+
);
98+
};
99+
100+
const selector = (state: MessageComposerState) => ({
101+
editing: state.editedMessage != null,
102+
});
103+
104+
export const InlineEditableMessage = (props: MessageUIComponentProps) => {
105+
const { client } = useChatContext();
106+
const { channel } = useChannelStateContext();
107+
const { message } = useMessageContext();
108+
109+
const { MessageActions: OuterMessageActions = MessageActions } = useComponentContext();
110+
111+
const [editingComposer] = useState(
112+
() =>
113+
new MessageComposerController({
114+
compositionContext: channel,
115+
client,
116+
config: { drafts: { enabled: false } },
117+
}),
118+
);
119+
120+
const { editing } = useStateStore(editingComposer.state, selector);
121+
122+
const startEditing = useCallback(() => {
123+
editingComposer.initState({ composition: message });
124+
}, [editingComposer, message]);
125+
const stopEditing = useCallback(() => {
126+
editingComposer.clear();
127+
}, [editingComposer]);
128+
129+
const contextValue = useMemo<InlineEditContextValue>(
130+
() => ({ isEditing: editing, startEditing, stopEditing }),
131+
[editing, startEditing, stopEditing],
132+
);
133+
134+
const MessageActionsWithInlineEdit = useMemo(() => {
135+
const Component = (actionsProps: ComponentProps<typeof MessageActions>) => {
136+
const messageActionSet = useMemo(
137+
() =>
138+
insertInlineEditAction(
139+
actionsProps.messageActionSet ?? defaultMessageActionSet,
140+
),
141+
[actionsProps.messageActionSet],
142+
);
143+
144+
return (
145+
<OuterMessageActions {...actionsProps} messageActionSet={messageActionSet} />
146+
);
147+
};
148+
Component.displayName = 'MessageActionsWithInlineEdit';
149+
return Component;
150+
}, [OuterMessageActions]);
151+
152+
if (editing) {
153+
return (
154+
<MessageComposerControllerProvider messageComposerController={editingComposer}>
155+
<InlineEditComposer onExit={stopEditing} />
156+
</MessageComposerControllerProvider>
157+
);
158+
}
159+
160+
return (
161+
<InlineEditContext.Provider value={contextValue}>
162+
<WithComponents overrides={{ MessageActions: MessageActionsWithInlineEdit }}>
163+
<MessageUI {...props} />
164+
</WithComponents>
165+
</InlineEditContext.Provider>
166+
);
167+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { InlineEditableMessage } from './InlineEditMessage';

examples/vite/src/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
@import url('./AppSettings/AppSettings.scss') layer(stream-app-overrides);
1010
@import url('./CustomMessageActions/CustomMessageActions.scss')
1111
layer(stream-app-overrides);
12+
@import url('./InlineEditMessage/InlineEditMessage.scss') layer(stream-app-overrides);
1213
@import url('./SystemNotification/SystemNotification.scss') layer(stream-app-overrides);
1314
@import url('./AccessibilityNavigation/ReturnToSkipNavigation.scss')
1415
layer(stream-app-overrides);

0 commit comments

Comments
 (0)