diff --git a/src/lib/strformat.js b/src/lib/strformat.js index efc341f3..eee1a37b 100644 --- a/src/lib/strformat.js +++ b/src/lib/strformat.js @@ -49,11 +49,16 @@ export function stringToColorHash(value) { return hash; } +// User avatar letter color id. +export function letterTileColorId(userId) { + return Math.abs(stringToColorHash(userId)) % 16; +} + // Converts user or topic ID to a CSS color class. Ideally it should produce the same color value as Java version. // params: // - {string} id: user or topic ID // - {boolean} light: light or dark version of the color. // - {boolean} fg: foreground or background color. export function idToColorClass(id, light, fg) { - return (light ? 'lt-' : 'dk-') + (fg ? 'fg-' : 'bg-') + (Math.abs(stringToColorHash(id)) % 16); + return (light ? 'lt-' : 'dk-') + (fg ? 'fg-' : 'bg-') + letterTileColorId(id); } diff --git a/src/lib/utils.js b/src/lib/utils.js index b5fce4e6..168ad323 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -2,13 +2,6 @@ import Tinode from 'tinode-sdk'; -import { stringToColorHash } from './strformat.js'; - -// User avatar letter color id. -export function letterTileColor(userId) { - return Math.abs(stringToColorHash(userId)) % 16; -} - // Make shortcut icon appear with a green dot + show unread count in title. export function updateFavicon(count) { const oldIcon = document.getElementById('shortcut-icon'); diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index b8047b7e..752c1443 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -22,8 +22,8 @@ import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PA import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64, imageScaled, makeImageUrl, fitImageSize } from '../lib/blob-helpers.js'; import HashNavigation from '../lib/navigation.js'; -import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js'; -import { sanitizeImageUrl, letterTileColor } from '../lib/utils.js'; +import { bytesToHumanSize, shortDateFormat, letterTileColorId } from '../lib/strformat.js'; +import { sanitizeImageUrl } from '../lib/utils.js'; // Run timer with this frequency (ms) for checking notification queue. const NOTIFICATION_EXEC_INTERVAL = 300; @@ -121,10 +121,22 @@ class MessagesView extends React.Component { this.handleQuoteClick = this.handleQuoteClick.bind(this); this.convertIntoThumbnails = this.convertIntoThumbnails.bind(this); + this.chatMessageRefs = {}; + this.getOrCreateMessageRef = this.getOrCreateMessageRef.bind(this); + this.readNotificationQueue = []; this.readNotificationTimer = null; } + getOrCreateMessageRef(seqId) { + if (this.chatMessageRefs.hasOwnProperty(seqId)) { + return this.chatMessageRefs[seqId]; + } + const ref = React.createRef(); + this.chatMessageRefs[seqId] = ref; + return ref; + } + componentDidMount() { // this.propsChange(this.props, this.state); if (this.messagesScroller) { @@ -868,7 +880,7 @@ class MessagesView extends React.Component { } this.convertIntoThumbnails(ents, 0, (success) => { if (success) { - const msg = Drafty.createQuote(header, cont, letterTileColor(thisFrom)); + const msg = Drafty.createQuote(header, cont, letterTileColorId(thisFrom)); this.setState({reply: {content: msg, seq: m.seq}}); } }); @@ -916,12 +928,11 @@ class MessagesView extends React.Component { scale.call(this, b); } } else { - //let saveThis = this; const from = this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref)); fetch(from) .then(e => e.blob()) .then((b) => { - scale.call(saveThis, b); + scale.call(this, b); }); return; } @@ -932,11 +943,14 @@ class MessagesView extends React.Component { } handleQuoteClick(replyToSeq) { - const element = document.getElementById("msg-" + replyToSeq); + const ref = this.getOrCreateMessageRef(replyToSeq); + const element = ref.current; if (element) { element.scrollIntoView({block: "center", behavior: "smooth"}); element.style.backgroundColor = 'rgb(0, 0, 0, 0.4)'; setTimeout(() => { element.style.backgroundColor = ''; } , 1000); + } else { + console.error("Unresolved ref: seqId", replyToSeq); } } @@ -1036,6 +1050,9 @@ class MessagesView extends React.Component { chatBoxClass='chat-box'; } + // Ref for this chat message. + const ref = this.getOrCreateMessageRef(msg.seq); + messageNodes.push( ); @@ -1167,7 +1185,7 @@ class MessagesView extends React.Component { onError={this.props.onError} replyTo={this.state.reply} onQuoteClick={this.handleQuoteClick} - onFormatQuote={draftyFormatter} + formatter={draftyFormatter} onCancelReply={this.handleCancelReply} />} ); @@ -1179,6 +1197,7 @@ class MessagesView extends React.Component { } }; +// Transforms styles (and corresponding entities) for formatting reply quotes. function quotePreviewFmt(fmt, ent) { let tp = fmt.tp; if (!tp) { diff --git a/src/widgets/chat-message.jsx b/src/widgets/chat-message.jsx index 4014d025..3c0d4643 100644 --- a/src/widgets/chat-message.jsx +++ b/src/widgets/chat-message.jsx @@ -10,7 +10,7 @@ import UploadingImage from './uploading-image.jsx' import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js'; -class ChatMessage extends React.PureComponent { +class BaseChatMessage extends React.PureComponent { constructor(props) { super(props); @@ -94,7 +94,6 @@ class ChatMessage extends React.PureComponent { const fullDisplay = (this.props.userFrom && this.props.response && !this.props.deleted && (this.props.sequence == 'single' || this.props.sequence == 'last')); - const msgId = 'msg-' + this.props.seq; let content = this.props.content; const attachments = []; if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) { @@ -133,7 +132,7 @@ class ChatMessage extends React.PureComponent { } return ( -
  • +
  • {this.props.userFrom && this.props.response ?
    {fullDisplay ? @@ -182,4 +181,7 @@ class ChatMessage extends React.PureComponent { } }; -export default injectIntl(ChatMessage); +const IntlChatMessage = injectIntl(BaseChatMessage); +const ChatMessage = React.forwardRef((props, ref) => ); + +export default ChatMessage; diff --git a/src/widgets/doc-preview.jsx b/src/widgets/doc-preview.jsx index 6ba01982..8714c91d 100644 --- a/src/widgets/doc-preview.jsx +++ b/src/widgets/doc-preview.jsx @@ -47,7 +47,7 @@ export default class DocPreview extends React.PureComponent { noInput={true} tinode={this.props.tinode} replyTo={this.props.replyTo} - formatter={this.props.onFormatQuote} + formatter={this.props.formatter} onCancelReply={this.props.onCancelReply} onSendMessage={this.handleSendDoc} onError={this.props.onError} /> diff --git a/src/widgets/image-preview.jsx b/src/widgets/image-preview.jsx index c0018ce3..d62ea997 100644 --- a/src/widgets/image-preview.jsx +++ b/src/widgets/image-preview.jsx @@ -75,7 +75,7 @@ export default class ImagePreview extends React.PureComponent { acceptBlank={true} tinode={this.props.tinode} replyTo={this.props.replyTo} - formatter={this.props.onFormatQuote} + formatter={this.props.formatter} onCancelReply={this.props.onCancelReply} onSendMessage={this.handleSendImage} onError={this.props.onError} /> diff --git a/src/widgets/send-message.jsx b/src/widgets/send-message.jsx index b9cd6ab0..bb660433 100644 --- a/src/widgets/send-message.jsx +++ b/src/widgets/send-message.jsx @@ -162,7 +162,7 @@ class SendMessage extends React.PureComponent { let quote = null; if (this.props.replyTo) { - quote = Drafty.format(this.props.replyTo.content, this.props.onFormatQuote, this); + quote = Drafty.format(this.props.replyTo.content, this.props.formatter, this); } return (