import './emoji.css';
import uuid from 'shortid';
import Prism from 'prismjs';
import urlRegex from 'url-regex';
import InstantReplace from 'slate-instant-replace';
import { Emoji as EMEmoji, emojiIndex } from 'emoji-mart';
import { Slate, Editable, withReact, ReactEditor } from 'slate-react';
import { Text, Editor, Transforms, Range, createEditor } from 'slate';
import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { Paper, Chip, Stack, Box, useTheme, ToggleButton, ToggleButtonGroup, Avatar, Icon, Popover, MenuItem, ListItemIcon, ListItemText, IconButton } from '../';
// prism
;Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold); // prettier-ignore
// import message
import DashupUIChatEmbed from './Embed';
// let context
let Embed = null;
let trigger = null;
let DashupUIContext = null;
// create withmentions hook
const withMentions = (editor) => {
// is inline
const { isInline, isVoid } = editor;
// add is inline
editor.isInline = element => {
return ['mention', 'emoji'].includes(element.type) ? true : isInline(element);
};
// add is void
editor.isVoid = element => {
return ['mention', 'emoji'].includes(element.type) ? true : isVoid(element);
};
// return editor
return editor;
};
// leaf
const Leaf = ({ attributes, children, leaf }) => {
return (
{ children }
);
};
// create emoji
const Emoji = ({ attributes, children, element }) => {
// theme
const theme = useTheme();
// return chip
return (
{ children }
);
};
// create mention
const Mention = ({ attributes, children, element }) => {
// theme
const theme = useTheme();
// dashup
const dashup = (typeof eden === 'undefined' ? null : eden).dashup;
// page
const page = element.trigger === '#' ? dashup?.page(element.mention?.id) : null;
// let color
let color = page?.get('color');
let parent = (page && (page?.get('parent') || 'root') !== 'root') ? dashup?.page(page?.get('parent')) : null;
// check page
if (page) {
// get parentmost color
while (parent && !color) {
// get new parent
const newParent = ((parent.get('parent') || 'root') !== 'root') ? dashup?.page(parent.get('parent')) : null;
color = newParent?.get('color');
// set parent
parent = newParent;
}
}
// set color
const actualColor = element.mention?.color || color;
// return jsx
return (
) : (
!!element.mention.icon && (
)
)
) }
/>
);
};
// create element
const Element = (props = {}) => {
// return type
return (
props.element.type === 'mention' ? :
props.element.type === 'emoji' ? :
{ props.children }
);
};
// create embeds
const allEmbeds = {};
// chat input
const DashupUIChatInput = (props = {}) => {
// theme
const theme = useTheme();
// editor
const editor = useMemo(() => withMentions(withReact(createEditor())), []);
// Transformation function
const AddEmojis = (editor, lastWord) => {
// add emojis
editor.moveFocusBackward(lastWord.length); // select last word
editor.insertText({
type : 'emoji',
emoji : lastWord.split(':').join(''),
children : [{
text : ''
}],
}); // replace it
};
// plugins
const plugins = [InstantReplace(AddEmojis)];
// empty state
const emptyState = [{
type : 'paragraph',
children : [{ text : '' }]
}];
// all embeds
let [embedId] = useState(uuid());
// set all embeds
if (!allEmbeds[embedId]) allEmbeds[embedId] = [];
// set embeds
const [long, setLong] = useState(false);
const [emoji, setEmoji] = useState(null);
const [index, setIndex] = useState(0);
const [value, setValue] = useState(emptyState);
const [items, setItems] = useState([]);
const [embeds, setEmbeds] = useState(allEmbeds[embedId]);
const [mention, setMention] = useState(null);
const [posting, setPosting] = useState(false);
const [emojiRef, setEmojiRef] = useState(null);
const [mentionRef, setMentionRef] = useState(null);
const [emojiSearch, setEmojiSearch] = useState('');
// picker ref
const pickerRef = useRef(null);
// render methods
const renderLeaf = useCallback(props => , []);
const renderElement = useCallback(props => , []);
// decorate
const decorate = useCallback(([node, path]) => {
// ranges
const ranges = []
// is text
if (!Text.isText(node)) {
return ranges;
}
// get length
const getLength = (token) => {
if (typeof token === 'string') {
return token.length;
} else if (typeof token.content === 'string') {
return token.content.length;
} else {
return token.content.reduce((l, t) => l + getLength(t), 0);
}
};
// tokens
const tokens = Prism.tokenize(node?.text, Prism.languages.markdown);
// start
let start = 0;
// loop tokens
for (const token of tokens) {
const length = getLength(token);
const end = start + length;
if (typeof token !== 'string') {
ranges.push({
[token.type] : true,
focus : { path, offset : end },
anchor : { path, offset : start },
});
}
// end
start = end;
}
// return ranges
return ranges;
}, []);
// const toText
const toText = (children) => {
// map children
return children.map((child) => {
// empty child
if (!child) return;
// if text
if (child.text) return child.text;
if (child.emoji) return child.emoji;
if (child.mention) return `${child.trigger}[${child.mention.display}](${child.mention.id})`;
if (child.type === 'paragraph') return `${toText(child.children)}\n`;
// if children
if (child.children) return `${toText(child.children)}`;
}).filter((i) => i).join('');
};
// on send
const onSend = async (e, data) => {
// prevent default
e.preventDefault();
e.stopPropagation();
// check value
if (JSON.stringify(value) === JSON.stringify(emptyState)) return;
// value to string
const text = toText(value || []);
// validate
if (posting || long) return;
// set posting
setPosting(true);
// message
const newMessage = {
by : data.by,
temp : uuid(),
embeds : [],
subject : data.thread,
message : text,
created_at : new Date(),
updated_at : new Date(),
};
allEmbeds[embedId] = [];
setEmbeds([...allEmbeds[embedId]]);
// set embeds
Transforms.select(editor, Editor.start(editor, []));
// delete
Transforms.delete(editor, {
at : {
focus : Editor.end(editor, []),
anchor : Editor.start(editor, []),
},
});
// on send
if (props.onSend) {
// on send
await props.onSend(e, newMessage);
} else {
// emit message
data.onMessage(newMessage);
// call join
const message = await data.dashup.action({
type : 'page',
struct : 'channel',
}, 'send', newMessage);
// check if success
if (!message && typeof eden !== 'undefined') {
// alert error message
eden.alert.error('Failed to send');
}
}
// update
setPosting(false);
};
// load embed
const loadEmbed = async (d, url) => {
// call join
const data = await d.dashup.action({
type : 'page',
struct : 'channel',
}, 'embed', url);
// remove from embeds if fail
if (!data || !data.type) {
// disable embed
allEmbeds[embedId] = allEmbeds[embedId].filter((e) => e.url !== url);
// return update
return setEmbeds([...allEmbeds[embedId]]);
}
// get embed
const embed = allEmbeds[embedId].find((e) => e.url === url);
// check embed
if (!embed) return;
// set data
embed.data = data;
embed.loading = false;
// set embeds
setEmbeds([...allEmbeds[embedId]]);
};
// on key down
const onKeyDown = (e, data) => {
// check mention ref
if ((mention && mentionRef) || (emoji && emojiRef)) {
// actual items
let actualItems = items;
// emoji
if (emoji) {
actualItems = emojiIndex.search(emojiSearch);
}
// check mention ref
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
// prevent default
e.preventDefault();
// set index
setIndex(index === 0 ? 0 : index - 1);
} else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
// prevent default
e.preventDefault();
// set index
setIndex(index >= (actualItems.length - 1) ? index : index + 1);
} else if (['Tab', 'Enter'].includes(e.key)) {
// prevent default
e.preventDefault();
// set index
(mention ? onMention : onEmoji)(actualItems[index]);
} else if (e.key === 'Escape') {
setMention(null);
}
} else {
// check key code
if (e.keyCode !== 13 || e.shiftKey) return true;
// prevent default
e.preventDefault();
// do on comment
return onSend(e, data);
}
};
// on keyup
const onChange = (data, value) => {
// set value
setValue(value);
// check sending
if (posting) return;
// try/catch
try {
// get selection
const { selection } = editor;
// check selection
if (selection && Range.isCollapsed(selection)) {
// get out
const [start] = Range.edges(selection);
const wordBefore = Editor.before(editor, start, {
unit : 'word',
});
const before = wordBefore && Editor.before(editor, wordBefore);
const beforeRange = before && Editor.range(editor, before, start);
const beforeText = beforeRange && Editor.string(editor, beforeRange);
// check before text
if (beforeText) {
// emoji picker
if (beforeText.split('')[0] === ':') {
// set emoji picker
setMention(null);
setEmojiSearch(beforeText.split('').slice(1).join(''));
// set emoji
return setEmoji(beforeRange);
}
// find match in text
trigger = (typeof eden === 'undefined' ? [] : eden.chat?.triggers() || []).find((m) => {
// find trigger
return beforeText.split('')[0] === m.trigger;
});
// check found
if (trigger) {
// search
trigger.search(beforeText.split('').slice(1).join('')).then((items) => {
// set items
setItems([...items].reverse());
setIndex(items.length - 1);
});
// mention
setEmoji(null);
setEmojiSearch('');
return setMention(beforeRange);
}
}
}
// unset
setEmoji(null);
setMention(null);
setEmojiSearch('');
} catch (e) {}
// load embeds
const entries = (toText(value).match(urlRegex()) || []);
// loop entries
entries.forEach((entry) => {
// get embeds
if (!allEmbeds[embedId].find((e) => e.url === entry)) {
// set entry
allEmbeds[embedId].push({
url : entry,
data : {},
loading : loadEmbed(data, entry),
});
}
});
// remove old
allEmbeds[embedId] = allEmbeds[embedId].filter((e) => entries.includes(e.url));
// embeds
setEmbeds([...allEmbeds[embedId]]);
};
// insert mention
const onEmoji = (data) => {
// select target
if (emoji) Transforms.select(editor, emoji);
// mention
const insertEmoji = {
type : 'emoji',
emoji : data.colons,
children : [{
text : ''
}],
};
// set mention
setEmoji(null);
// insert nodes
Transforms.insertNodes(editor, insertEmoji);
Transforms.move(editor);
// set emoji ref
setEmojiRef(null);
};
// insert mention
const onMention = (data) => {
// select target
Transforms.select(editor, mention);
// mention
const insertMention = {
type : 'mention',
mention : data,
trigger : trigger?.trigger,
children : [{
text : ''
}],
};
// set mention
setMention(null);
// insert nodes
Transforms.insertNodes(editor, insertMention);
Transforms.move(editor);
};
// use effect
useEffect(() => {
// try/catch
try {
// check mention
if (!mention) {
// set index
setIndex(0);
setMentionRef(null);
// check index
return;
}
// dom range
const domRange = ReactEditor.toDOMRange(editor, mention);
// set ref
setMentionRef(domRange?.endContainer?.parentNode);
} catch (e) {}
}, [mention]);
// use effect
useEffect(() => {
// try/catch
try {
// check mention
if (!emoji) {
// set index
setIndex(0);
setEmojiRef(null);
// check index
return;
}
// dom range
const domRange = ReactEditor.toDOMRange(editor, emoji);
// set ref
setEmojiRef(domRange?.endContainer?.parentNode);
} catch (e) {}
}, [emoji]);
// use effect
useEffect(() => {
// done
return () => {
delete allEmbeds[embedId];
};
}, [embedId]);
// create body
const renderBody = (data) => {
// return jsx
return (
<>
{ !!embeds.length && (
{ embeds.map((embed, i) => {
// return jsx
return (
);
}) }
) }
onChange(data, v) }
>
onKeyDown(e, data) }
renderLeaf={ renderLeaf }
renderElement={ renderElement }
/>
onSend(e, data) }>
setMentionRef(null) }
anchorEl={ mentionRef }
transformOrigin={ {
vertical : 'bottom',
horizontal : 'center',
} }
anchorOrigin={ {
vertical : 'top',
horizontal : 'right',
} }
hideBackdrop
disableAutoFocus
disableEnforceFocus
>
{ items.map((item, i) => {
// return jsx
return (
);
}) }
setEmojiRef(null) }
anchorEl={ emojiRef }
transformOrigin={ {
vertical : 'bottom',
horizontal : 'center',
} }
anchorOrigin={ {
vertical : 'top',
horizontal : 'right',
} }
hideBackdrop
disableAutoFocus
disableEnforceFocus
>
{ !!(emojiSearch || '').length && emojiIndex.search(emojiSearch).map((item, i) => {
// return jsx
return (
onEmoji(item) } sx={ {
backgroundColor : index === i ? 'primary.main' : undefined
} }>
);
}) }
>
);
};
// return jsx
return props.noChat ? renderBody(props) : (
{ (data) => {
// return jsx
return renderBody(data);
} }
);
};
// export default page menu
export default (ctx) => {
// use context
Embed = DashupUIChatEmbed(ctx);
DashupUIContext = ctx;
// return actual component
return DashupUIChatInput;
};