|
| 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 | +}; |
0 commit comments