refactor: message input

This commit is contained in:
Tristan Yang
2023-10-27 17:52:19 +08:00
parent a1f0d10893
commit 61e014ae5f
26 changed files with 2601 additions and 1461 deletions
+41 -33
View File
@@ -1,11 +1,11 @@
{
"name": "vocechat-web",
"version": "0.5.11",
"version": "0.6.0",
"homepage": "https://voce.chat",
"dependencies": {
"@emoji-mart/react": "^1.1.1",
"@metamask/onboarding": "^1.0.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@radix-ui/react-popover": "^1.0.7",
"@react-oauth/google": "^0.11.1",
"@reduxjs/toolkit": "^1.9.7",
"@svgr/webpack": "^8.1.0",
@@ -13,26 +13,34 @@
"@toast-ui/editor": "^3.2.2",
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
"@toast-ui/react-editor": "^3.2.3",
"@udecode/plate": "16.9.1",
"@udecode/plate-break": "^24.5.2",
"@udecode/plate-combobox": "^24.5.2",
"@udecode/plate-common": "^24.5.2",
"@udecode/plate-emoji": "^24.5.2",
"@udecode/plate-floating": "^24.5.2",
"@udecode/plate-font": "^24.5.2",
"@udecode/plate-mention": "^24.5.2",
"@udecode/plate-node-id": "^24.5.2",
"@udecode/plate-paragraph": "^24.5.2",
"@udecode/plate-trailing-block": "^24.5.2",
"@uiball/loaders": "^1.3.0",
"agora-rtc-sdk-ng": "^4.19.0",
"broadcast-channel": "^5.3.0",
"agora-rtc-sdk-ng": "^4.19.2",
"broadcast-channel": "^5.5.1",
"browserslist": "^4.22.1",
"clsx": "^2.0.0",
"copy-image-clipboard": "^2.1.2",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"dayjs": "^1.11.10",
"emoji-mart": "5.5.2",
"eslint": "^8.50.0",
"firebase": "^10.4.0",
"eslint": "^8.52.0",
"firebase": "^10.5.2",
"framer-motion": "^10.16.4",
"fs-extra": "^11.1.1",
"heic2any": "^0.0.4",
"html-webpack-plugin": "^5.5.3",
"i18next": "^23.5.1",
"i18next": "^23.6.0",
"i18next-browser-languagedetector": "^7.1.0",
"i18next-http-backend": "^2.2.2",
"i18next-http-backend": "^2.3.0",
"linkify-plugin-mention": "^4.1.1",
"linkify-react": "^4.1.1",
"linkifyjs": "^4.1.1",
@@ -50,27 +58,28 @@
"react-error-boundary": "^4.0.11",
"react-helmet": "^6.1.0",
"react-hot-toast": "^2.4.1",
"react-i18next": "^13.2.2",
"react-i18next": "^13.3.1",
"react-redux": "^8.1.3",
"react-refresh": "0.14.0",
"react-router-dom": "^6.16.0",
"react-router-dom": "^6.17.0",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.3",
"react-use-wizard": "^2.2.3",
"react-use-wizard": "^2.2.4",
"react-viewport-list": "^7.1.1",
"react-virtuoso": "^4.6.0",
"react-virtuoso": "^4.6.2",
"rooks": "^7.14.1",
"slate": "^0.94.1",
"slate-history": "^0.93.0",
"slate-react": "0.94.2",
"slate": "^0.100.0",
"slate-history": "^0.100.0",
"slate-hyperscript": "^0.100.0",
"slate-react": "0.100.0",
"source-map-loader": "^4.0.1",
"style-loader": "^3.3.3",
"styled-components": "^5.3.11",
"tailwind-merge": "1.14.0",
"terser-webpack-plugin": "^5.3.9",
"tippy.js": "^6.3.7",
"typescript": "^5.2.2",
"wavesurfer.js": "^7.3.4",
"webpack": "^5.88.2",
"wavesurfer.js": "^7.4.2",
"webpack": "^5.89.0",
"webpack-dev-server": "^4.15.1",
"webpack-manifest-plugin": "^5.0.0",
"workbox-core": "^7.0.0",
@@ -107,24 +116,23 @@
"devDependencies": {
"@babel/core": "^7.23.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@ianvs/prettier-plugin-sort-imports": "^4.1.0",
"@types/emoji-mart": "^3.0.10",
"@types/masonry-layout": "^4.2.5",
"@types/node": "^20.8.2",
"@types/react": "18.2.25",
"@types/react-dom": "^18.2.10",
"@types/react-helmet": "^6.1.7",
"@types/react-syntax-highlighter": "^15.5.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
"@types/masonry-layout": "^4.2.6",
"@types/node": "^20.8.9",
"@types/react": "18.2.33",
"@types/react-dom": "^18.2.14",
"@types/react-helmet": "^6.1.8",
"@types/react-syntax-highlighter": "^15.5.9",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"@welldone-software/why-did-you-render": "^7.0.1",
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.3",
"babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "^10.0.1",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"gh-pages": "^6.0.0",
@@ -134,6 +142,6 @@
"postcss-loader": "^7.3.3",
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3"
"tailwindcss": "^3.3.5"
}
}
+119
View File
@@ -0,0 +1,119 @@
'use client';
// import React, { useRef } from 'react';
import { ClipboardEvent, MutableRefObject, useEffect, useRef } from 'react';
import { Plate, PlateEditor } from '@udecode/plate-common';
import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph';
import { useLocalstorageState } from 'rooks';
import { plugins } from './plugins';
import { getMessageFromPlateValues, ParagraphInput } from '@/utils';
import { Editor } from './plate-ui/editor';
import { MentionCombobox } from './plate-ui/mention-combobox';
import { EmojiDropdownInput } from './plate-ui/emoji-dropdown-input';
import { MentionData, MessageWithMentions } from '@/types/message';
import { useAppSelector } from '@/app/store';
import { shallowEqual } from 'react-redux';
import { ChatContext } from '@/types/common';
import useUploadFile from '@/hooks/useUploadFile';
export const initialMessage = [
{
id: '1',
type: ELEMENT_PARAGRAPH,
children: [{ text: '' }],
},
];
type Props = {
editorRef: MutableRefObject<PlateEditor | null>;
members: number[];
debug?: boolean;
id: string;
placeholder: string;
sendMessage: () => void;
updateMessage: (msg:MessageWithMentions) => void;
};
export default function MessageInput({
editorRef,
members,
debug = false,
placeholder,
id,
sendMessage,
updateMessage
}: Props) {
const [context, to] = id.split("_") as [ChatContext, number];
const { addStageFile } = useUploadFile({ context, id: to });
const userData= useAppSelector(store=>store.users.byId,shallowEqual)
const editorContainerRef = useRef(null);
const [input, setInput] = useLocalstorageState(id, initialMessage);
const handleSendMessage = () => {
sendMessage();
};
useEffect(() => {
const text = getMessageFromPlateValues(input as ParagraphInput[]);
updateMessage(text)
}, [input])
// 监听文件粘贴事件
useEffect(() => {
const handlePasteEvent = (evt: ClipboardEvent<Window>) => {
const files = [...evt.clipboardData.files];
if (files.length) {
const filesData = files.map((file) => {
const { size, type, name } = file;
// console.log("paste event name", name);
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
addStageFile(filesData);
}
};
// @ts-ignore
window.addEventListener("paste", handlePasteEvent,true);
return () => {
// @ts-ignore
window.removeEventListener("paste", handlePasteEvent,true);
};
}, [id]);
const items:MentionData[]=members.map(uid=>({
key:`${uid}`,
text:userData[uid].name,
data:{
uid,
}
}))
return (
<>
<div ref={editorContainerRef} className="input w-full pr-14 md:pr-0 max-h-[50vh] overflow-auto text-sm text-gray-600 dark:text-white">
<Plate
// @ts-ignore
editorRef={editorRef}
id={id}
onChange={(values) => {
setInput(values);
}}
plugins={plugins}
value={input}
>
<Editor
sendMessage={handleSendMessage}
// className="px-2 py-3"
autoFocus
placeholder={placeholder}
/>
<MentionCombobox items={items} />
<div className="absolute left-3 bottom-3">
<EmojiDropdownInput />
</div>
</Plate>
</div>
{debug && (
<div className="whitespace-pre p-2 text-xs">
{JSON.stringify(input, null, 2)}
</div>
)}
</>
);
}
@@ -0,0 +1,156 @@
'use client';
import React, { useEffect } from 'react';
import * as Popover from '@radix-ui/react-popover';
import {
comboboxActions,
ComboboxContentItemProps,
ComboboxContentProps,
ComboboxProps,
Data,
NoData,
TComboboxItem,
useActiveComboboxStore,
useComboboxContent,
useComboboxContentState,
useComboboxControls,
useComboboxItem,
useComboboxSelectors,
} from '@udecode/plate-combobox';
import { useEditorState, useEventEditorSelectors } from '@udecode/plate-common';
import { createVirtualRef } from '@udecode/plate-floating';
import { cn } from '@/utils';
export function ComboboxItem<TData extends Data = NoData>({
combobox,
index,
item,
onRenderItem,
}: ComboboxContentItemProps<TData>) {
const { props } = useComboboxItem({ item, index, combobox, onRenderItem });
return (
<div
className={cn(
'relative flex cursor-pointer select-none items-center rounded-sm text-sm outline-none ml-1 my-1 transition-colors',
'hover:bg-gray-300/50 dark:hover:bg-gray-500/50 hover:text-gray-500 data-[highlighted=true]:bg-gray-300/50 dark:data-[highlighted=true]:bg-gray-500/50 data-[highlighted=true]:text-gray-500'
)}
{...props}
/>
);
}
export function ComboboxContent<TData extends Data = NoData>(
props: ComboboxContentProps<TData>
) {
const {
component: Component,
items,
portalElement,
combobox,
onRenderItem,
} = props;
const editor = useEditorState();
const filteredItems =
useComboboxSelectors.filteredItems() as TComboboxItem<TData>[];
const activeComboboxStore = useActiveComboboxStore()!;
const state = useComboboxContentState({ items, combobox });
const { menuProps, targetRange } = useComboboxContent(state);
return (
<Popover.Root open>
<Popover.PopoverAnchor
virtualRef={createVirtualRef(editor, targetRange ?? undefined)}
/>
<Popover.Portal container={portalElement}>
<Popover.Content
{...menuProps}
sideOffset={5}
side="bottom"
align="start"
className={cn(
'z-[500] m-0 max-h-[400px] w-[200px] overflow-x-hidden overflow-y-scroll rounded-md bg-slate-100 dark:bg-slate-900 py-2 shadow-md'
)}
onOpenAutoFocus={(event) => event.preventDefault()}
>
{Component ? Component({ store: activeComboboxStore }) : null}
{filteredItems.map((item, index) => (
<ComboboxItem
key={item.key}
item={item}
combobox={combobox}
index={index}
onRenderItem={onRenderItem}
/>
))}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
/**
* Register the combobox id, trigger, onSelectItem
* Renders the combobox if active.
*/
export function Combobox<TData extends Data = NoData>({
id,
trigger,
searchPattern,
onSelectItem,
controlled,
maxSuggestions,
filter,
sort,
disabled: _disabled,
...props
}: ComboboxProps<TData>) {
const storeItems = useComboboxSelectors.items();
const disabled =
_disabled ?? (storeItems.length === 0 && !props.items?.length);
const focusedEditorId = useEventEditorSelectors.focus?.();
const combobox = useComboboxControls();
const activeId = useComboboxSelectors.activeId();
const editor = useEditorState();
useEffect(() => {
comboboxActions.setComboboxById({
id,
trigger,
searchPattern,
controlled,
onSelectItem,
maxSuggestions,
filter,
sort,
});
}, [
id,
trigger,
searchPattern,
controlled,
onSelectItem,
maxSuggestions,
filter,
sort,
]);
if (
!combobox ||
!editor.selection ||
focusedEditorId !== editor.id ||
activeId !== id ||
disabled
) {
return null;
}
return <ComboboxContent combobox={combobox} {...props} />;
}
@@ -0,0 +1,60 @@
import React from 'react';
import { PlateContent, useEditorRef } from '@udecode/plate-common';
import { useKey } from 'rooks';
import { cn, isMobile } from '@/utils';
import type { PlateContentProps } from '@udecode/plate-common';
export type EditorProps = PlateContentProps & {
sendMessage: () => void;
};
const Editor = React.forwardRef<HTMLDivElement, EditorProps>(
({ id, sendMessage, className, disabled, readOnly, ...props }, ref) => {
const editorRef = useEditorRef(id);
useKey(
'Enter',
(evt) => {
console.log('enter', editorRef, evt);
if (!editorRef) return;
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.isComposing) {
return true;
}
evt.preventDefault();
sendMessage();
editorRef.reset();
},
{
when: !isMobile(),
// @ts-ignore
target: ref,
}
);
return (
<div
ref={ref}
className="relative h-fit w-full pl-10"
>
<PlateContent
className={cn(
'relative overflow-hidden whitespace-pre-wrap break-words',
'w-full rounded-md bg-background ring-offset-background placeholder:text-gray-500 focus-visible:outline-none',
'[&_[data-slate-placeholder]]:opacity-30',
'[&_[data-slate-placeholder]]:top-[auto_!important]',
className
)}
spellCheck={false}
disableDefaultStyles
readOnly={disabled ?? readOnly}
aria-disabled={disabled}
{...props}
/>
</div>
);
}
);
Editor.displayName = 'Editor';
export { Editor };
@@ -0,0 +1,41 @@
import React from 'react';
import { ComboboxItemProps } from '@udecode/plate-combobox';
import {
EmojiItemData,
KEY_EMOJI,
TEmojiCombobox,
useEmojiComboboxState,
} from '@udecode/plate-emoji';
import { Combobox } from './combobox';
export function EmojiComboboxItem({ item }: ComboboxItemProps<EmojiItemData>) {
const {
data: { id, emoji },
} = item;
return (
<div>
{emoji} :{id}:
</div>
);
}
export function EmojiCombobox<TData extends EmojiItemData = EmojiItemData>({
pluginKey = KEY_EMOJI,
id = pluginKey,
...props
}: TEmojiCombobox<TData>) {
const { trigger, onSelectItem } = useEmojiComboboxState({ pluginKey });
return (
<Combobox
id={id}
trigger={trigger}
controlled
onSelectItem={onSelectItem}
onRenderItem={EmojiComboboxItem}
{...props}
/>
);
}
@@ -0,0 +1,53 @@
import React from 'react';
import {
Emoji,
EmojiDropdownMenuOptions,
useEmojiDropdownMenuState,
} from '@udecode/plate-emoji';
import IconSmile from '@/assets/icons/emoji.smile.svg'
import { EmojiPopup } from './emoji-popup';
import { emojiCategoryIcons, emojiSearchIcons } from './emoji-icons';
import { EmojiPicker } from './emoji-picker';
type EmojiDropdownMenuProps = {
options?: EmojiDropdownMenuOptions;
}&{
onSelectEmoji?: (emoji: Emoji) => void;
};
export function EmojiDropdownInput({
options,
onSelectEmoji,
...props
}: EmojiDropdownMenuProps) {
const { isOpen, setIsOpen, emojiPickerState } =
useEmojiDropdownMenuState(options);
const handleClick = () => {
setIsOpen(!isOpen);
};
const rest=onSelectEmoji?{onSelectEmoji}:{}
return (
<EmojiPopup
control={
<button className="relative h-6 w-6" onClick={handleClick} {...props}>
<IconSmile className="w-full h-full" />
</button>
}
isOpen={isOpen}
setIsOpen={setIsOpen}
>
<EmojiPicker
{...emojiPickerState}
isOpen={isOpen}
setIsOpen={setIsOpen}
icons={{
categories: emojiCategoryIcons,
search: emojiSearchIcons,
}}
settings={options?.settings}
{...rest}
/>
</EmojiPopup>
);
}
@@ -0,0 +1,156 @@
import React from 'react';
import { EmojiCategoryList } from '@udecode/plate-emoji';
export const emojiCategoryIcons: Record<
EmojiCategoryList,
{ outline: React.ReactElement; solid: React.ReactElement }
> = {
activity: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 0C5.373 0 0 5.372 0 12c0 6.627 5.373 12 12 12 6.628 0 12-5.373 12-12 0-6.628-5.372-12-12-12m9.949 11H17.05c.224-2.527 1.232-4.773 1.968-6.113A9.966 9.966 0 0 1 21.949 11M13 11V2.051a9.945 9.945 0 0 1 4.432 1.564c-.858 1.491-2.156 4.22-2.392 7.385H13zm-2 0H8.961c-.238-3.165-1.536-5.894-2.393-7.385A9.95 9.95 0 0 1 11 2.051V11zm0 2v8.949a9.937 9.937 0 0 1-4.432-1.564c.857-1.492 2.155-4.221 2.393-7.385H11zm4.04 0c.236 3.164 1.534 5.893 2.392 7.385A9.92 9.92 0 0 1 13 21.949V13h2.04zM4.982 4.887C5.718 6.227 6.726 8.473 6.951 11h-4.9a9.977 9.977 0 0 1 2.931-6.113M2.051 13h4.9c-.226 2.527-1.233 4.771-1.969 6.113A9.972 9.972 0 0 1 2.051 13m16.967 6.113c-.735-1.342-1.744-3.586-1.968-6.113h4.899a9.961 9.961 0 0 1-2.931 6.113" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M16.17 337.5c0 44.98 7.565 83.54 13.98 107.9C35.22 464.3 50.46 496 174.9 496c9.566 0 19.59-.4707 29.84-1.271L17.33 307.3C16.53 317.6 16.17 327.7 16.17 337.5zM495.8 174.5c0-44.98-7.565-83.53-13.98-107.9c-4.688-17.54-18.34-31.23-36.04-35.95C435.5 27.91 392.9 16 337 16c-9.564 0-19.59 .4707-29.84 1.271l187.5 187.5C495.5 194.4 495.8 184.3 495.8 174.5zM26.77 248.8l236.3 236.3c142-36.1 203.9-150.4 222.2-221.1L248.9 26.87C106.9 62.96 45.07 177.2 26.77 248.8zM256 335.1c0 9.141-7.474 16-16 16c-4.094 0-8.188-1.564-11.31-4.689L164.7 283.3C161.6 280.2 160 276.1 160 271.1c0-8.529 6.865-16 16-16c4.095 0 8.189 1.562 11.31 4.688l64.01 64C254.4 327.8 256 331.9 256 335.1zM304 287.1c0 9.141-7.474 16-16 16c-4.094 0-8.188-1.564-11.31-4.689L212.7 235.3C209.6 232.2 208 228.1 208 223.1c0-9.141 7.473-16 16-16c4.094 0 8.188 1.562 11.31 4.688l64.01 64.01C302.5 279.8 304 283.9 304 287.1zM256 175.1c0-9.141 7.473-16 16-16c4.094 0 8.188 1.562 11.31 4.688l64.01 64.01c3.125 3.125 4.688 7.219 4.688 11.31c0 9.133-7.468 16-16 16c-4.094 0-8.189-1.562-11.31-4.688l-64.01-64.01C257.6 184.2 256 180.1 256 175.1z" />
</svg>
),
},
custom: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M417.1 368c-5.937 10.27-16.69 16-27.75 16c-5.422 0-10.92-1.375-15.97-4.281L256 311.4V448c0 17.67-14.33 32-31.1 32S192 465.7 192 448V311.4l-118.3 68.29C68.67 382.6 63.17 384 57.75 384c-11.06 0-21.81-5.734-27.75-16c-8.828-15.31-3.594-34.88 11.72-43.72L159.1 256L41.72 187.7C26.41 178.9 21.17 159.3 29.1 144C36.63 132.5 49.26 126.7 61.65 128.2C65.78 128.7 69.88 130.1 73.72 132.3L192 200.6V64c0-17.67 14.33-32 32-32S256 46.33 256 64v136.6l118.3-68.29c3.838-2.213 7.939-3.539 12.07-4.051C398.7 126.7 411.4 132.5 417.1 144c8.828 15.31 3.594 34.88-11.72 43.72L288 256l118.3 68.28C421.6 333.1 426.8 352.7 417.1 368z" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M417.1 368c-5.937 10.27-16.69 16-27.75 16c-5.422 0-10.92-1.375-15.97-4.281L256 311.4V448c0 17.67-14.33 32-31.1 32S192 465.7 192 448V311.4l-118.3 68.29C68.67 382.6 63.17 384 57.75 384c-11.06 0-21.81-5.734-27.75-16c-8.828-15.31-3.594-34.88 11.72-43.72L159.1 256L41.72 187.7C26.41 178.9 21.17 159.3 29.1 144C36.63 132.5 49.26 126.7 61.65 128.2C65.78 128.7 69.88 130.1 73.72 132.3L192 200.6V64c0-17.67 14.33-32 32-32S256 46.33 256 64v136.6l118.3-68.29c3.838-2.213 7.939-3.539 12.07-4.051C398.7 126.7 411.4 132.5 417.1 144c8.828 15.31 3.594 34.88-11.72 43.72L288 256l118.3 68.28C421.6 333.1 426.8 352.7 417.1 368z" />
</svg>
),
},
flags: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0l6.084 24H8L1.916 0zM21 5h-4l-1-4H4l3 12h3l1 4h13L21 5zM6.563 3h7.875l2 8H8.563l-2-8zm8.832 10l-2.856 1.904L12.063 13h3.332zM19 13l-1.5-6h1.938l2 8H16l3-2z" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M64 496C64 504.8 56.75 512 48 512h-32C7.25 512 0 504.8 0 496V32c0-17.75 14.25-32 32-32s32 14.25 32 32V496zM476.3 0c-6.365 0-13.01 1.35-19.34 4.233c-45.69 20.86-79.56 27.94-107.8 27.94c-59.96 0-94.81-31.86-163.9-31.87C160.9 .3055 131.6 4.867 96 15.75v350.5c32-9.984 59.87-14.1 84.85-14.1c73.63 0 124.9 31.78 198.6 31.78c31.91 0 68.02-5.971 111.1-23.09C504.1 355.9 512 344.4 512 332.1V30.73C512 11.1 495.3 0 476.3 0z" />
</svg>
),
},
foods: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M17 4.978c-1.838 0-2.876.396-3.68.934.513-1.172 1.768-2.934 4.68-2.934a1 1 0 0 0 0-2c-2.921 0-4.629 1.365-5.547 2.512-.064.078-.119.162-.18.244C11.73 1.838 10.798.023 9.207.023 8.579.022 7.85.306 7 .978 5.027 2.54 5.329 3.902 6.492 4.999 3.609 5.222 0 7.352 0 12.969c0 4.582 4.961 11.009 9 11.009 1.975 0 2.371-.486 3-1 .629.514 1.025 1 3 1 4.039 0 9-6.418 9-11 0-5.953-4.055-8-7-8M8.242 2.546c.641-.508.943-.523.965-.523.426.169.975 1.405 1.357 3.055-1.527-.629-2.741-1.352-2.98-1.846.059-.112.241-.356.658-.686M15 21.978c-1.08 0-1.21-.109-1.559-.402l-.176-.146c-.367-.302-.816-.452-1.266-.452s-.898.15-1.266.452l-.176.146c-.347.292-.477.402-1.557.402-2.813 0-7-5.389-7-9.009 0-5.823 4.488-5.991 5-5.991 1.939 0 2.484.471 3.387 1.251l.323.276a1.995 1.995 0 0 0 2.58 0l.323-.276c.902-.78 1.447-1.251 3.387-1.251.512 0 5 .168 5 6 0 3.617-4.187 9-7 9" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M481.9 270.1C490.9 279.1 496 291.3 496 304C496 316.7 490.9 328.9 481.9 337.9C472.9 346.9 460.7 352 448 352H64C51.27 352 39.06 346.9 30.06 337.9C21.06 328.9 16 316.7 16 304C16 291.3 21.06 279.1 30.06 270.1C39.06 261.1 51.27 256 64 256H448C460.7 256 472.9 261.1 481.9 270.1zM475.3 388.7C478.3 391.7 480 395.8 480 400V416C480 432.1 473.3 449.3 461.3 461.3C449.3 473.3 432.1 480 416 480H96C79.03 480 62.75 473.3 50.75 461.3C38.74 449.3 32 432.1 32 416V400C32 395.8 33.69 391.7 36.69 388.7C39.69 385.7 43.76 384 48 384H464C468.2 384 472.3 385.7 475.3 388.7zM50.39 220.8C45.93 218.6 42.03 215.5 38.97 211.6C35.91 207.7 33.79 203.2 32.75 198.4C31.71 193.5 31.8 188.5 32.99 183.7C54.98 97.02 146.5 32 256 32C365.5 32 457 97.02 479 183.7C480.2 188.5 480.3 193.5 479.2 198.4C478.2 203.2 476.1 207.7 473 211.6C469.1 215.5 466.1 218.6 461.6 220.8C457.2 222.9 452.3 224 447.3 224H64.67C59.73 224 54.84 222.9 50.39 220.8zM372.7 116.7C369.7 119.7 368 123.8 368 128C368 131.2 368.9 134.3 370.7 136.9C372.5 139.5 374.1 141.6 377.9 142.8C380.8 143.1 384 144.3 387.1 143.7C390.2 143.1 393.1 141.6 395.3 139.3C397.6 137.1 399.1 134.2 399.7 131.1C400.3 128 399.1 124.8 398.8 121.9C397.6 118.1 395.5 116.5 392.9 114.7C390.3 112.9 387.2 111.1 384 111.1C379.8 111.1 375.7 113.7 372.7 116.7V116.7zM244.7 84.69C241.7 87.69 240 91.76 240 96C240 99.16 240.9 102.3 242.7 104.9C244.5 107.5 246.1 109.6 249.9 110.8C252.8 111.1 256 112.3 259.1 111.7C262.2 111.1 265.1 109.6 267.3 107.3C269.6 105.1 271.1 102.2 271.7 99.12C272.3 96.02 271.1 92.8 270.8 89.88C269.6 86.95 267.5 84.45 264.9 82.7C262.3 80.94 259.2 79.1 256 79.1C251.8 79.1 247.7 81.69 244.7 84.69V84.69zM116.7 116.7C113.7 119.7 112 123.8 112 128C112 131.2 112.9 134.3 114.7 136.9C116.5 139.5 118.1 141.6 121.9 142.8C124.8 143.1 128 144.3 131.1 143.7C134.2 143.1 137.1 141.6 139.3 139.3C141.6 137.1 143.1 134.2 143.7 131.1C144.3 128 143.1 124.8 142.8 121.9C141.6 118.1 139.5 116.5 136.9 114.7C134.3 112.9 131.2 111.1 128 111.1C123.8 111.1 119.7 113.7 116.7 116.7L116.7 116.7z" />
</svg>
),
},
frequent: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 4h-2l-.001 7H9v2h2v2h2v-2h4v-2h-4z" />
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0m0 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512zM232 256C232 264 236 271.5 242.7 275.1L338.7 339.1C349.7 347.3 364.6 344.3 371.1 333.3C379.3 322.3 376.3 307.4 365.3 300L280 243.2V120C280 106.7 269.3 96 255.1 96C242.7 96 231.1 106.7 231.1 120L232 256z" />
</svg>
),
},
nature: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M15.5 8a1.5 1.5 0 1 0 .001 3.001A1.5 1.5 0 0 0 15.5 8M8.5 8a1.5 1.5 0 1 0 .001 3.001A1.5 1.5 0 0 0 8.5 8" />
<path d="M18.933 0h-.027c-.97 0-2.138.787-3.018 1.497-1.274-.374-2.612-.51-3.887-.51-1.285 0-2.616.133-3.874.517C7.245.79 6.069 0 5.093 0h-.027C3.352 0 .07 2.67.002 7.026c-.039 2.479.276 4.238 1.04 5.013.254.258.882.677 1.295.882.191 3.177.922 5.238 2.536 6.38.897.637 2.187.949 3.2 1.102C8.04 20.6 8 20.795 8 21c0 1.773 2.35 3 4 3 1.648 0 4-1.227 4-3 0-.201-.038-.393-.072-.586 2.573-.385 5.435-1.877 5.925-7.587.396-.22.887-.568 1.104-.788.763-.774 1.079-2.534 1.04-5.013C23.929 2.67 20.646 0 18.933 0M3.223 9.135c-.237.281-.837 1.155-.884 1.238-.15-.41-.368-1.349-.337-3.291.051-3.281 2.478-4.972 3.091-5.031.256.015.731.27 1.265.646-1.11 1.171-2.275 2.915-2.352 5.125-.133.546-.398.858-.783 1.313M12 22c-.901 0-1.954-.693-2-1 0-.654.475-1.236 1-1.602V20a1 1 0 1 0 2 0v-.602c.524.365 1 .947 1 1.602-.046.307-1.099 1-2 1m3-3.48v.02a4.752 4.752 0 0 0-1.262-1.02c1.092-.516 2.239-1.334 2.239-2.217 0-1.842-1.781-2.195-3.977-2.195-2.196 0-3.978.354-3.978 2.195 0 .883 1.148 1.701 2.238 2.217A4.8 4.8 0 0 0 9 18.539v-.025c-1-.076-2.182-.281-2.973-.842-1.301-.92-1.838-3.045-1.853-6.478l.023-.041c.496-.826 1.49-1.45 1.804-3.102 0-2.047 1.357-3.631 2.362-4.522C9.37 3.178 10.555 3 11.948 3c1.447 0 2.685.192 3.733.57 1 .9 2.316 2.465 2.316 4.48.313 1.651 1.307 2.275 1.803 3.102.035.058.068.117.102.178-.059 5.967-1.949 7.01-4.902 7.19m6.628-8.202c-.037-.065-.074-.13-.113-.195a7.587 7.587 0 0 0-.739-.987c-.385-.455-.648-.768-.782-1.313-.076-2.209-1.241-3.954-2.353-5.124.531-.376 1.004-.63 1.261-.647.636.071 3.044 1.764 3.096 5.031.027 1.81-.347 3.218-.37 3.235" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M332.7 19.85C334.6 8.395 344.5 0 356.1 0C363.6 0 370.6 3.52 375.1 9.502L392 32H444.1C456.8 32 469.1 37.06 478.1 46.06L496 64H552C565.3 64 576 74.75 576 88V112C576 156.2 540.2 192 496 192H426.7L421.6 222.5L309.6 158.5L332.7 19.85zM448 64C439.2 64 432 71.16 432 80C432 88.84 439.2 96 448 96C456.8 96 464 88.84 464 80C464 71.16 456.8 64 448 64zM416 256.1V480C416 497.7 401.7 512 384 512H352C334.3 512 320 497.7 320 480V364.8C295.1 377.1 268.8 384 240 384C211.2 384 184 377.1 160 364.8V480C160 497.7 145.7 512 128 512H96C78.33 512 64 497.7 64 480V249.8C35.23 238.9 12.64 214.5 4.836 183.3L.9558 167.8C-3.331 150.6 7.094 133.2 24.24 128.1C41.38 124.7 58.76 135.1 63.05 152.2L66.93 167.8C70.49 182 83.29 191.1 97.97 191.1H303.8L416 256.1z" />
</svg>
),
},
objects: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 0a9 9 0 0 0-5 16.482V21s2.035 3 5 3 5-3 5-3v-4.518A9 9 0 0 0 12 0zm0 2c3.86 0 7 3.141 7 7s-3.14 7-7 7-7-3.141-7-7 3.14-7 7-7zM9 17.477c.94.332 1.946.523 3 .523s2.06-.19 3-.523v.834c-.91.436-1.925.689-3 .689a6.924 6.924 0 0 1-3-.69v-.833zm.236 3.07A8.854 8.854 0 0 0 12 21c.965 0 1.888-.167 2.758-.451C14.155 21.173 13.153 22 12 22c-1.102 0-2.117-.789-2.764-1.453z" />
<path d="M14.745 12.449h-.004c-.852-.024-1.188-.858-1.577-1.824-.421-1.061-.703-1.561-1.182-1.566h-.009c-.481 0-.783.497-1.235 1.537-.436.982-.801 1.811-1.636 1.791l-.276-.043c-.565-.171-.853-.691-1.284-1.794-.125-.313-.202-.632-.27-.913-.051-.213-.127-.53-.195-.634C7.067 9.004 7.039 9 6.99 9A1 1 0 0 1 7 7h.01c1.662.017 2.015 1.373 2.198 2.134.486-.981 1.304-2.058 2.797-2.075 1.531.018 2.28 1.153 2.731 2.141l.002-.008C14.944 8.424 15.327 7 16.979 7h.032A1 1 0 1 1 17 9h-.011c-.149.076-.256.474-.319.709a6.484 6.484 0 0 1-.311.951c-.429.973-.79 1.789-1.614 1.789" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
<path d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z" />
</svg>
),
},
people: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0m0 22C6.486 22 2 17.514 2 12S6.486 2 12 2s10 4.486 10 10-4.486 10-10 10" />
<path d="M8 7a2 2 0 1 0-.001 3.999A2 2 0 0 0 8 7M16 7a2 2 0 1 0-.001 3.999A2 2 0 0 0 16 7M15.232 15c-.693 1.195-1.87 2-3.349 2-1.477 0-2.655-.805-3.347-2H15m3-2H6a6 6 0 1 0 12 0" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM256 432C332.1 432 396.2 382 415.2 314.1C419.1 300.4 407.8 288 393.6 288H118.4C104.2 288 92.92 300.4 96.76 314.1C115.8 382 179.9 432 256 432V432zM176.4 160C158.7 160 144.4 174.3 144.4 192C144.4 209.7 158.7 224 176.4 224C194 224 208.4 209.7 208.4 192C208.4 174.3 194 160 176.4 160zM336.4 224C354 224 368.4 209.7 368.4 192C368.4 174.3 354 160 336.4 160C318.7 160 304.4 174.3 304.4 192C304.4 209.7 318.7 224 336.4 224z" />
</svg>
),
},
places: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M6.5 12C5.122 12 4 13.121 4 14.5S5.122 17 6.5 17 9 15.879 9 14.5 7.878 12 6.5 12m0 3c-.275 0-.5-.225-.5-.5s.225-.5.5-.5.5.225.5.5-.225.5-.5.5M17.5 12c-1.378 0-2.5 1.121-2.5 2.5s1.122 2.5 2.5 2.5 2.5-1.121 2.5-2.5-1.122-2.5-2.5-2.5m0 3c-.275 0-.5-.225-.5-.5s.225-.5.5-.5.5.225.5.5-.225.5-.5.5" />
<path d="M22.482 9.494l-1.039-.346L21.4 9h.6c.552 0 1-.439 1-.992 0-.006-.003-.008-.003-.008H23c0-1-.889-2-1.984-2h-.642l-.731-1.717C19.262 3.012 18.091 2 16.764 2H7.236C5.909 2 4.738 3.012 4.357 4.283L3.626 6h-.642C1.889 6 1 7 1 8h.003S1 8.002 1 8.008C1 8.561 1.448 9 2 9h.6l-.043.148-1.039.346a2.001 2.001 0 0 0-1.359 2.097l.751 7.508a1 1 0 0 0 .994.901H3v1c0 1.103.896 2 2 2h2c1.104 0 2-.897 2-2v-1h6v1c0 1.103.896 2 2 2h2c1.104 0 2-.897 2-2v-1h1.096a.999.999 0 0 0 .994-.901l.751-7.508a2.001 2.001 0 0 0-1.359-2.097M6.273 4.857C6.402 4.43 6.788 4 7.236 4h9.527c.448 0 .834.43.963.857L19.313 9H4.688l1.585-4.143zM7 21H5v-1h2v1zm12 0h-2v-1h2v1zm2.189-3H2.811l-.662-6.607L3 11h18l.852.393L21.189 18z" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M39.61 196.8L74.8 96.29C88.27 57.78 124.6 32 165.4 32H346.6C387.4 32 423.7 57.78 437.2 96.29L472.4 196.8C495.6 206.4 512 229.3 512 256V448C512 465.7 497.7 480 480 480H448C430.3 480 416 465.7 416 448V400H96V448C96 465.7 81.67 480 64 480H32C14.33 480 0 465.7 0 448V256C0 229.3 16.36 206.4 39.61 196.8V196.8zM109.1 192H402.9L376.8 117.4C372.3 104.6 360.2 96 346.6 96H165.4C151.8 96 139.7 104.6 135.2 117.4L109.1 192zM96 256C78.33 256 64 270.3 64 288C64 305.7 78.33 320 96 320C113.7 320 128 305.7 128 288C128 270.3 113.7 256 96 256zM416 320C433.7 320 448 305.7 448 288C448 270.3 433.7 256 416 256C398.3 256 384 270.3 384 288C384 305.7 398.3 320 416 320z" />
</svg>
),
},
symbols: {
outline: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h11v2H0zM4 11h3V6h4V4H0v2h4zM15.5 17c1.381 0 2.5-1.116 2.5-2.493s-1.119-2.493-2.5-2.493S13 13.13 13 14.507 14.119 17 15.5 17m0-2.986c.276 0 .5.222.5.493 0 .272-.224.493-.5.493s-.5-.221-.5-.493.224-.493.5-.493M21.5 19.014c-1.381 0-2.5 1.116-2.5 2.493S20.119 24 21.5 24s2.5-1.116 2.5-2.493-1.119-2.493-2.5-2.493m0 2.986a.497.497 0 0 1-.5-.493c0-.271.224-.493.5-.493s.5.222.5.493a.497.497 0 0 1-.5.493M22 13l-9 9 1.513 1.5 8.99-9.009zM17 11c2.209 0 4-1.119 4-2.5V2s.985-.161 1.498.949C23.01 4.055 23 6 23 6s1-1.119 1-3.135C24-.02 21 0 21 0h-2v6.347A5.853 5.853 0 0 0 17 6c-2.209 0-4 1.119-4 2.5s1.791 2.5 4 2.5M10.297 20.482l-1.475-1.585a47.54 47.54 0 0 1-1.442 1.129c-.307-.288-.989-1.016-2.045-2.183.902-.836 1.479-1.466 1.729-1.892s.376-.871.376-1.336c0-.592-.273-1.178-.818-1.759-.546-.581-1.329-.871-2.349-.871-1.008 0-1.79.293-2.344.879-.556.587-.832 1.181-.832 1.784 0 .813.419 1.748 1.256 2.805-.847.614-1.444 1.208-1.794 1.784a3.465 3.465 0 0 0-.523 1.833c0 .857.308 1.56.924 2.107.616.549 1.423.823 2.42.823 1.173 0 2.444-.379 3.813-1.137L8.235 24h2.819l-2.09-2.383 1.333-1.135zm-6.736-6.389a1.02 1.02 0 0 1 .73-.286c.31 0 .559.085.747.254a.849.849 0 0 1 .283.659c0 .518-.419 1.112-1.257 1.784-.536-.651-.805-1.231-.805-1.742a.901.901 0 0 1 .302-.669M3.74 22c-.427 0-.778-.116-1.057-.349-.279-.232-.418-.487-.418-.766 0-.594.509-1.288 1.527-2.083.968 1.134 1.717 1.946 2.248 2.438-.921.507-1.686.76-2.3.76" />
</svg>
),
solid: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M500.3 7.251C507.7 13.33 512 22.41 512 31.1V175.1C512 202.5 483.3 223.1 447.1 223.1C412.7 223.1 383.1 202.5 383.1 175.1C383.1 149.5 412.7 127.1 447.1 127.1V71.03L351.1 90.23V207.1C351.1 234.5 323.3 255.1 287.1 255.1C252.7 255.1 223.1 234.5 223.1 207.1C223.1 181.5 252.7 159.1 287.1 159.1V63.1C287.1 48.74 298.8 35.61 313.7 32.62L473.7 .6198C483.1-1.261 492.9 1.173 500.3 7.251H500.3zM74.66 303.1L86.5 286.2C92.43 277.3 102.4 271.1 113.1 271.1H174.9C185.6 271.1 195.6 277.3 201.5 286.2L213.3 303.1H239.1C266.5 303.1 287.1 325.5 287.1 351.1V463.1C287.1 490.5 266.5 511.1 239.1 511.1H47.1C21.49 511.1-.0019 490.5-.0019 463.1V351.1C-.0019 325.5 21.49 303.1 47.1 303.1H74.66zM143.1 359.1C117.5 359.1 95.1 381.5 95.1 407.1C95.1 434.5 117.5 455.1 143.1 455.1C170.5 455.1 191.1 434.5 191.1 407.1C191.1 381.5 170.5 359.1 143.1 359.1zM440.3 367.1H496C502.7 367.1 508.6 372.1 510.1 378.4C513.3 384.6 511.6 391.7 506.5 396L378.5 508C372.9 512.1 364.6 513.3 358.6 508.9C352.6 504.6 350.3 496.6 353.3 489.7L391.7 399.1H336C329.3 399.1 323.4 395.9 321 389.6C318.7 383.4 320.4 376.3 325.5 371.1L453.5 259.1C459.1 255 467.4 254.7 473.4 259.1C479.4 263.4 481.6 271.4 478.7 278.3L440.3 367.1zM116.7 219.1L19.85 119.2C-8.112 90.26-6.614 42.31 24.85 15.34C51.82-8.137 93.26-3.642 118.2 21.83L128.2 32.32L137.7 21.83C162.7-3.642 203.6-8.137 231.6 15.34C262.6 42.31 264.1 90.26 236.1 119.2L139.7 219.1C133.2 225.6 122.7 225.6 116.7 219.1H116.7z" />
</svg>
),
},
};
export const emojiSearchIcons = {
loupe: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z" />
</svg>
),
delete: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z" />
</svg>
),
};
@@ -0,0 +1,197 @@
import React, { memo, useCallback } from 'react';
import {
Emoji,
EmojiSettings,
GridRow,
UseEmojiPickerType,
} from '@udecode/plate-emoji';
import { cn } from '@/utils';
export type EmojiPickerContentProps = Pick<
UseEmojiPickerType,
| 'i18n'
| 'onMouseOver'
| 'onSelectEmoji'
| 'emojiLibrary'
| 'isSearching'
| 'searchResult'
| 'visibleCategories'
| 'refs'
| 'settings'
>;
export type EmojiButtonProps = {
index: number;
emoji: Emoji;
onSelect: (emoji: Emoji) => void;
onMouseOver: (emoji?: Emoji) => void;
};
export type RowOfButtonsProps = Pick<
UseEmojiPickerType,
'onMouseOver' | 'onSelectEmoji' | 'emojiLibrary'
> & {
row: GridRow;
};
const Button = memo(
({ index, emoji, onSelect, onMouseOver }: EmojiButtonProps) => {
return (
<button
type="button"
aria-label={emoji.skins[0].native}
tabIndex={-1}
data-index={index}
onClick={() => onSelect(emoji)}
onMouseEnter={() => onMouseOver(emoji)}
onMouseLeave={() => onMouseOver()}
className="group relative flex h-[36px] w-[36px] cursor-pointer items-center justify-center border-none bg-transparent text-2xl leading-none"
>
<div
aria-hidden="true"
className="absolute inset-0 rounded-full bg-[rgba(0,0,0,0.05)] opacity-0 group-hover:opacity-100"
/>
<span data-emoji-set="native" style={{ position: 'relative' }}>
{emoji.skins[0].native}
</span>
</button>
);
}
);
Button.displayName = 'Button';
const RowOfButtons = memo(
({ row, emojiLibrary, onSelectEmoji, onMouseOver }: RowOfButtonsProps) => (
<div key={row.id} data-index={row.id} className="flex">
{row.elements.map((emojiId, index) => (
<Button
key={emojiId}
index={index}
emoji={emojiLibrary.getEmoji(emojiId)}
onSelect={onSelectEmoji}
onMouseOver={onMouseOver}
/>
))}
</div>
)
);
RowOfButtons.displayName = 'RowOfButtons';
export function EmojiPickerContent({
i18n,
onSelectEmoji,
onMouseOver,
emojiLibrary,
isSearching = false,
searchResult,
visibleCategories,
refs,
settings = EmojiSettings,
}: EmojiPickerContentProps) {
const getRowWidth = settings.perLine.value * settings.buttonSize.value;
const isCategoryVisible = useCallback(
(categoryId: any) => {
return visibleCategories.has(categoryId)
? visibleCategories.get(categoryId)
: false;
},
[visibleCategories]
);
const EmojiList = useCallback(() => {
return emojiLibrary
.getGrid()
.sections()
.map(({ id: categoryId }) => {
const section = emojiLibrary.getGrid().section(categoryId);
const { buttonSize } = settings;
return (
<div
key={categoryId}
data-id={categoryId}
ref={section.root}
style={{ width: getRowWidth }}
>
<div className="sticky -top-px z-[1] bg-background/90 p-1 backdrop-blur-[4px] dark:text-gray-200">
{i18n.categories[categoryId]}
</div>
<div
className="relative flex flex-wrap"
style={{ height: section.getRows().length * buttonSize.value }}
>
{isCategoryVisible(categoryId) &&
section
.getRows()
.map((row: GridRow, index) => (
<RowOfButtons
key={index}
emojiLibrary={emojiLibrary}
row={row}
onSelectEmoji={onSelectEmoji}
onMouseOver={onMouseOver}
/>
))}
</div>
</div>
);
});
}, [
emojiLibrary,
getRowWidth,
i18n.categories,
isCategoryVisible,
onSelectEmoji,
onMouseOver,
settings,
]);
const SearchList = useCallback(() => {
return (
<div data-id="search" style={{ width: getRowWidth }}>
<div className="sticky -top-px z-[1] bg-background/90 p-1 backdrop-blur-[4px] dark:text-gray-200">
{i18n.searchResult}
</div>
<div className="relative flex flex-wrap">
{searchResult.map((emoji: Emoji, index: number) => (
<Button
key={emoji.id}
index={index}
emoji={emojiLibrary.getEmoji(emoji.id)}
onSelect={onSelectEmoji}
onMouseOver={onMouseOver}
/>
))}
</div>
</div>
);
}, [
emojiLibrary,
getRowWidth,
i18n.searchResult,
searchResult,
onSelectEmoji,
onMouseOver,
]);
return (
<div
className={cn(
'h-full min-h-[50%] overflow-y-auto overflow-x-hidden px-3',
'[&::-webkit-scrollbar]:w-4',
'[&::-webkit-scrollbar-button]:hidden [&::-webkit-scrollbar-button]:h-0 [&::-webkit-scrollbar-button]:w-0',
':hover:[&::-webkit-scrollbar-thumb]:bg-[#f3f4f6]',
'[&::-webkit-scrollbar-thumb]:min-h-[65px] [&::-webkit-scrollbar-thumb]:rounded-2xl [&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-white',
'[&::-webkit-scrollbar-track]:border-0'
)}
data-id="scroll"
ref={refs.current.contentRoot}
>
<div ref={refs.current.content} className="h-full">
{isSearching ? SearchList() : EmojiList()}
</div>
</div>
);
}
@@ -0,0 +1,80 @@
import React from 'react';
import {
EmojiCategoryList,
IEmojiFloatingLibrary,
UseEmojiPickerType,
} from '@udecode/plate-emoji';
import { cn } from '@/utils';
export type EmojiPickerNavigationProps = Pick<
UseEmojiPickerType,
'i18n' | 'emojiLibrary' | 'icons' | 'focusedCategory'
> & {
onClick: (id: EmojiCategoryList) => void;
};
const getBarProperty = (
emojiLibrary: IEmojiFloatingLibrary,
focusedCategory?: EmojiCategoryList
) => {
let width = 0;
let position = 0;
if (focusedCategory) {
width = 100 / emojiLibrary.getGrid().size;
position = focusedCategory
? emojiLibrary.indexOf(focusedCategory) * 100
: 0;
}
return { width, position };
};
export function EmojiPickerNavigation({
i18n,
icons,
emojiLibrary,
focusedCategory,
onClick,
}: EmojiPickerNavigationProps) {
const { width, position } = getBarProperty(emojiLibrary, focusedCategory);
return (
<nav
id="emoji-nav"
className="mb-2.5 border-0 border-b border-solid border-b-gray-100 dark:border-b-gray-900 p-3"
>
<div className="relative flex">
{emojiLibrary
.getGrid()
.sections()
.map(({ id }) => (
<button
key={id}
aria-label={i18n.categories[id]}
title={i18n.categories[id]}
type="button"
className={cn(
'flex grow cursor-pointer items-center justify-center border-none bg-transparent fill-current text-sm text-gray-500 hover:text-gray-800',
id === focusedCategory &&
'pointer-events-none fill-current text-blue-600'
)}
onClick={() => onClick(id)}
>
<span style={{ width: '20px', height: '20px' }}>
{icons.categories[id].outline}
</span>
</button>
))}
<div
className="absolute -bottom-3 left-0 h-[3px] w-full rounded-t bg-blue-600 opacity-100 transition-transform duration-200"
style={{
visibility: `${focusedCategory ? 'visible' : 'hidden'}`,
width: `${width}%`,
transform: `translateX(${position}%)`,
}}
/>
</div>
</nav>
);
}
@@ -0,0 +1,75 @@
import React from 'react';
import { UseEmojiPickerType } from '@udecode/plate-emoji';
export type EmojiPickerPreviewProps = Pick<
UseEmojiPickerType,
'emoji' | 'hasFound' | 'isSearching' | 'i18n'
>;
export type EmojiPreviewProps = Pick<UseEmojiPickerType, 'emoji'>;
export type NoEmojiPreviewProps = Pick<UseEmojiPickerType, 'i18n'>;
export type PickAnEmojiPreviewProps = NoEmojiPreviewProps;
function EmojiPreview({ emoji }: EmojiPreviewProps) {
return (
<div className="flex items-center border-t border-gray-100 dark:border-gray-900 p-2">
<div className="flex items-center justify-center text-3xl">
{emoji?.skins[0].native}
</div>
<div className="overflow-hidden pl-2">
<div className="truncate text-sm text-gray-600 dark:text-gray-400">{emoji?.name}</div>
<div className="truncate text-xs text-muted-foreground">{`:${emoji?.id}:`}</div>
</div>
</div>
);
}
function NoEmoji({ i18n }: NoEmojiPreviewProps) {
return (
<div className="flex items-center border-t border-gray-100 dark:border-gray-900 p-2">
<div className="flex items-center justify-center text-3xl">😢</div>
<div className="overflow-hidden pl-2">
<div className="truncate text-sm text-gray-600">
{i18n.searchNoResultsTitle}
</div>
<div className="truncate text-xs text-muted-foreground">
{i18n.searchNoResultsSubtitle}
</div>
</div>
</div>
);
}
function PickAnEmoji({ i18n }: PickAnEmojiPreviewProps) {
return (
<div className="flex items-center border-t border-gray-100 dark:border-gray-900 p-2">
<div className="flex items-center justify-center text-3xl"></div>
<div className="overflow-hidden pl-2">
<div className="truncate text-lg text-gray-800 dark:text-gray-200">
{i18n.pick}
</div>
</div>
</div>
);
}
export function EmojiPickerPreview({
emoji,
hasFound = true,
isSearching = false,
i18n,
...props
}: EmojiPickerPreviewProps) {
const showPickEmoji = !emoji && !(isSearching && !hasFound);
const showNoEmoji = isSearching && !hasFound;
const showPreview = emoji;
return (
<>
{showPreview && <EmojiPreview emoji={emoji} {...props} />}
{showPickEmoji && <PickAnEmoji i18n={i18n} {...props} />}
{showNoEmoji && <NoEmoji i18n={i18n} {...props} />}
</>
);
}
@@ -0,0 +1,41 @@
import React from 'react';
import { UseEmojiPickerType } from '@udecode/plate-emoji';
import IconSearch from '@/assets/icons/search.svg'
import IconClose from '@/assets/icons/close.circle.svg'
import { cn } from '@/utils';
export type EmojiPickerSearchAndClearProps = Pick<
UseEmojiPickerType,
'i18n' | 'searchValue' | 'clearSearch'
>;
export function EmojiPickerSearchAndClear({
i18n,
searchValue,
clearSearch,
}: EmojiPickerSearchAndClearProps) {
return (
<>
<span
className={cn(
'absolute left-2 top-1/2 z-10 flex h-5 w-5 -translate-y-1/2'
)}
>
<IconSearch className="w-full h-full" />
</span>
{searchValue && (
<button
title={i18n.clear}
aria-label="Clear"
type="button"
className={cn(
'absolute right-0 top-1/2 flex h-8 w-8 -translate-y-1/2 cursor-pointer border-none bg-transparent justify-center items-center'
)}
onClick={clearSearch}
>
<IconClose className="w-5 h-5" />
</button>
)}
</>
);
}
@@ -0,0 +1,30 @@
import React, { ReactNode } from 'react';
import { UseEmojiPickerType } from '@udecode/plate-emoji';
export type EmojiPickerSearchBarProps = {
children: ReactNode;
} & Pick<UseEmojiPickerType, 'i18n' | 'searchValue' | 'setSearch'>;
export function EmojiPickerSearchBar({
i18n,
searchValue,
setSearch,
children,
}: EmojiPickerSearchBarProps) {
return (
<div className="flex items-center px-2">
<div className="relative flex grow">
<input
type="text"
placeholder={i18n.search}
autoComplete="off"
aria-label="Search"
className="block w-full appearance-none rounded-lg border-0 bg-gray-100 dark:bg-gray-900 dark:text-gray-200 px-8 py-2 outline-none"
onChange={(event) => setSearch(event.target.value)}
value={searchValue}
/>
{children}
</div>
</div>
);
}
@@ -0,0 +1,75 @@
import React from 'react';
import { EmojiSettings, UseEmojiPickerType } from '@udecode/plate-emoji';
import { cn } from '@/utils';
import { EmojiPickerContent } from './emoji-picker-content';
import { EmojiPickerNavigation } from './emoji-picker-navigation';
import { EmojiPickerPreview } from './emoji-picker-preview';
import { EmojiPickerSearchAndClear } from './emoji-picker-search-and-clear';
import { EmojiPickerSearchBar } from './emoji-picker-search-bar';
export function EmojiPicker({
i18n,
searchValue,
setSearch,
clearSearch,
isSearching,
hasFound,
searchResult,
emoji,
onSelectEmoji,
onMouseOver,
emojiLibrary,
icons,
handleCategoryClick,
focusedCategory,
visibleCategories,
refs,
settings = EmojiSettings,
}: UseEmojiPickerType) {
return (
<div
className={cn(
'flex flex-col rounded dark:text-gray-300',
'h-[400px] w-[316px] shadow-[rgb(15_15_15_/_5%)_0_0_0_1px,_rgb(15_15_15_/_10%)_0_3px_6px,_rgb(15_15_15_/_20%)_0_9px_24px]'
)}
>
<EmojiPickerNavigation
i18n={i18n}
emojiLibrary={emojiLibrary}
icons={icons}
focusedCategory={focusedCategory}
onClick={handleCategoryClick}
/>
<EmojiPickerSearchBar
i18n={i18n}
setSearch={setSearch}
searchValue={searchValue}
>
<EmojiPickerSearchAndClear
i18n={i18n}
clearSearch={clearSearch}
searchValue={searchValue}
/>
</EmojiPickerSearchBar>
<EmojiPickerContent
i18n={i18n}
emojiLibrary={emojiLibrary}
isSearching={isSearching}
searchResult={searchResult}
visibleCategories={visibleCategories}
settings={settings}
onSelectEmoji={onSelectEmoji}
onMouseOver={onMouseOver}
refs={refs}
/>
<EmojiPickerPreview
i18n={i18n}
emoji={emoji}
hasFound={hasFound}
isSearching={isSearching}
/>
</div>
);
}
@@ -0,0 +1,33 @@
import React, { ReactNode } from 'react';
import * as Popover from '@radix-ui/react-popover';
type EmojiPopupProps = {
control: ReactNode;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
children: ReactNode;
};
export function EmojiPopup({
control,
isOpen,
setIsOpen,
children,
}: EmojiPopupProps) {
return (
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
<Popover.Trigger asChild>{control}</Popover.Trigger>
<Popover.Portal>
<Popover.Content
align="start"
alignOffset={-12}
sideOffset={20}
className="z-[100] bg-gray-200 dark:bg-gray-800"
>
{children}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
@@ -0,0 +1,43 @@
import React from 'react';
import { ComboboxProps, Data, NoData } from '@udecode/plate-combobox';
import { getPluginOptions, useEditorRef } from '@udecode/plate-common';
import {
ELEMENT_MENTION,
getMentionOnSelectItem,
MentionPlugin,
} from '@udecode/plate-mention';
import { Combobox } from './combobox';
import User from '@/components/User';
export interface MentionComboboxProps<TData extends Data = NoData>
extends Partial<ComboboxProps<TData>> {
pluginKey?: string;
}
export function MentionCombobox<TData extends Data = NoData>({
pluginKey = ELEMENT_MENTION,
id = pluginKey,
...props
}: MentionComboboxProps<TData>) {
const editor = useEditorRef();
const { trigger } = getPluginOptions<MentionPlugin>(editor, pluginKey);
return (
<Combobox
onRenderItem={({item})=>{
// @ts-ignore
const {data:{uid}}=item;
return <User key={uid} uid={uid} interactive={false} />
}}
id={id}
trigger={trigger!}
controlled
onSelectItem={getMentionOnSelectItem({
key: pluginKey,
})}
{...props}
/>
);
}
@@ -0,0 +1,57 @@
import React, { forwardRef } from 'react';
import {
getHandler,
PlateElement,
PlateElementProps,
Value,
} from '@udecode/plate-common';
import { TMentionElement } from '@udecode/plate-mention';
import { useFocused, useSelected } from 'slate-react';
import { cn } from '@/utils';
export interface MentionElementProps
extends PlateElementProps<Value, TMentionElement> {
/**
* Prefix rendered before mention
*/
prefix?: string;
onClick?: (mentionNode: any) => void;
renderLabel?: (mentionable: TMentionElement) => string;
}
const MentionElement = forwardRef<
React.ElementRef<typeof PlateElement>,
MentionElementProps
>(({ prefix, renderLabel, className, onClick, ...props }, ref) => {
const { children, element } = props;
const selected = useSelected();
const focused = useFocused();
return (
<PlateElement
ref={ref}
className={cn(
'inline-block cursor-pointer rounded-md px-1.5 py-0.5 align-baseline text-sm font-medium bg-gray-100',
selected && focused && 'ring-2 ring-ring',
element.children[0].bold === true && 'font-bold',
element.children[0].italic === true && 'italic',
element.children[0].underline === true && 'underline',
className
)}
data-slate-value={element.value}
contentEditable={false}
onClick={getHandler(onClick, element)}
{...props}
>
{prefix}
{renderLabel ? renderLabel(element) : element.value}
{children}
</PlateElement>
);
});
MentionElement.displayName = 'MentionElement';
export { MentionElement };
@@ -0,0 +1,46 @@
import React from 'react';
import {
getHandler,
PlateElement,
PlateElementProps,
Value,
} from '@udecode/plate-common';
import { TMentionElement } from '@udecode/plate-mention';
import { useFocused, useSelected } from 'slate-react';
import { cn } from '@/utils';
export interface MentionInputElementProps
extends PlateElementProps<Value, TMentionElement> {
onClick?: (mentionNode: any) => void;
}
const MentionInputElement = React.forwardRef<
React.ElementRef<typeof PlateElement>,
MentionInputElementProps
>(({ className, onClick, ...props }, ref) => {
const { children, element } = props;
const selected = useSelected();
const focused = useFocused();
return (
<PlateElement
asChild
ref={ref}
data-slate-value={element.value}
className={cn(
'inline-block rounded-md bg-muted px-1.5 py-0.5 align-baseline text-sm',
selected && focused && 'ring-2 ring-ring',
className
)}
onClick={getHandler(onClick, element)}
{...props}
>
<span>{children}</span>
</PlateElement>
);
});
MentionInputElement.displayName = 'MentionInputElement';
export { MentionInputElement };
@@ -0,0 +1,22 @@
import React from 'react';
import { PlateElement, PlateElementProps } from '@udecode/plate-common';
import { cn } from '@/utils';
const ParagraphElement = React.forwardRef<
React.ElementRef<typeof PlateElement>,
PlateElementProps
>(({ className, children, ...props }: PlateElementProps, ref) => {
return (
<PlateElement
ref={ref}
className={cn('m-0 px-0 py-1', className)}
{...props}
>
{children}
</PlateElement>
);
});
ParagraphElement.displayName = 'ParagraphElement';
export { ParagraphElement };
+75
View File
@@ -0,0 +1,75 @@
import {
createExitBreakPlugin,
createSoftBreakPlugin,
} from '@udecode/plate-break';
import { createComboboxPlugin } from '@udecode/plate-combobox';
import { createPlugins, RenderAfterEditable } from '@udecode/plate-common';
import { createEmojiPlugin } from '@udecode/plate-emoji';
import {
createMentionPlugin,
ELEMENT_MENTION,
ELEMENT_MENTION_INPUT,
} from '@udecode/plate-mention';
import { createNodeIdPlugin } from '@udecode/plate-node-id';
import {
createParagraphPlugin,
ELEMENT_PARAGRAPH,
} from '@udecode/plate-paragraph';
import { createTrailingBlockPlugin } from '@udecode/plate-trailing-block';
import { EmojiCombobox } from './plate-ui/emoji-combobox';
import { MentionElement } from './plate-ui/mention-element';
import { MentionInputElement } from './plate-ui/mention-input-element';
import { ParagraphElement } from './plate-ui/paragraph-element';
export const plugins = createPlugins(
[
// Nodes
createParagraphPlugin(),
createMentionPlugin({
options: {
createMentionNode: (item) => {
const { key, text } = item;
return { value: text, uid: key };
},
},
}),
// Functionality
createComboboxPlugin(),
createEmojiPlugin({
renderAfterEditable: EmojiCombobox as RenderAfterEditable,
}),
createExitBreakPlugin({
options: {
rules: [
{
hotkey: 'mod+enter',
},
{
hotkey: 'mod+shift+enter',
before: true,
},
],
},
}),
createNodeIdPlugin(),
createSoftBreakPlugin({
options: {
rules: [{ hotkey: 'shift+enter' }],
},
}),
createTrailingBlockPlugin({
options: { type: ELEMENT_PARAGRAPH },
}),
],
{
components: {
[ELEMENT_MENTION]: MentionElement,
[ELEMENT_MENTION_INPUT]: MentionInputElement,
[ELEMENT_PARAGRAPH]: ParagraphElement,
},
}
);
-73
View File
@@ -1,73 +0,0 @@
import { useEffect, useRef, useState } from "react";
import clsx from "clsx";
import { Picker } from "emoji-mart";
import { useOutsideClick } from "rooks";
import SmileIcon from "@/assets/icons/emoji.smile.svg";
import Tooltip from "../Tooltip";
export default function EmojiPicker({ selectEmoji }: { selectEmoji: (e: string) => void }) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const togglePickerVisible = () => {
setVisible((prev) => !prev);
};
useEffect(() => {
if (ref && ref.current) {
ref.current.innerHTML = "";
new Picker({
data: async () => {
const response = await fetch("https://cdn.jsdelivr.net/npm/@emoji-mart/data");
return response.json();
},
parent: ref.current,
onEmojiSelect: (item) => {
console.log("eee333", item);
selectEmoji(item.native);
}
// onClickOutside: () => {
// setVisible(false);
// }
});
}
}, [selectEmoji]);
useOutsideClick(
ref,
(evt) => {
const clickEle = evt.target as HTMLElement | null;
if (!clickEle) return;
const ignore =
(clickEle.nodeName == "svg" && clickEle.dataset.emoji == "toggler") ||
(clickEle.nodeName == "path" &&
clickEle.parentElement &&
clickEle.parentElement.dataset.emoji == "toggler");
if (ignore) return;
setVisible(false);
},
visible
);
return (
<Tooltip placement="top" tip="Emojis" disabled={visible}>
<div className="hidden md:flex relative w-fit items-center">
<div
ref={ref}
className={clsx(
`z-50 absolute -top-5 -left-5 -translate-y-full`,
visible ? "block" : "hidden"
)}
>
{/* emoji picker */}
</div>
<SmileIcon
data-emoji="toggler"
className="cursor-pointer select-none !w-[22px] !h-[22px]"
onClick={togglePickerVisible}
/>
</div>
</Tooltip>
);
}
+1 -1
View File
@@ -100,7 +100,7 @@ const Toolbar: FC<Props> = ({
<Tooltip placement="top" tip="Send">
<SendIcon
className={"w-6 h-6 cursor-pointer animate-zoomIn dark:fill-gray-300"}
onClick={sendMessages.bind(null, undefined)}
onClick={sendMessages.bind(null)}
/>
</Tooltip>
)}
+35 -28
View File
@@ -1,4 +1,4 @@
import { FC, useEffect, useState } from "react";
import { FC, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
@@ -12,15 +12,17 @@ import useDraft from "@/hooks/useDraft";
import useSendMessage from "@/hooks/useSendMessage";
import useUploadFile from "@/hooks/useUploadFile";
import useUserOperation from "@/hooks/useUserOperation";
// import MixedInput, { useMixedEditor } from "../MixedInput";
import StyledButton from "../styled/Button";
import TextInput from "../TextInput";
import EmojiPicker from "./EmojiPicker";
import Replying from "./Replying";
import Toolbar from "./Toolbar";
// import StyledSend from "./styled";
import UploadFileList from "./UploadFileList";
import { shallowEqual } from "react-redux";
import MessageInput from "../MessageInput";
import { Emoji } from "@udecode/plate-emoji";
import { EmojiDropdownInput } from "../MessageInput/plate-ui/emoji-dropdown-input";
import { MessageWithMentions } from "@/types/message";
import { PlateEditor } from "@udecode/plate-common";
const Modes = {
text: "text",
@@ -35,6 +37,8 @@ const Send: FC<IProps> = ({
context = "channel",
id
}) => {
const editorRef = useRef<PlateEditor | null>(null);
const { t } = useTranslation("chat");
const { unblockThisContact, blocked } = useUserOperation({
uid: context == "dm" ? id : undefined,
@@ -42,8 +46,10 @@ const Send: FC<IProps> = ({
});
const { resetStageFiles } = useUploadFile({ context, id });
const { getDraft, getUpdateDraft } = useDraft({ context, id });
// const editor = useMixedEditor(`${context}_${id}`);
const [msgs, setMsgs] = useState([]);
const [msg, setMsg] = useState<MessageWithMentions>({
text:"",
mentions:[]
})
const [markdownEditor, setMarkdownEditor] = useState(null);
const [markdownFullscreen, setMarkdownFullscreen] = useState(false);
const dispatch = useAppDispatch();
@@ -64,31 +70,32 @@ const Send: FC<IProps> = ({
const usersData = useAppSelector((store) => store.users.byId, shallowEqual);
const { sendMessage } = useSendMessage({ context, from: from_uid, to: id });
const insertEmoji = (emoji: string) => {
const insertEmoji = (emoji: Emoji) => {
console.log({emoji});
if (mode == Modes.markdown && markdownEditor) {
// markdown insert emoji
markdownEditor.insertText(emoji);
const {native}=emoji.skins[0]
markdownEditor.insertText(native);
}
};
const handleSendMessage = async (messages?: any[]) => {
if (!id) return;
const sendingMsgs = messages ?? msgs;
if (sendingMsgs && sendingMsgs.length) {
const handleSendMessage = async () => {
if (!id || !msg.text.trim()) return;
// send text msgs
for await (const msg of sendingMsgs) {
const { type: content_type, content, properties = {} } = msg;
if ((content as string).trim() === "") continue; // 空消息不发送
properties.local_id = properties.local_id ?? +new Date();
await sendMessage({
id,
reply_mid: replying_mid,
type: content_type,
content,
from_uid,
properties
});
if(editorRef.current) {
editorRef.current.reset()
}
}
const {text,mentions}=msg;
const properties={mentions};
properties.local_id = +new Date();
await sendMessage({
id,
reply_mid: replying_mid,
type: "text",
content:text,
from_uid,
properties
});
// send files
if (uploadFiles.length !== 0) {
uploadFiles.forEach((fileInfo) => {
@@ -166,11 +173,11 @@ const Send: FC<IProps> = ({
isMarkdownMode ? `grid grid-cols-[1fr_1fr] grid-rows-[auto_auto] gap-0` : "gap-4"
)}
>
{mode == Modes.markdown && <EmojiPicker selectEmoji={insertEmoji} />}
{mode == Modes.text && null}
{mode == Modes.markdown && <EmojiDropdownInput options={{closeOnSelect:false}} onSelectEmoji={insertEmoji} />}
{mode == Modes.text && <MessageInput editorRef={editorRef} members={members} id={`${context}_${id}`} updateMessage={setMsg} sendMessage={handleSendMessage} placeholder={placeholder} />}
<Toolbar
sendMessages={handleSendMessage}
sendVisible={msgs.length > 0 || uploadFiles.length > 0}
sendVisible={msg.text.trim().length > 0 || uploadFiles.length > 0}
context={context}
to={id}
mode={mode}
+1 -1
View File
@@ -66,7 +66,7 @@ declare module "*.module.css" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "@emoji-mart/react";
// declare module "@emoji-mart/react";
interface Window {
ethereum: any;
+9 -1
View File
@@ -1,5 +1,5 @@
import { ChatEvent } from "./sse";
import { TComboboxItem } from '@udecode/plate-combobox';
export type ContentType =
| "text/plain"
| "text/markdown"
@@ -30,3 +30,11 @@ export interface UploadFileResponse {
};
}
export interface ChatMessage extends Omit<ChatEvent, "type"> {}
export type MentionData = TComboboxItem & { data: any };
export type MessageWithMentions = {
text: string;
mentions: number[];
};
+47 -4
View File
@@ -18,7 +18,50 @@ import IconPdf from "@/assets/icons/file.pdf.svg";
import IconUnknown from "@/assets/icons/file.unknown.svg";
import IconVideo from "@/assets/icons/file.video.svg";
import { StoredUser } from "./app/slices/users";
import { clsx } from 'clsx';
import type { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { MessageWithMentions } from "./types/message";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type MentionInput = {
type: 'mention';
value: string;
uid: string;
id: string;
};
export type ParagraphInput = {
id: string;
type: 'p';
children: ({ text: string } | MentionInput)[];
};
export function getMessageFromPlateValues(
values: ParagraphInput[]
): MessageWithMentions {
const mentions: number[] = [];
const text = values
.map((node) => {
return node.children
.map((child) => {
if ('text' in child) {
return child.text;
} else {
mentions.push(+child.uid);
return ` @${child.uid} `;
}
})
.join('');
})
.join('\n');
return {
text,
mentions,
};
}
export const isMobile = () =>
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
@@ -215,7 +258,7 @@ export const normalizeFileMessage = (data: MessagePayload) => {
const { properties, content, sending = false, content_type } = data;
const isFile = content_type == ContentTypes.file;
const isPic = isImage(properties?.content_type, properties?.size);
// gif暂不支持缩略图
// gif 暂不支持缩略图
const isGif = isPic && properties?.content_type == "image/gif";
let res: null | { file_path?: string; content?: string; download?: string; thumbnail: string } =
null;
@@ -260,7 +303,7 @@ export const normalizeArchiveData = (
avatar?: number | string;
}
) => {
// uid存在,则favorite,否则archive
// uid 存在,则 favorite,否则 archive
const prefix = uid
? `${BASE_URL}/favorite/attachment/${uid}/${filePath}/`
: `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=`;
@@ -427,7 +470,7 @@ export const playAgoraVideo = (uid: number, videoTrack?: ICameraVideoTrack | nul
// 转换一下邀请连接
export const transformInviteLink = (link: string) => {
// 确保http开头
// 确保 http 开头
const _link = link.startsWith("http") ? link : `http://${link}`;
// return _link;
// 替换掉域名
@@ -439,7 +482,7 @@ export const transformInviteLink = (link: string) => {
export const reloadCurrentPage = () => {
if (isElectronContext()) {
// // 改变theme color 然后electron reload(约定)
// // 改变 theme color 然后 electron reload(约定)
// const metaThemeColor = document.querySelector("meta[name=theme-color]");
// if (metaThemeColor) {
// metaThemeColor.setAttribute("content", "#123456");
+1108 -1320
View File
File diff suppressed because it is too large Load Diff