stream-chat-react
Version:
React components to create chat conversations or livestream style chat
212 lines (211 loc) • 7.39 kB
JavaScript
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