Updates to replyTo feature.
This commit is contained in:
+100
-4
@@ -1377,7 +1377,6 @@ img.channel {
|
|||||||
flex: none;
|
flex: none;
|
||||||
padding: 0.4rem 0.4rem 0.4rem 0.6rem;
|
padding: 0.4rem 0.4rem 0.4rem 0.6rem;
|
||||||
background-color: #eceff1;
|
background-color: #eceff1;
|
||||||
/* box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); */ /* Faint shadow above panel */
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
}
|
}
|
||||||
@@ -2154,6 +2153,106 @@ ul.tabbar li.active > a {
|
|||||||
color: #D84315!important;
|
color: #D84315!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Colors of user mentions */
|
||||||
|
.mn-light-color0 {
|
||||||
|
color: #ef9a9a;
|
||||||
|
}
|
||||||
|
.mn-light-color1 {
|
||||||
|
color: #90caf9;
|
||||||
|
}
|
||||||
|
.mn-light-color2 {
|
||||||
|
color: #b0bec5;
|
||||||
|
}
|
||||||
|
.mn-light-color3 {
|
||||||
|
color: #b39ddb;
|
||||||
|
}
|
||||||
|
.mn-light-color4 {
|
||||||
|
color: #ffab91;
|
||||||
|
}
|
||||||
|
.mn-light-color5 {
|
||||||
|
color: #a5d6a7;
|
||||||
|
}
|
||||||
|
.mn-light-color6 {
|
||||||
|
color: #eeeeee;
|
||||||
|
}
|
||||||
|
.mn-light-color7 {
|
||||||
|
color: #e6ee9c;
|
||||||
|
}
|
||||||
|
.mn-light-color8 {
|
||||||
|
color: #c5e1a5;
|
||||||
|
}
|
||||||
|
.mn-light-color9 {
|
||||||
|
color: #f0e57d;
|
||||||
|
}
|
||||||
|
.mn-light-color10 {
|
||||||
|
color: #f48fb1;
|
||||||
|
}
|
||||||
|
.mn-light-color11 {
|
||||||
|
color: #9fa8da;
|
||||||
|
}
|
||||||
|
.mn-light-color12 {
|
||||||
|
color: #ffe082;
|
||||||
|
}
|
||||||
|
.mn-light-color13 {
|
||||||
|
color: #bcaaa4;
|
||||||
|
}
|
||||||
|
.mn-light-color14 {
|
||||||
|
color: #80deea;
|
||||||
|
}
|
||||||
|
.mn-light-color15 {
|
||||||
|
color: #ce93d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Darker version of user mention colors */
|
||||||
|
.mn-dark-color0 {
|
||||||
|
color: #C62828;
|
||||||
|
}
|
||||||
|
.mn-dark-color1 {
|
||||||
|
color: #AD1457;
|
||||||
|
}
|
||||||
|
.mn-dark-color2 {
|
||||||
|
color: #6A1B9A;
|
||||||
|
}
|
||||||
|
.mn-dark-color3 {
|
||||||
|
color: #4527A0;
|
||||||
|
}
|
||||||
|
.mn-dark-color4 {
|
||||||
|
color: #283593;
|
||||||
|
}
|
||||||
|
.mn-dark-color5 {
|
||||||
|
color: #1565C0;
|
||||||
|
}
|
||||||
|
.mn-dark-color6 {
|
||||||
|
color: #0277BD;
|
||||||
|
}
|
||||||
|
.mn-dark-color7 {
|
||||||
|
color: #00838F;
|
||||||
|
}
|
||||||
|
.mn-dark-color8 {
|
||||||
|
color: #00695C;
|
||||||
|
}
|
||||||
|
.mn-dark-color9 {
|
||||||
|
color: #2E7D32;
|
||||||
|
}
|
||||||
|
.mn-dark-color10 {
|
||||||
|
color: #558B2F;
|
||||||
|
}
|
||||||
|
.mn-dark-color11 {
|
||||||
|
color: #9E9D24;
|
||||||
|
}
|
||||||
|
.mn-dark-color12 {
|
||||||
|
color: #F9A825;
|
||||||
|
}
|
||||||
|
.mn-dark-color13 {
|
||||||
|
color: #FF8F00;
|
||||||
|
}
|
||||||
|
.mn-dark-color14 {
|
||||||
|
color: #EF6C00;
|
||||||
|
}
|
||||||
|
.mn-dark-color15 {
|
||||||
|
color: #D84315;
|
||||||
|
}
|
||||||
|
|
||||||
/* Material icons resized from the default 24pt */
|
/* Material icons resized from the default 24pt */
|
||||||
.material-icons {
|
.material-icons {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
@@ -2463,8 +2562,6 @@ ul.tabbar li.active > a {
|
|||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
margin-left: 0.2em;
|
margin-left: 0.2em;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-quote-header {
|
.reply-quote-header {
|
||||||
@@ -2475,4 +2572,3 @@ ul.tabbar li.active > a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import Tinode from 'tinode-sdk';
|
||||||
|
|
||||||
|
import { stringHash } from './strformat.js';
|
||||||
|
|
||||||
|
// User avatar letter color id.
|
||||||
|
export function letterTileColor(userId) {
|
||||||
|
return Math.abs(stringHash(userId)) % 16;
|
||||||
|
}
|
||||||
|
|
||||||
// Make shortcut icon appear with a green dot + show unread count in title.
|
// Make shortcut icon appear with a green dot + show unread count in title.
|
||||||
export function updateFavicon(count) {
|
export function updateFavicon(count) {
|
||||||
const oldIcon = document.getElementById('shortcut-icon');
|
const oldIcon = document.getElementById('shortcut-icon');
|
||||||
|
|||||||
+152
-110
@@ -23,7 +23,7 @@ import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileTo
|
|||||||
imageScaled, makeImageUrl, fitImageSize } from '../lib/blob-helpers.js';
|
imageScaled, makeImageUrl, fitImageSize } from '../lib/blob-helpers.js';
|
||||||
import HashNavigation from '../lib/navigation.js';
|
import HashNavigation from '../lib/navigation.js';
|
||||||
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
||||||
import { sanitizeImageUrl } from '../lib/utils.js';
|
import { sanitizeImageUrl, letterTileColor } from '../lib/utils.js';
|
||||||
|
|
||||||
// Run timer with this frequency (ms) for checking notification queue.
|
// Run timer with this frequency (ms) for checking notification queue.
|
||||||
const NOTIFICATION_EXEC_INTERVAL = 300;
|
const NOTIFICATION_EXEC_INTERVAL = 300;
|
||||||
@@ -114,6 +114,7 @@ class MessagesView extends React.Component {
|
|||||||
this.postReadNotification = this.postReadNotification.bind(this);
|
this.postReadNotification = this.postReadNotification.bind(this);
|
||||||
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
||||||
|
|
||||||
|
this.doSendMessage = this.doSendMessage.bind(this);
|
||||||
this.handleSendMessage = this.handleSendMessage.bind(this);
|
this.handleSendMessage = this.handleSendMessage.bind(this);
|
||||||
this.handlePickReply = this.handlePickReply.bind(this);
|
this.handlePickReply = this.handlePickReply.bind(this);
|
||||||
this.handleCancelReply = this.handleCancelReply.bind(this);
|
this.handleCancelReply = this.handleCancelReply.bind(this);
|
||||||
@@ -652,6 +653,17 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doSendMessage(msg, uploadCompletionPromise, uploader) {
|
||||||
|
let head = null;
|
||||||
|
if (this.state.reply && this.state.reply.content) {
|
||||||
|
head = {replyToSeq: this.state.reply.seq};
|
||||||
|
let quote = this.state.reply.content;
|
||||||
|
msg = Drafty.attachQuote(msg, quote);
|
||||||
|
}
|
||||||
|
this.props.sendMessage(msg, uploadCompletionPromise, uploader, head);
|
||||||
|
this.setState({reply: null});
|
||||||
|
}
|
||||||
|
|
||||||
// Send attachment as Drafty message:
|
// Send attachment as Drafty message:
|
||||||
// - if file is too large, upload it and send a s link.
|
// - if file is too large, upload it and send a s link.
|
||||||
// - if file is small enough, just send it in-band.
|
// - if file is small enough, just send it in-band.
|
||||||
@@ -671,12 +683,13 @@ class MessagesView extends React.Component {
|
|||||||
urlPromise: uploadCompletionPromise
|
urlPromise: uploadCompletionPromise
|
||||||
});
|
});
|
||||||
// Pass data and the uploader to the TinodeWeb.
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
|
this.doSendMessage(msg, uploadCompletionPromise, uploader);
|
||||||
} else {
|
} else {
|
||||||
// Small enough to send inband.
|
// Small enough to send inband.
|
||||||
fileToBase64(file,
|
fileToBase64(file,
|
||||||
(mime, bits, fname) => {
|
(mime, bits, fname) => {
|
||||||
this.props.sendMessage(Drafty.attachFile(null, {mime: mime, data: bits, filename: fname}));
|
const msg = Drafty.attachFile(null, {mime: mime, data: bits, filename: fname});
|
||||||
|
this.doSendMessage(msg, undefined, undefined);
|
||||||
},
|
},
|
||||||
this.props.onError
|
this.props.onError
|
||||||
);
|
);
|
||||||
@@ -737,10 +750,10 @@ class MessagesView extends React.Component {
|
|||||||
});
|
});
|
||||||
if (caption) {
|
if (caption) {
|
||||||
msg = Drafty.appendLineBreak(msg);
|
msg = Drafty.appendLineBreak(msg);
|
||||||
msg = Drafty.append(msg, Drafty.init(caption));
|
msg = Drafty.append(msg, Drafty.parse(caption));
|
||||||
}
|
}
|
||||||
// Pass data and the uploader to the TinodeWeb.
|
// Pass data and the uploader to the TinodeWeb.
|
||||||
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
|
this.doSendMessage(msg, uploadCompletionPromise, uploader);
|
||||||
}
|
}
|
||||||
)},
|
)},
|
||||||
// Failure
|
// Failure
|
||||||
@@ -763,9 +776,9 @@ class MessagesView extends React.Component {
|
|||||||
});
|
});
|
||||||
if (caption) {
|
if (caption) {
|
||||||
msg = Drafty.appendLineBreak(msg);
|
msg = Drafty.appendLineBreak(msg);
|
||||||
msg = Drafty.append(msg, Drafty.init(caption));
|
msg = Drafty.append(msg, Drafty.parse(caption));
|
||||||
}
|
}
|
||||||
this.props.sendMessage(msg);
|
this.doSendMessage(msg, undefined, undefined);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,33 +817,23 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePickReply(m) {
|
handlePickReply(m) {
|
||||||
this.setState({reply: null})
|
this.setState({reply: null});
|
||||||
//instance.setState({typingIndicator: false});
|
|
||||||
if (m) {
|
if (m) {
|
||||||
let cont = m.content
|
let cont = m.content
|
||||||
if (cont) {
|
if (cont) {
|
||||||
if (typeof cont == 'string') {
|
if (typeof cont == 'string') {
|
||||||
cont = Drafty.init(cont);
|
cont = Drafty.init(cont);
|
||||||
}
|
}
|
||||||
cont = Drafty.preview(cont, 30, (src, target) => {
|
cont = Drafty.replyPreview(cont, 30, quotePreviewFmt);
|
||||||
if (src.data && src.tp && src.tp == 'IM') {
|
|
||||||
// For images, copy val and ref fields.
|
|
||||||
['val', 'ref'].forEach(key => {
|
|
||||||
const val = src.data[key];
|
|
||||||
if (val) {
|
|
||||||
target.data[key] = val;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Get the author.
|
||||||
let header = '';
|
let header = '';
|
||||||
const seq = m.seq;
|
const seq = m.seq;
|
||||||
const idx = this.msgIndex(seq);
|
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
if (idx < this.state.messages.length) {
|
const m0 = topic.findMessage(seq);
|
||||||
const m0 = this.state.messages[idx];
|
let thisFrom;
|
||||||
|
if (m0) {
|
||||||
let thisFrom = m0.from || 'chan';
|
thisFrom = m0.from || 'chan';
|
||||||
if (thisFrom != this.props.myUserId) {
|
if (thisFrom != this.props.myUserId) {
|
||||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||||
const user = topic.userDesc(thisFrom);
|
const user = topic.userDesc(thisFrom);
|
||||||
@@ -844,7 +847,7 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
header = Drafty.init(header);
|
header = Drafty.init(header);
|
||||||
// Make small image previews.
|
// Make small image previews.
|
||||||
let ents = [];
|
const ents = [];
|
||||||
if (cont.ent) {
|
if (cont.ent) {
|
||||||
Drafty.entities(cont, (data, idx, tp) => {
|
Drafty.entities(cont, (data, idx, tp) => {
|
||||||
if (tp == 'IM') {
|
if (tp == 'IM') {
|
||||||
@@ -858,7 +861,7 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
this.convertIntoThumbnails(ents, 0, (success) => {
|
this.convertIntoThumbnails(ents, 0, (success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
const msg = Drafty.createQuote(header, cont);
|
const msg = Drafty.createQuote(header, cont, letterTileColor(thisFrom));
|
||||||
this.setState({reply: {content: msg, seq: m.seq}});
|
this.setState({reply: {content: msg, seq: m.seq}});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -868,20 +871,6 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
msgIndex(seq) {
|
|
||||||
let l = -1;
|
|
||||||
let r = this.state.messages.length;
|
|
||||||
while (r > l + 1) {
|
|
||||||
const m = Math.floor((l + r) / 2);
|
|
||||||
if (this.state.messages[m].seq < seq) {
|
|
||||||
l = m;
|
|
||||||
} else {
|
|
||||||
r = m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn images in the provided Drafty.ent entries into thumbnails.
|
// Turn images in the provided Drafty.ent entries into thumbnails.
|
||||||
convertIntoThumbnails(ents, idx, done) {
|
convertIntoThumbnails(ents, idx, done) {
|
||||||
if (idx >= ents.length) {
|
if (idx >= ents.length) {
|
||||||
@@ -889,13 +878,14 @@ class MessagesView extends React.Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scale(origBlob) {
|
const scale = (origBlob) => {
|
||||||
imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false,
|
imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false,
|
||||||
// Success
|
// Success
|
||||||
(mime, blob, width, height, fname) => {
|
(mime, blob, width, height, fname) => {
|
||||||
let ex = ents[idx];
|
const ex = ents[idx];
|
||||||
|
|
||||||
ex.data.mime = mime;
|
ex.data.mime = mime;
|
||||||
ex.data.val = blob;
|
ex.data.size = blob.size;
|
||||||
ex.data.width = width;
|
ex.data.width = width;
|
||||||
ex.data.height = height;
|
ex.data.height = height;
|
||||||
ex.data.name = fname;
|
ex.data.name = fname;
|
||||||
@@ -919,11 +909,11 @@ class MessagesView extends React.Component {
|
|||||||
scale.call(this, b);
|
scale.call(this, b);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let saveThis = this;
|
//let saveThis = this;
|
||||||
const from = this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref));
|
const from = this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref));
|
||||||
fetch(from)
|
fetch(from)
|
||||||
.then(e => e.blob())
|
.then(e => e.blob())
|
||||||
.then(function(b) {
|
.then((b) => {
|
||||||
scale.call(saveThis, b);
|
scale.call(saveThis, b);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -931,21 +921,11 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSendMessage(msg) {
|
handleSendMessage(msg) {
|
||||||
if (this.state.reply) {
|
if (this.state.reply && this.state.reply.content && typeof msg == 'string') {
|
||||||
let quote = this.state.reply.content;
|
// Turn it into Drafty so we can make a quoted Drafty object later.
|
||||||
if (quote) {
|
msg = Drafty.parse(msg);
|
||||||
if (typeof msg == 'string') {
|
|
||||||
msg = Drafty.init(msg);
|
|
||||||
}
|
|
||||||
msg = Drafty.attachQuote(msg, quote);
|
|
||||||
let head = {replyToSeq: this.state.reply.seq};
|
|
||||||
|
|
||||||
this.props.sendMessage(msg, undefined, undefined, head);
|
|
||||||
this.setState({reply: null})
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.props.sendMessage(msg)
|
this.doSendMessage(msg, undefined, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelReply() {
|
handleCancelReply() {
|
||||||
@@ -980,6 +960,10 @@ class MessagesView extends React.Component {
|
|||||||
component2 = (
|
component2 = (
|
||||||
<ImagePreview
|
<ImagePreview
|
||||||
content={this.state.imagePreview}
|
content={this.state.imagePreview}
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
replyTo={this.state.reply}
|
||||||
|
formatter={draftyFormatter}
|
||||||
|
onCancelReply={this.handleCancelReply}
|
||||||
onClose={this.handleClosePreview}
|
onClose={this.handleClosePreview}
|
||||||
onSendMessage={this.sendImageAttachment} />
|
onSendMessage={this.sendImageAttachment} />
|
||||||
);
|
);
|
||||||
@@ -995,6 +979,10 @@ class MessagesView extends React.Component {
|
|||||||
component2 = (
|
component2 = (
|
||||||
<DocPreview
|
<DocPreview
|
||||||
content={this.state.docPreview}
|
content={this.state.docPreview}
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
replyTo={this.state.reply}
|
||||||
|
formatter={draftyFormatter}
|
||||||
|
onCancelReply={this.handleCancelReply}
|
||||||
onClose={this.handleClosePreview}
|
onClose={this.handleClosePreview}
|
||||||
onSendMessage={this.sendFileAttachment} />
|
onSendMessage={this.sendFileAttachment} />
|
||||||
);
|
);
|
||||||
@@ -1074,7 +1062,7 @@ class MessagesView extends React.Component {
|
|||||||
pickReply={this.handlePickReply}
|
pickReply={this.handlePickReply}
|
||||||
replyToSeq={msg.head ? msg.head.replyToSeq : null}
|
replyToSeq={msg.head ? msg.head.replyToSeq : null}
|
||||||
onQuoteClick={this.handleQuoteClick}
|
onQuoteClick={this.handleQuoteClick}
|
||||||
onFormatQuote={quoteFormatter}
|
onFormatQuote={draftyFormatter}
|
||||||
|
|
||||||
key={msg.seq} />
|
key={msg.seq} />
|
||||||
);
|
);
|
||||||
@@ -1172,7 +1160,6 @@ class MessagesView extends React.Component {
|
|||||||
:
|
:
|
||||||
<SendMessage
|
<SendMessage
|
||||||
disabled={!this.state.isWriter}
|
disabled={!this.state.isWriter}
|
||||||
//onSendMessage={this.props.sendMessage}
|
|
||||||
onKeyPress={this.sendKeyPress}
|
onKeyPress={this.sendKeyPress}
|
||||||
onAttachFile={this.handleAttachFile}
|
onAttachFile={this.handleAttachFile}
|
||||||
onAttachImage={this.handleAttachImage}
|
onAttachImage={this.handleAttachImage}
|
||||||
@@ -1182,7 +1169,7 @@ class MessagesView extends React.Component {
|
|||||||
replyTo={this.state.reply}
|
replyTo={this.state.reply}
|
||||||
onQuoteClick={this.handleQuoteClick}
|
onQuoteClick={this.handleQuoteClick}
|
||||||
onSendMessage={this.handleSendMessage}
|
onSendMessage={this.handleSendMessage}
|
||||||
onFormatQuote={quoteFormatter}
|
onFormatQuote={draftyFormatter}
|
||||||
onCancelReply={this.handleCancelReply} />}
|
onCancelReply={this.handleCancelReply} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1194,31 +1181,69 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Converts Drafty object into a one-line reply quote.
|
function quotePreviewFmt(fmt, ent) {
|
||||||
function quoteFormatter(style, data, values, key) {
|
let tp = fmt.tp;
|
||||||
|
if (!tp) {
|
||||||
|
if (!ent || !ent.tp) {
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
tp = ent.tp;
|
||||||
|
}
|
||||||
|
const new_fmt = {at: fmt.at, len: fmt.len, tp: fmt.tp};
|
||||||
|
switch (tp) {
|
||||||
|
case 'BR':
|
||||||
|
// Replace new line with a space.
|
||||||
|
return [null, null];
|
||||||
|
case 'HL':
|
||||||
|
return [new_fmt, ent];
|
||||||
|
case 'LN':
|
||||||
|
// Disable links in previews.
|
||||||
|
return [null, null];
|
||||||
|
case 'IM':
|
||||||
|
// Keep images as is.
|
||||||
|
return [new_fmt, ent];
|
||||||
|
case 'BN':
|
||||||
|
new_fmt.tp = null;
|
||||||
|
return [new_fmt, { tp: 'IC', data: { orig: 'BN', iconName: 'dashboard', iconTitle: 'drafty_form'} }];
|
||||||
|
case 'FM':
|
||||||
|
new_fmt.tp = null;
|
||||||
|
new_fmt.len = 0;
|
||||||
|
return [new_fmt, {tp: 'IC', data: { orig: 'FM', iconName: 'dashboard', iconTitle: 'drafty_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'} }];
|
||||||
|
case 'QQ':
|
||||||
|
// Quote/citation.
|
||||||
|
return [null,null];
|
||||||
|
default:
|
||||||
|
return [new_fmt, ent];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts Drafty elements into React classes.
|
||||||
|
// 'this' is set by the caller.
|
||||||
|
function draftyFormatter(style, data, values, key) {
|
||||||
|
if (style == 'EX') {
|
||||||
|
// attachments are handled elsewhere.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let el = Drafty.tagName(style);
|
let el = Drafty.tagName(style);
|
||||||
let attr = { key: key };
|
|
||||||
if (el) {
|
if (el) {
|
||||||
const { formatMessage } = this.props.intl;
|
const { formatMessage } = this.props.intl;
|
||||||
|
let attr = Drafty.attrValue(style, data) || {};
|
||||||
|
attr.key = key;
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case 'BR':
|
|
||||||
// Replace new line with a space.
|
|
||||||
el = React.Fragment;
|
|
||||||
values = [' '];
|
|
||||||
break;
|
|
||||||
case 'HL':
|
case 'HL':
|
||||||
// Make highlight less prominent in preview.
|
// Highlighted text. Assign class name.
|
||||||
attr.className = 'highlight preview';
|
attr.className = 'highlight';
|
||||||
break;
|
|
||||||
case 'LN':
|
|
||||||
// Disable links in previews.
|
|
||||||
el = 'span';
|
|
||||||
break;
|
break;
|
||||||
case 'IM':
|
case 'IM':
|
||||||
// We show image thumbnails stored inline.
|
// Additional processing for images
|
||||||
if (data) {
|
if (data) {
|
||||||
attr = Drafty.attrValue(style, data) || {};
|
|
||||||
attr.key = key;
|
|
||||||
attr.className = 'inline-image';
|
attr.className = 'inline-image';
|
||||||
const dim = fitImageSize(data.width, data.height,
|
const dim = fitImageSize(data.width, data.height,
|
||||||
Math.min(this.props.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5), REM_SIZE * 24, false) ||
|
Math.min(this.props.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5), REM_SIZE * 24, false) ||
|
||||||
@@ -1230,52 +1255,69 @@ function quoteFormatter(style, data, values, key) {
|
|||||||
minWidth: dim.dstWidth + 'px',
|
minWidth: dim.dstWidth + 'px',
|
||||||
minHeight: dim.dstHeight + 'px'
|
minHeight: dim.dstHeight + 'px'
|
||||||
};
|
};
|
||||||
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
|
if (!Drafty.isProcessing(data)) {
|
||||||
attr.alt = data.name;
|
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
|
||||||
if (!attr.src) {
|
attr.alt = data.name;
|
||||||
attr.src = 'img/broken_image.png';
|
if (attr.src) {
|
||||||
|
attr.onClick = this.handleImagePreview;
|
||||||
|
attr.className += ' image-clickable';
|
||||||
|
attr.loading = 'lazy';
|
||||||
|
} else {
|
||||||
|
attr.src = 'img/broken_image.png';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use custom element instead of <img>.
|
||||||
|
el = UploadingImage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'BN':
|
case 'BN':
|
||||||
el = 'span';
|
// Button
|
||||||
attr.className = 'flat-button faux';
|
attr.onClick = this.handleFormButtonClick;
|
||||||
|
let inner = React.Children.map(values, (child) => {
|
||||||
|
return typeof child == 'string' ? child : undefined;
|
||||||
|
});
|
||||||
|
if (!inner || inner.length == 0) {
|
||||||
|
inner = [attr.name]
|
||||||
|
}
|
||||||
|
// Get text which will be sent back when the button is clicked.
|
||||||
|
attr['data-title'] = inner.join('');
|
||||||
|
break;
|
||||||
|
case 'MN':
|
||||||
|
// Mention
|
||||||
|
if (data && data.hasOwnProperty('color')) {
|
||||||
|
attr.className = 'mn-dark-color' + data.color;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'FM':
|
case 'FM':
|
||||||
el = React.Fragment;
|
// Form
|
||||||
values = [<i key="fm" className="material-icons">dashboard</i>,
|
attr.className = 'bot-form';
|
||||||
//messages.drafty_form].concat(values || []);
|
|
||||||
formatMessage(messages.drafty_form)].concat(values || []);
|
|
||||||
break;
|
break;
|
||||||
case 'RW':
|
case 'RW':
|
||||||
el = React.Fragment;
|
// Form element formatting is dependent on element content.
|
||||||
break;
|
|
||||||
case 'EX':
|
|
||||||
if (data && data.mime == 'application/json') {
|
|
||||||
// Ignore JSON attachments: they are form response payloads.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
el = React.Fragment;
|
|
||||||
values = [<i key="ex" className="material-icons">attachment</i>,
|
|
||||||
//messages.drafty_attachment];
|
|
||||||
formatMessage(messages.drafty_attachment)];
|
|
||||||
break;
|
break;
|
||||||
case 'QQ':
|
case 'QQ':
|
||||||
// Quote/citation.
|
// Quote/citation.
|
||||||
attr.className = 'reply-quote';
|
attr.className = 'reply-quote'
|
||||||
attr.onClick = this.handleQuoteClick;
|
attr.onClick = this.handleQuoteClick;
|
||||||
break;
|
break;
|
||||||
case 'QH':
|
case 'IC':
|
||||||
// Quote/citation.
|
// Icon.
|
||||||
attr.className = 'reply-quote-header';
|
if (data.orig == 'BN') {
|
||||||
break;
|
attr.className = 'flat-button faux';
|
||||||
case 'QB':
|
} else {
|
||||||
// Nothing.
|
el = React.Fragment;
|
||||||
|
//values = [<i >]
|
||||||
|
const iconKey = data.orig.toLowerCase();
|
||||||
|
values = [<i key={iconKey} className="material-icons">{data.iconName}</i>,
|
||||||
|
formatMessage(messages[data.iconTitle])].concat(values || []);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (el == '_UNKN') {
|
if (el == '_UNKN') {
|
||||||
el = React.Fragment;
|
// Unknown element.
|
||||||
values = [<i key="unkn" className="material-icons">extension</i>];
|
// TODO: make it prettier.
|
||||||
|
el = <><span className="material-icons">extension</span></>;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -904,6 +904,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// - msg - Drafty message with content
|
// - msg - Drafty message with content
|
||||||
// - promise - Promise to be resolved when the upload is completed
|
// - promise - Promise to be resolved when the upload is completed
|
||||||
// - uploader - for tracking progress
|
// - uploader - for tracking progress
|
||||||
|
// - head - head dictionary to be attached to the message
|
||||||
handleSendMessage(msg, promise, uploader, head) {
|
handleSendMessage(msg, promise, uploader, head) {
|
||||||
const topic = this.tinode.getTopic(this.state.topicSelected);
|
const topic = this.tinode.getTopic(this.state.topicSelected);
|
||||||
|
|
||||||
@@ -912,12 +913,7 @@ class TinodeWeb extends React.Component {
|
|||||||
msg._uploader = uploader;
|
msg._uploader = uploader;
|
||||||
|
|
||||||
if (head) {
|
if (head) {
|
||||||
if (!msg.hasOwnProperty('head')) {
|
msg.head = Object.assign(msg.head || {}, head);
|
||||||
msg.head = {};
|
|
||||||
}
|
|
||||||
for (const [key, value] of Object.entries(head)) {
|
|
||||||
msg.head[key] = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!topic.isSubscribed()) {
|
if (!topic.isSubscribed()) {
|
||||||
|
|||||||
@@ -116,16 +116,7 @@ class ChatMessage extends React.PureComponent {
|
|||||||
onError={this.props.onError}
|
onError={this.props.onError}
|
||||||
key={i} />);
|
key={i} />);
|
||||||
}, this);
|
}, this);
|
||||||
|
const tree = Drafty.format(content, this.props.formatter, this);
|
||||||
let qte = [];
|
|
||||||
if (content.qte) {
|
|
||||||
qte = Drafty.format(content.qte, this.props.onFormatQuote, this);
|
|
||||||
// Append a line break.
|
|
||||||
const lf = Drafty.appendLineBreak(null);
|
|
||||||
qte = qte.concat(Drafty.format(lf, draftyFormatter, this, qte.length));
|
|
||||||
}
|
|
||||||
let tree = Drafty.format(content, draftyFormatter, this, qte.length);
|
|
||||||
tree = qte.concat(tree);
|
|
||||||
content = React.createElement(React.Fragment, null, tree);
|
content = React.createElement(React.Fragment, null, tree);
|
||||||
} else if (this.props.deleted) {
|
} else if (this.props.deleted) {
|
||||||
// Message represents a range of deleted messages.
|
// Message represents a range of deleted messages.
|
||||||
@@ -191,100 +182,4 @@ class ChatMessage extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert Drafty object to a tree of React elements.
|
|
||||||
import { BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
|
|
||||||
import { fitImageSize } from '../lib/blob-helpers.js';
|
|
||||||
|
|
||||||
// Converts Drafty elements into React classes.
|
|
||||||
// 'this' is set by the caller.
|
|
||||||
function draftyFormatter(style, data, values, key) {
|
|
||||||
if (style == 'EX') {
|
|
||||||
// attachments are handled elsewhere.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let el = Drafty.tagName(style);
|
|
||||||
if (el) {
|
|
||||||
const attr = Drafty.attrValue(style, data) || {};
|
|
||||||
attr.key = key;
|
|
||||||
switch (style) {
|
|
||||||
case 'HL':
|
|
||||||
// Highlighted text. Assign class name.
|
|
||||||
attr.className = 'highlight';
|
|
||||||
break;
|
|
||||||
case 'IM':
|
|
||||||
// Additional processing for images
|
|
||||||
if (data) {
|
|
||||||
attr.className = 'inline-image';
|
|
||||||
const dim = fitImageSize(data.width, data.height,
|
|
||||||
Math.min(this.props.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5), REM_SIZE * 24, false) ||
|
|
||||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
|
||||||
attr.style = {
|
|
||||||
width: dim.dstWidth + 'px',
|
|
||||||
height: dim.dstHeight + 'px',
|
|
||||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
|
||||||
minWidth: dim.dstWidth + 'px',
|
|
||||||
minHeight: dim.dstHeight + 'px'
|
|
||||||
};
|
|
||||||
if (!Drafty.isProcessing(data)) {
|
|
||||||
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
|
|
||||||
attr.alt = data.name;
|
|
||||||
if (attr.src) {
|
|
||||||
attr.onClick = this.handleImagePreview;
|
|
||||||
attr.className += ' image-clickable';
|
|
||||||
attr.loading = 'lazy';
|
|
||||||
} else {
|
|
||||||
attr.src = 'img/broken_image.png';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use custom element instead of <img>.
|
|
||||||
el = UploadingImage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'BN':
|
|
||||||
// Button
|
|
||||||
attr.onClick = this.handleFormButtonClick;
|
|
||||||
let inner = React.Children.map(values, (child) => {
|
|
||||||
return typeof child == 'string' ? child : undefined;
|
|
||||||
});
|
|
||||||
if (!inner || inner.length == 0) {
|
|
||||||
inner = [attr.name]
|
|
||||||
}
|
|
||||||
// Get text which will be sent back when the button is clicked.
|
|
||||||
attr['data-title'] = inner.join('');
|
|
||||||
break;
|
|
||||||
case 'FM':
|
|
||||||
// Form
|
|
||||||
attr.className = 'bot-form';
|
|
||||||
break;
|
|
||||||
case 'RW':
|
|
||||||
// Form element formatting is dependent on element content.
|
|
||||||
break;
|
|
||||||
case 'QQ':
|
|
||||||
// Quote/citation.
|
|
||||||
attr.className = 'reply-quote'
|
|
||||||
attr.onClick = this.handleQuoteClick;
|
|
||||||
break;
|
|
||||||
case 'QH':
|
|
||||||
// Quote header.
|
|
||||||
attr.className = 'reply-quote-header';
|
|
||||||
break;
|
|
||||||
case 'QB':
|
|
||||||
// Quote body.
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (el == '_UNKN') {
|
|
||||||
// Unknown element.
|
|
||||||
// TODO: make it prettier.
|
|
||||||
el = <><span className="material-icons">extension</span></>;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return React.createElement(el, attr, values);
|
|
||||||
} else {
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(ChatMessage);
|
export default injectIntl(ChatMessage);
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export default class DocPreview extends React.PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
<SendMessage
|
<SendMessage
|
||||||
noInput={true}
|
noInput={true}
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
replyTo={this.props.replyTo}
|
||||||
|
formatter={this.props.onFormatQuote}
|
||||||
|
onCancelReply={this.props.onCancelReply}
|
||||||
onSendMessage={this.handleSendDoc}
|
onSendMessage={this.handleSendDoc}
|
||||||
onError={this.props.onError} />
|
onError={this.props.onError} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
<SendMessage
|
<SendMessage
|
||||||
messagePrompt="add_image_caption"
|
messagePrompt="add_image_caption"
|
||||||
acceptBlank={true}
|
acceptBlank={true}
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
replyTo={this.props.replyTo}
|
||||||
|
formatter={this.props.onFormatQuote}
|
||||||
|
onCancelReply={this.props.onCancelReply}
|
||||||
onSendMessage={this.handleSendImage}
|
onSendMessage={this.handleSendImage}
|
||||||
onError={this.props.onError} />
|
onError={this.props.onError} />
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -147,8 +147,6 @@ class SendMessage extends React.PureComponent {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (this.props.replyTo) {
|
if (this.props.replyTo) {
|
||||||
//quote = Drafty.format(this.props.replyTo.content, quoteFormatter, this);
|
|
||||||
//quote = Drafty.format(this.props.replyTo.content, this.props.onFormatQuote, this);
|
|
||||||
const replyToSeq = this.props.replyTo.seq;
|
const replyToSeq = this.props.replyTo.seq;
|
||||||
this.props.onQuoteClick(replyToSeq);
|
this.props.onQuoteClick(replyToSeq);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user