diff --git a/src/lib/strformat.js b/src/lib/strformat.js index eee1a37b..b0b30c54 100644 --- a/src/lib/strformat.js +++ b/src/lib/strformat.js @@ -39,7 +39,7 @@ export function bytesToHumanSize(bytes) { // Get 32 bit integer hash value for a string. Ideally it should produce the same value // as Java's String#hash(). -export function stringToColorHash(value) { +function stringToColorHash(value) { let hash = 0; value = '' + value; for (let i = 0; i < value.length; i++) { diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index 20d03aa8..7182e9f7 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -674,7 +674,7 @@ class MessagesView extends React.Component { sendMessage(msg, uploadCompletionPromise, uploader) { let head; if (this.state.reply && this.state.reply.content) { - head = {replyToSeq: this.state.reply.seq}; + head = {reply: '' + this.state.reply.seq}; // Turn it into Drafty so we can make a quoted Drafty object later. if (typeof msg == 'string') { msg = Drafty.parse(msg); @@ -879,79 +879,87 @@ class MessagesView extends React.Component { }, this); } - this.convertIntoThumbnails(ents, 0, (success) => { - if (success) { + + // Turn all images into thumbnails. + const promises = []; + ents.map((ex) => { + let p = new Promise((resolve, reject) => { + const scale = (origBlob) => { + imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false, + // Success + (mime, blob, width, height, fname) => { + ex.data.mime = mime; + ex.data.size = blob.size; + ex.data.width = width; + ex.data.height = height; + ex.data.name = fname; + ex.data.ref = undefined; + + blobToBase64(blob, (blobMime, tinyBits64) => { + ex.data.val = tinyBits64; + resolve(true); + }); + }, + // Failure + (err) => { + reject(`Could not scale image: ${err}`); + }); + } + if (ex.data.val) { + const b = base64ToBlob(ex.data.val, ex.data.mime); + if (b) { + scale(b); + } + } else { + const from = this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref)); + fetch(from) + .then(e => { + if (e.ok) { + return e.blob(); + } else { + reject(`Image fetch unsuccessful: ${e.status} - ${e.statusText}`); + } + }) + .then((b) => scale(b)) + .catch((err) => reject(`Error fetching image data: ${err}`)); + return; + } + }); + + promises.push(p); + }); + + Promise.all(promises) + .then((_) => { + // All done. Create a reply quote. const msg = Drafty.createQuote(header, cont, letterTileColorId(thisFrom)); this.setState({reply: {content: msg, seq: m.seq}}); - } - }); + }) + .catch((err) => { + this.props.onError(err, 'err'); + }); return; } } } - // Turn images in the provided Drafty.ent entries into thumbnails. - convertIntoThumbnails(ents, idx, done) { - if (idx >= ents.length) { - done(true); - return; - } - - const scale = (origBlob) => { - imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false, - // Success - (mime, blob, width, height, fname) => { - const ex = ents[idx]; - - ex.data.mime = mime; - ex.data.size = blob.size; - ex.data.width = width; - ex.data.height = height; - ex.data.name = fname; - ex.data.ref = undefined; - - blobToBase64(blob, (blobMime, tinyBits64) => { - ex.data.val = tinyBits64; - this.convertIntoThumbnails(ents, idx + 1, done); - }); - }, - // Failure - (err) => { - done(false); - this.props.onError(err, 'err'); - }); - } - const ex = ents[idx]; - if (ex.data.val) { - const b = base64ToBlob(ex.data.val, ex.data.mime); - if (b) { - scale.call(this, b); - } - } else { - const from = this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref)); - fetch(from) - .then(e => e.blob()) - .then((b) => { - scale.call(this, b); - }); - return; - } - } - handleCancelReply() { this.setState({reply: null}) } handleQuoteClick(replyToSeq) { const ref = this.getOrCreateMessageRef(replyToSeq); + if (!ref) { + return; + } 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); + console.error("Unresolved message ref: seqId", replyToSeq); } } @@ -975,7 +983,7 @@ class MessagesView extends React.Component { ); }); @@ -1225,17 +1236,17 @@ function quotePreviewFmt(fmt, ent) { return [new_fmt, ent]; case 'BN': new_fmt.tp = null; - return [new_fmt, { tp: 'IC', data: { orig: 'BN', iconName: 'dashboard', iconTitle: 'drafty_form'} }]; + return [new_fmt, { tp: 'IC', data: { orig: 'BN', iconName: 'button'}}]; case 'FM': new_fmt.tp = null; new_fmt.len = 0; - return [new_fmt, {tp: 'IC', data: { orig: 'FM', iconName: 'dashboard', iconTitle: 'drafty_form'} }]; + return [new_fmt, {tp: 'IC', data: { orig: 'FM', iconName: 'form'}}]; case 'RW': return [null, null]; case 'EX': // Make it an icon. new_fmt.tp = null; - return [new_fmt, {tp: 'IC', data: { orig: 'EX', iconName: 'attachment', iconTitle: 'drafty_attachment'} }]; + return [new_fmt, {tp: 'IC', data: { orig: 'EX', iconName: 'attachment'}}]; case 'QQ': // Quote/citation. return [null,null]; @@ -1327,14 +1338,31 @@ function draftyFormatter(style, data, values, key) { break; case 'IC': // Icon. - if (data.orig == 'BN') { + if (data.iconName == 'button') { attr.className = 'flat-button faux'; } else { + let iconName; + let iconTitle; + switch (data.iconName) { + case 'form': + iconName = 'dashboard'; + iconTitle = 'drafty_form'; + break; + case 'attachment': + iconName = 'attachment'; + iconTitle = 'drafty_attachment'; + break; + default: + break; + } el = React.Fragment; - //values = [] - const iconKey = data.orig.toLowerCase(); - values = [{data.iconName}, - formatMessage(messages[data.iconTitle])].concat(values || []); + if (iconName && iconTitle) { + const iconKey = data.orig.toLowerCase(); + values = [{iconName}, + formatMessage(messages[iconTitle])].concat(values || []); + } else { + values = []; + } } break; default: diff --git a/src/widgets/chat-message.jsx b/src/widgets/chat-message.jsx index d980a9ed..4e638877 100644 --- a/src/widgets/chat-message.jsx +++ b/src/widgets/chat-message.jsx @@ -62,7 +62,8 @@ class BaseChatMessage extends React.PureComponent { e.preventDefault(); e.stopPropagation(); const menuItems = this.props.received == Tinode.MESSAGE_STATUS_FAILED ? ['menu_item_send_retry'] : []; - if (this.props.received >= Tinode.MESSAGE_STATUS_FAILED && + if (this.props.userIsWriter && + this.props.received > Tinode.MESSAGE_STATUS_FAILED && this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) { menuItems.push('menu_item_reply'); } @@ -82,7 +83,9 @@ class BaseChatMessage extends React.PureComponent { e.preventDefault(); e.stopPropagation(); const replyToSeq = this.props.replyToSeq; - this.props.onQuoteClick(replyToSeq); + if (replyToSeq) { + this.props.onQuoteClick(replyToSeq); + } } render() { diff --git a/src/widgets/doc-preview.jsx b/src/widgets/doc-preview.jsx index dd3c3116..67f69a80 100644 --- a/src/widgets/doc-preview.jsx +++ b/src/widgets/doc-preview.jsx @@ -46,7 +46,7 @@ export default class DocPreview extends React.PureComponent {