UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

212 lines (211 loc) 7.39 kB
import { A as useMessageComposerController, Gt as IconEmoji, Jn as usePopoverPosition, _r as useMessageComposerContext, j as useIsCooldownActive, or as useTranslationContext, qn as Button } from "./useNotificationApi.88c33caa.mjs"; import { useEffect, useState } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; import { BaseSearchSource, getTokenizedSuggestionDisplayName, getTriggerCharWithToken, insertItemWithTrigger, replaceWordWithEntity } from "stream-chat"; import mergeWith from "lodash.mergewith"; import PickerImport from "@emoji-mart/react"; //#region src/plugins/Emojis/EmojiPicker.tsx var Picker = PickerImport.default ?? PickerImport; var isShadowRoot = (node) => !!node.host; var defaultButtonClassName = "str-chat__emoji-picker-button"; var classNames = { pickerContainerClassName: "str-chat__message-textarea-emoji-picker-container", wrapperClassName: "str-chat__message-textarea-emoji-picker" }; var EmojiPicker = (props) => { const { t } = useTranslationContext("EmojiPicker"); const { textareaRef } = useMessageComposerContext("EmojiPicker"); const { textComposer } = useMessageComposerController(); const isCooldownActive = useIsCooldownActive(); const [displayPicker, setDisplayPicker] = useState(false); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const { refs, strategy, x, y } = usePopoverPosition({ offset: 8, placement: props.placement ?? "top-end" }); useEffect(() => { refs.setReference(referenceElement); }, [referenceElement, refs]); useEffect(() => { refs.setFloating(popperElement); }, [popperElement, refs]); const { pickerContainerClassName, wrapperClassName } = classNames; const { ButtonIconComponent = IconEmoji } = props; const pickerStyle = props.pickerProps?.style; useEffect(() => { if (!popperElement || !referenceElement) return; const handlePointerDown = (e) => { const target = e.target; const rootNode = target.getRootNode(); if (popperElement.contains(isShadowRoot(rootNode) ? rootNode.host : target) || referenceElement.contains(target)) return; setDisplayPicker(false); }; window.addEventListener("pointerdown", handlePointerDown); return () => window.removeEventListener("pointerdown", handlePointerDown); }, [referenceElement, popperElement]); return /* @__PURE__ */ jsxs("div", { className: props.wrapperClassName ?? wrapperClassName, children: [displayPicker && /* @__PURE__ */ jsx("div", { className: props.pickerContainerClassName ?? pickerContainerClassName, ref: setPopperElement, style: { left: x ?? 0, position: strategy, top: y ?? 0 }, children: /* @__PURE__ */ jsx(Picker, { data: async () => (await import("@emoji-mart/data")).default, onEmojiSelect: (e) => { const textarea = textareaRef.current; if (!textarea) return; textComposer.insertText({ text: e.native }); textarea.focus(); if (props.closeOnEmojiSelect) setDisplayPicker(false); }, ...props.pickerProps, style: { ...pickerStyle, "--shadow": "none" } }) }), /* @__PURE__ */ jsx(Button, { appearance: "ghost", "aria-expanded": displayPicker, "aria-label": t("aria/Emoji picker"), circular: true, className: props.buttonClassName ?? defaultButtonClassName, disabled: isCooldownActive, onClick: () => setDisplayPicker((cv) => !cv), ref: setReferenceElement, size: "sm", type: "button", variant: "secondary", children: ButtonIconComponent && /* @__PURE__ */ jsx(ButtonIconComponent, {}) })] }); }; //#endregion //#region src/plugins/Emojis/middleware/textComposerEmojiMiddleware.ts var EmojiSearchSource = class extends BaseSearchSource { constructor(emojiSearchIndex, options) { super(options); this.type = "emoji"; this.emojiSearchIndex = emojiSearchIndex; } async query(searchQuery) { if (searchQuery.length === 0) return { items: [], next: null }; return { items: (await this.emojiSearchIndex.search(searchQuery) ?? []).filter(Boolean).slice(0, 7).map(({ emoticons = [], id, name, native, skins = [] }) => { const [firstSkin] = skins; return { emoticons, id, name, native: native ?? firstSkin.native }; }), next: null }; } filterQueryResults(items) { return items.map((item) => ({ ...item, ...getTokenizedSuggestionDisplayName({ displayName: item.id, searchToken: this.searchQuery }) })); } }; var DEFAULT_OPTIONS = { minChars: 1, trigger: ":" }; /** * TextComposer middleware for mentions * Usage: * * const textComposer = new TextComposer(options); * * textComposer.use(new createTextComposerEmojiMiddleware(emojiSearchIndex, { * minChars: 2 * })); * * @param emojiSearchIndex * @param {{ * minChars: number; * trigger: string; * }} options * @returns */ var createTextComposerEmojiMiddleware = (emojiSearchIndex, options) => { const finalOptions = mergeWith(DEFAULT_OPTIONS, options ?? {}); const emojiSearchSource = new EmojiSearchSource(emojiSearchIndex); emojiSearchSource.activate(); return { id: "stream-io/emoji-middleware", handlers: { onChange: async ({ complete, forward, next, state }) => { if (!state.selection) return forward(); const triggerWithToken = getTriggerCharWithToken({ acceptTrailingSpaces: false, text: state.text.slice(0, state.selection.end), trigger: finalOptions.trigger }); if (!triggerWithToken || triggerWithToken.length < finalOptions.minChars) { const hasSuggestionsForTrigger = state.suggestions?.trigger === finalOptions.trigger; const newState = { ...state }; if (hasSuggestionsForTrigger && newState.suggestions) delete newState.suggestions; return next(newState); } if (triggerWithToken && triggerWithToken === finalOptions.trigger) emojiSearchSource.resetStateAndActivate(); const textWithReplacedWord = await replaceWordWithEntity({ caretPosition: state.selection.end, getEntityString: async (word) => { const { items } = await emojiSearchSource.query(word); const emoji = items.filter(Boolean).slice(0, 10).find(({ emoticons }) => !!emoticons?.includes(word)); if (!emoji) return null; const [firstSkin] = emoji.skins ?? []; return emoji.native ?? firstSkin.native; }, text: state.text }); if (textWithReplacedWord !== state.text) return complete({ ...state, suggestions: void 0, text: textWithReplacedWord }); return complete({ ...state, suggestions: { query: triggerWithToken.slice(1), searchSource: emojiSearchSource, trigger: finalOptions.trigger } }); }, onSuggestionItemSelect: ({ complete, forward, state }) => { const { selectedSuggestion } = state.change ?? {}; if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger) return forward(); emojiSearchSource.resetStateAndActivate(); return complete({ ...state, ...insertItemWithTrigger({ insertText: `${"native" in selectedSuggestion ? selectedSuggestion.native : ""} `, selection: state.selection, text: state.text, trigger: finalOptions.trigger }), suggestions: void 0 }); } } }; }; //#endregion export { EmojiPicker, createTextComposerEmojiMiddleware }; //# sourceMappingURL=emojis.mjs.map