import {formatTimestamp} from '@components/utils' import {textToLinkedText} from '@components/utils/Text' import {Tooltip} from '@material-ui/core' import {makeStyles} from '@material-ui/core/styles' import {CSSProperties} from '@material-ui/styles' import settings from '@models/api/Settings' import {compTextMessage, TextMessage, TextMessages} from '@models/ISharedContent' import {assert, findTextColorRGB} from '@models/utils' import {getRandomColorRGB, rgba} from '@models/utils' import _ from 'lodash' import {Observer, useObserver} from 'mobx-react-lite' import React, {useEffect, useRef} from 'react' import {ContentProps} from './Content' class TextMember{ messages: TextMessage[] = [] isStatic = false abortScroll = false editing = false } // Update (send) the content if needed function onUpdateTexts(messages: TextMessage[], div:HTMLDivElement, props: ContentProps) { const newTexts: TextMessages = {messages, scroll:[div.scrollLeft, div.scrollTop]} const newUrl = JSON.stringify(newTexts) if (props.content.url !== newUrl && props.updateAndSend) { props.content.url = newUrl props.updateAndSend(props.content) } } interface TextDivProps extends ContentProps{ text: TextMessage textEditing: boolean textToShow: JSX.Element[] member: TextMember div: HTMLDivElement | null autoFocus: boolean } interface TextEditProps extends TextDivProps{ css: React.CSSProperties } function sendText(text: string, props: TextEditProps){ // console.log(`send text ${text}`) props.text.message = text const messagesToSend = props.member.messages.filter(text => text.message.length) if (props.div){ onUpdateTexts(messagesToSend, props.div, props) } } export const TextEdit: React.FC<TextEditProps> = (props:TextEditProps) => { const [text, setText] = React.useState(props.text.message) const sendTextLaterRef = useRef<any>(undefined) useEffect(() => { sendTextLaterRef.current = _.throttle(sendText, 1000, {leading: false}) }, []) const sendTextLater = sendTextLaterRef.current return <Observer>{() => <div style={{...props.css, position:'relative', margin:0, border:0, padding:0, backgroundColor:'none'}}> <div style={{...props.css, color:'red', position:'relative', width:'100%', overflow: 'hidden', visibility:'hidden'}}>{text+'\u200b'}</div> <textarea value={text} autoFocus={props.autoFocus} style={{...props.css, font: 'inherit', verticalAlign:'baseline', resize:'none', position:'absolute', top:0, left:0, width:'100%', height:'100%', border:'none', letterSpacing: 'inherit', overflow: 'hidden' }} onChange={(ev) => { setText(ev.currentTarget.value) if (sendTextLater){ sendTextLater(ev.currentTarget.value, props) } }} onBlur={()=>{ if (sendTextLater){ sendTextLater.cancel() } sendText(text, props) }} onFocus={()=>{ }} onKeyDown={(ev) => { if (ev.key === 'Escape' || ev.key === 'Esc') { ev.stopPropagation() ev.preventDefault() if (sendTextLater){ sendTextLater.cancel() } sendText(text, props) props.stores.contents.setEditing('') } }} /> </div>}</Observer> } export const TextDiv: React.FC<TextDivProps> = (props:TextDivProps) => { const timestamp = formatTimestamp(props.text.time) // make formated timestamp for tooltip const rgb = props.text.color?.length ? props.text.color : getRandomColorRGB(props.text.name) const textColor = props.text.textColor?.length ? props.text.textColor : findTextColorRGB(rgb) const css:CSSProperties = { color: rgba(textColor, 1), backgroundColor:settings.useTransparent ? rgba(rgb, 0.5) : rgba(rgb, 1), padding:'0.1em', fontSize: 16, lineHeight: 1.2, width:'100%', overflow: 'clip', boxSizing: 'border-box', whiteSpace: 'pre-wrap', wordWrap: 'break-word', overflowWrap: 'break-word', } const {backgroundColor, ...cssEdit} = css cssEdit.backgroundColor = rgba(rgb, 1) return <Tooltip title={<React.Fragment>{props.text.name} <br /> {timestamp}</React.Fragment>} placement="left" arrow={true} suppressContentEditableWarning={true}><div> {props.textEditing ? <TextEdit {...props} css={cssEdit} /> : <div style={css} // Select text by static click onPointerDown={()=>{ props.member.isStatic = true }} onPointerMove={()=>{ props.member.isStatic = false }} onPointerUp={(ev)=>{ if (!props.member.isStatic){ return } const target = ev.target if (target instanceof Node){ ev.preventDefault() const selection = window.getSelection() if (selection){ if (selection.rangeCount && selection.getRangeAt(0).toString()){ selection.removeAllRanges() }else{ const range = document.createRange() range.selectNode(target) selection.removeAllRanges() selection.addRange(range) } } } }} > {props.textToShow} </div>} </div></Tooltip> } const cssText: CSSProperties = { height: '100%', width: '100%', whiteSpace: 'pre-line', pointerEvents: 'auto', overflowY: 'auto', overflowX: 'visible', wordWrap: 'break-word', } const useStyles = makeStyles({ text: cssText, textEdit: { ...cssText, cursor: 'default', userSelect: 'text', }, }) export const Text: React.FC<ContentProps> = (props:ContentProps) => { assert(props.content.type === 'text') const {contents, participants} = props.stores const memberRef = React.useRef<TextMember>(new TextMember()) const member = memberRef.current const classes = useStyles() const ref = useRef<HTMLDivElement>(null) const url = props.content.url const newTexts = JSON.parse(url) as TextMessages //const refEdit = useRef<HTMLDivElement>(null) const editing = useObserver(() => contents.editing === props.content.id) if (editing){ contents.setBeforeChangeEditing((cur, next) => { if (cur === props.content.id && next === ''){ if (ref.current){ member.messages = member.messages.filter(text => text.message.length) onUpdateTexts(member.messages, ref.current, props) } contents.setBeforeChangeEditing() // clear me } }) } useEffect(() => { if (!editing && ref.current) { if (ref.current.scrollLeft!==newTexts.scroll[0] || ref.current.scrollTop!==newTexts.scroll[1]){ member.abortScroll = true ref.current?.scroll(newTexts.scroll[0], newTexts.scroll[1]) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [newTexts.scroll[0], newTexts.scroll[1], editing]) // Update remote messages const indices = new Set<number>() newTexts.messages.forEach((newMessage) => { const index = member.messages.findIndex(msg => msg.pid === newMessage.pid && msg.time === newMessage.time) if (index === -1) { indices.add(member.messages.length) member.messages.push(newMessage) // Add new messages }else if (newMessage.pid !== participants.localId){ indices.add(index) member.messages[index] = newMessage // Update remote messages } }) // remove removed messages member.messages = member.messages.filter((msg, idx) => msg.pid === participants.localId || indices.has(idx)) member.messages.sort(compTextMessage) let focusToEdit:undefined|TextMessage = undefined if (editing) { // Make a new message to edit if needed. const last = member.messages.pop() if (last) { member.messages.push(last) } if (last?.pid !== participants.localId) { member.messages.push({message:'', pid:participants.localId, name:participants.local.information.name, color: participants.local.information.color, textColor: participants.local.information.textColor, time:Date.now()}) } if (!member.editing) { // Find the message to focus to edit, i.e. my last message. member.messages.reverse() focusToEdit = member.messages.find(message => message.pid === participants.localId) member.messages.reverse() } } // Makeing text (JSX element) to show const textDivs = member.messages.map((text, idx) => { const textEditing = (editing && (text.pid === participants.localId || !participants.remote.has(text.pid))) let textToShow:JSX.Element[] = [] if (!textEditing){ textToShow = textToLinkedText(text.message) } return <TextDiv {...props} key={idx} text={text} div={ref.current} textToShow={textToShow} member={member} textEditing={textEditing} autoFocus = {text === focusToEdit} /> }) const INTERVAL = 200 const handleScroll = _.debounce((ev:React.UIEvent<HTMLDivElement, UIEvent>) => { if (ref.current){ if (member.abortScroll) { member.abortScroll = false }else{ // console.log('sendScroll') onUpdateTexts(member.messages, ref.current, props) } } }, INTERVAL) member.editing = editing return <div ref={ref} className = {editing ? classes.textEdit : classes.text} onWheel = {ev => ev.ctrlKey || ev.stopPropagation() } onScroll = {(ev) => { if (!editing) { handleScroll(ev) } }} > {textDivs} </div> }