Basic and very raw implementation of the Reply To feature.

This commit is contained in:
aforge
2021-08-14 16:30:18 +03:00
parent 47e7632399
commit b482f6649d
9 changed files with 457 additions and 15 deletions
+31 -1
View File
@@ -1361,7 +1361,7 @@ 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 */ /* 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;
} }
@@ -1372,6 +1372,12 @@ img.channel {
font-size: 1.2rem; font-size: 1.2rem;
} }
#send-message-wrapper {
background-color: #eceff1;
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 2;
}
#sendMessage { #sendMessage {
overflow: hidden; overflow: hidden;
overflow-wrap: break-word; overflow-wrap: break-word;
@@ -1509,6 +1515,7 @@ img.channel {
margin: 0; margin: 0;
padding: 0; padding: 0;
align-items: flex-end; align-items: flex-end;
transition: background-color 1s ease-out;
} }
.chat-box .right { .chat-box .right {
@@ -2327,3 +2334,26 @@ ul.tabbar li.active > a {
.cropper .zoom:focus { .cropper .zoom:focus {
outline: none; outline: none;
} }
/* Blue quote bar left of the quoted (cited) text */
.reply-quote {
position: relative;
cursor: pointer;
padding-left: 1em;
border-left: solid #039be5;
line-height: 1.5em;
font-weight: 100;
margin-left: 0.2em;
display: flex;
flex-direction: column;
}
.reply-quote-header {
color: #039be5;
}
.reply-quote-preview {
display: flex;
align-items: center;
}
+3
View File
@@ -78,6 +78,9 @@ export const MAX_IMAGE_DIM = 1024;
// Linear dimensions of image preview: shrink image under this size for previews. // Linear dimensions of image preview: shrink image under this size for previews.
export const IMAGE_PREVIEW_DIM = 64; export const IMAGE_PREVIEW_DIM = 64;
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
export const IMAGE_THUMBNAIL_DIM = 25;
// Maximum number of online users to be shown in a topic title bar. Others will be // Maximum number of online users to be shown in a topic title bar. Others will be
// hidden under "+X more" // hidden under "+X more"
export const MAX_ONLINE_IN_TOPIC = 4; export const MAX_ONLINE_IN_TOPIC = 4;
+7
View File
@@ -760,6 +760,13 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"menu_item_reply": {
"translation": "Reply",
"defaultMessage": "Reply",
"description": "Reply to message",
"missing": false,
"obsolete": false
},
"menu_item_restore_topic": { "menu_item_restore_topic": {
"translation": "Restore", "translation": "Restore",
"defaultMessage": "Restore", "defaultMessage": "Restore",
+24
View File
@@ -244,3 +244,27 @@ export function base64ReEncode(str) {
} }
return str; return str;
} }
// Convert a base64 encoded string with the provided mime type into a Blob.
export function base64ToBlob(str, mime) {
if (!str) {
return null;
}
try {
// Make blob.
const bin = atob(str);
const length = bin.length;
const buf = new ArrayBuffer(length);
const arr = new Uint8Array(buf);
for (let i = 0; i < length; i++) {
arr[i] = bin.charCodeAt(i);
}
return new Blob([buf], { type: mime });
} catch(err) {
console.log("Failed to convert base64 to blob: ", err);
}
return null;
}
+291 -8
View File
@@ -17,11 +17,13 @@ import LogoView from './logo-view.jsx';
import SendMessage from '../widgets/send-message.jsx'; import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE, import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE,
MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js'; MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY,
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64, IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
imageScaled, makeImageUrl } from '../lib/blob-helpers.js'; import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64,
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';
// 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;
@@ -51,7 +53,18 @@ const messages = defineMessages({
id: 'file_attachment_too_large', id: 'file_attachment_too_large',
defaultMessage: 'The file size {size} exceeds the {limit} limit.', defaultMessage: 'The file size {size} exceeds the {limit} limit.',
description: 'Error message when attachment is too large' description: 'Error message when attachment is too large'
} },
drafty_form: {
id: 'drafty_form',
defaultMessage: 'Form: ',
description: 'Comment for form in drafty preview'
},
drafty_attachment: {
id: 'drafty_attachment',
defaultMessage: 'Attachment',
description: 'Comment for attachment in drafty preview'
},
}); });
// Checks if the access permissions are granted but not yet accepted. // Checks if the access permissions are granted but not yet accepted.
@@ -101,6 +114,12 @@ 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.handleSendMessage = this.handleSendMessage.bind(this);
this.handlePickReply = this.handlePickReply.bind(this);
this.handleCancelReply = this.handleCancelReply.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.convertIntoThumbnails = this.convertIntoThumbnails.bind(this);
this.readNotificationQueue = []; this.readNotificationQueue = [];
this.readNotificationTimer = null; this.readNotificationTimer = null;
} }
@@ -224,7 +243,8 @@ class MessagesView extends React.Component {
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false, fetchingMessages: false,
peerMessagingDisabled: false, peerMessagingDisabled: false,
channel: false channel: false,
reply: null
}; };
} else if (nextProps.topic != prevState.topic) { } else if (nextProps.topic != prevState.topic) {
const topic = nextProps.tinode.getTopic(nextProps.topic); const topic = nextProps.tinode.getTopic(nextProps.topic);
@@ -235,7 +255,8 @@ class MessagesView extends React.Component {
imagePostview: null, imagePostview: null,
typingIndicator: false, typingIndicator: false,
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false fetchingMessages: false,
reply: null
}; };
if (topic) { if (topic) {
@@ -784,6 +805,164 @@ class MessagesView extends React.Component {
uploader.cancel(); uploader.cancel();
} }
handlePickReply(m) {
this.setState({reply: null})
//instance.setState({typingIndicator: false});
if (m) {
let cont = m.content
if (cont) {
if (typeof cont == 'string') {
cont = Drafty.init(cont);
}
cont = Drafty.preview(cont, 30, (src, target) => {
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;
}
});
}
});
let header = '';
const seq = m.seq;
const idx = this.msgIndex(seq);
if (idx < this.state.messages.length) {
const m0 = this.state.messages[idx];
let thisFrom = m0.from || 'chan';
if (thisFrom != this.props.myUserId) {
const topic = this.props.tinode.getTopic(this.state.topic);
const user = topic.userDesc(thisFrom);
if (user && user.public) {
header = user.public.fn;
}
} else {
header = this.props.myUserName;
}
}
header = Drafty.init(header);
// Make small image previews.
let ents = [];
if (cont.ent) {
Drafty.entities(cont, (data, idx, tp) => {
if (tp == 'IM') {
ents.push({
tp: tp,
data: data
});
}
}, this);
}
this.convertIntoThumbnails(ents, 0, (success) => {
if (success) {
const msg = Drafty.createQuote(header, cont);
this.setState({reply: {content: msg, seq: m.seq}});
}
});
return;
}
}
}
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.
convertIntoThumbnails(ents, idx, done) {
if (idx >= ents.length) {
done(true);
return;
}
function scale(origBlob) {
imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false,
// Success
(mime, blob, width, height, fname) => {
let ex = ents[idx];
ex.data.mime = mime;
ex.data.val = blob;
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 {
let saveThis = this;
const from = this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref));
fetch(from)
.then(e => e.blob())
.then(function(b) {
scale.call(saveThis, b);
});
return;
}
}
handleSendMessage(msg) {
if (this.state.reply) {
let quote = this.state.reply.content;
if (quote) {
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)
}
handleCancelReply() {
this.setState({reply: null})
}
handleQuoteClick(replyToSeq) {
const element = document.getElementById("msg-" + replyToSeq);
if (element) {
element.scrollIntoView({block: "center", behavior: "smooth"});
element.style.backgroundColor = 'rgb(0, 0, 0, 0.4)';
setTimeout(() => { element.style.backgroundColor = ''; } , 1000);
}
}
render() { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
@@ -898,6 +1077,12 @@ class MessagesView extends React.Component {
onFormResponse={this.handleFormResponse} onFormResponse={this.handleFormResponse}
onError={this.props.onError} onError={this.props.onError}
onCancelUpload={this.handleCancelUpload} onCancelUpload={this.handleCancelUpload}
pickReply={this.handlePickReply}
replyToSeq={msg.head ? msg.head.replyToSeq : null}
onQuoteClick={this.handleQuoteClick}
onFormatQuote={quoteFormatter}
key={msg.seq} /> key={msg.seq} />
); );
} }
@@ -994,11 +1179,18 @@ class MessagesView extends React.Component {
: :
<SendMessage <SendMessage
disabled={!this.state.isWriter} disabled={!this.state.isWriter}
onSendMessage={this.props.sendMessage} //onSendMessage={this.props.sendMessage}
onKeyPress={this.sendKeyPress} onKeyPress={this.sendKeyPress}
onAttachFile={this.handleAttachFile} onAttachFile={this.handleAttachFile}
onAttachImage={this.handleAttachImage} onAttachImage={this.handleAttachImage}
onError={this.props.onError} />} onError={this.props.onError}
tinode={this.props.tinode}
replyTo={this.state.reply}
onQuoteClick={this.handleQuoteClick}
onSendMessage={this.handleSendMessage}
onFormatQuote={quoteFormatter}
onCancelReply={this.handleCancelReply} />}
</> </>
); );
} }
@@ -1009,4 +1201,95 @@ class MessagesView extends React.Component {
} }
}; };
// Converts Drafty object into a one-line reply quote.
function quoteFormatter(style, data, values, key) {
let el = Drafty.tagName(style);
let attr = { key: key };
if (el) {
const { formatMessage } = this.props.intl;
switch (style) {
case 'BR':
// Replace new line with a space.
el = React.Fragment;
values = [' '];
break;
case 'HL':
// Make highlight less prominent in preview.
attr.className = 'highlight preview';
break;
case 'LN':
// Disable links in previews.
el = 'span';
break;
case 'IM':
// We show image thumbnails stored inline.
if (data) {
attr = Drafty.attrValue(style, data) || {};
attr.key = key;
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'
};
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
attr.alt = data.name;
if (!attr.src) {
attr.src = 'img/broken_image.png';
}
}
break;
case 'BN':
el = 'span';
attr.className = 'flat-button faux';
break;
case 'FM':
el = React.Fragment;
values = [<i key="fm" className="material-icons">dashboard</i>,
//messages.drafty_form].concat(values || []);
formatMessage(messages.drafty_form)].concat(values || []);
break;
case 'RW':
el = React.Fragment;
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;
case 'QQ':
// Quote/citation.
attr.className = 'reply-quote';
attr.onClick = this.handleQuoteClick;
break;
case 'QH':
// Quote/citation.
attr.className = 'reply-quote-header';
break;
case 'QB':
// Nothing.
break;
default:
if (el == '_UNKN') {
el = React.Fragment;
values = [<i key="unkn" className="material-icons">extension</i>];
}
break;
}
return React.createElement(el, attr, values);
} else {
return values;
}
};
export default injectIntl(MessagesView); export default injectIntl(MessagesView);
+12 -1
View File
@@ -899,13 +899,22 @@ 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
handleSendMessage(msg, promise, uploader) { handleSendMessage(msg, promise, uploader, head) {
const topic = this.tinode.getTopic(this.state.topicSelected); const topic = this.tinode.getTopic(this.state.topicSelected);
msg = topic.createMessage(msg, false); msg = topic.createMessage(msg, false);
// The uploader is used to show progress. // The uploader is used to show progress.
msg._uploader = uploader; msg._uploader = uploader;
if (head) {
if (!msg.hasOwnProperty('head')) {
msg.head = {};
}
for (const [key, value] of Object.entries(head)) {
msg.head[key] = value;
}
}
if (!topic.isSubscribed()) { if (!topic.isSubscribed()) {
if (!promise) { if (!promise) {
promise = Promise.resolve(); promise = Promise.resolve();
@@ -1627,6 +1636,8 @@ class TinodeWeb extends React.Component {
(this.state.mobilePanel !== 'topic-view' || this.state.infoPanel)} (this.state.mobilePanel !== 'topic-view' || this.state.infoPanel)}
topic={this.state.topicSelected} topic={this.state.topicSelected}
myUserId={this.state.myUserId} myUserId={this.state.myUserId}
// User public.fn.
myUserName={this.state.sidePanelTitle}
serverVersion={this.state.serverVersion} serverVersion={this.state.serverVersion}
serverAddress={this.state.serverAddress} serverAddress={this.state.serverAddress}
applicationVisible={this.state.applicationVisible} applicationVisible={this.state.applicationVisible}
+43 -5
View File
@@ -1,6 +1,6 @@
// Single message, sent or received. // Single message, sent or received.
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk'; import { Drafty } from 'tinode-sdk';
import Attachment from './attachment.jsx'; import Attachment from './attachment.jsx';
@@ -9,7 +9,7 @@ import ReceivedMarker from './received-marker.jsx'
import UploadingImage from './uploading-image.jsx' import UploadingImage from './uploading-image.jsx'
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js'; import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
export default class ChatMessage extends React.Component { class ChatMessage extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -25,6 +25,8 @@ export default class ChatMessage extends React.Component {
this.handleFormButtonClick = this.handleFormButtonClick.bind(this); this.handleFormButtonClick = this.handleFormButtonClick.bind(this);
this.handleContextClick = this.handleContextClick.bind(this); this.handleContextClick = this.handleContextClick.bind(this);
this.handleCancelUpload = this.handleCancelUpload.bind(this); this.handleCancelUpload = this.handleCancelUpload.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
} }
handleImagePreview(e) { handleImagePreview(e) {
@@ -60,8 +62,12 @@ export default class ChatMessage extends React.Component {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const menuItems = this.props.received == Tinode.MESSAGE_STATUS_FAILED ? ['menu_item_send_retry'] : []; const menuItems = this.props.received == Tinode.MESSAGE_STATUS_FAILED ? ['menu_item_send_retry'] : [];
if (this.props.received >= Tinode.MESSAGE_STATUS_FAILED &&
this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) {
menuItems.push('menu_item_reply');
}
this.props.showContextMenu({ seq: this.props.seq, content: this.props.content, this.props.showContextMenu({ seq: this.props.seq, content: this.props.content,
y: e.pageY, x: e.pageX }, menuItems); y: e.pageY, x: e.pageX, pickReply: this.props.pickReply }, menuItems);
} }
handleProgress(ratio) { handleProgress(ratio) {
@@ -72,6 +78,13 @@ export default class ChatMessage extends React.Component {
this.props.onCancelUpload(this.props.seq, this.props.uploader); this.props.onCancelUpload(this.props.seq, this.props.uploader);
} }
handleQuoteClick(e) {
e.preventDefault();
e.stopPropagation();
const replyToSeq = this.props.replyToSeq;
this.props.onQuoteClick(replyToSeq);
}
render() { render() {
const sideClass = this.props.deleted ? 'center' : const sideClass = this.props.deleted ? 'center' :
(this.props.sequence + ' ' + (this.props.response ? 'left' : 'right')); (this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'));
@@ -80,6 +93,7 @@ export default class ChatMessage extends React.Component {
const fullDisplay = (this.props.userFrom && this.props.response && !this.props.deleted && const fullDisplay = (this.props.userFrom && this.props.response && !this.props.deleted &&
(this.props.sequence == 'single' || this.props.sequence == 'last')); (this.props.sequence == 'single' || this.props.sequence == 'last'));
const msgId = 'msg-' + this.props.seq;
let content = this.props.content; let content = this.props.content;
const attachments = []; const attachments = [];
if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) { if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) {
@@ -99,7 +113,17 @@ export default class ChatMessage extends React.Component {
onError={this.props.onError} onError={this.props.onError}
key={i} />); key={i} />);
}, this); }, this);
content = React.createElement(React.Fragment, null, Drafty.format(content, draftyFormatter, 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);
} else if (this.props.deleted) { } else if (this.props.deleted) {
// Message represents a range of deleted messages. // Message represents a range of deleted messages.
content = <><i className="material-icons gray">block</i> <i className="gray"> content = <><i className="material-icons gray">block</i> <i className="gray">
@@ -115,7 +139,7 @@ export default class ChatMessage extends React.Component {
} }
return ( return (
<li className={sideClass}> <li id={msgId} className={sideClass}>
{this.props.userFrom && this.props.response ? {this.props.userFrom && this.props.response ?
<div className="avatar-box"> <div className="avatar-box">
{fullDisplay ? {fullDisplay ?
@@ -234,6 +258,18 @@ function draftyFormatter(style, data, values, key) {
case 'RW': case 'RW':
// Form element formatting is dependent on element content. // Form element formatting is dependent on element content.
break; 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: default:
if (el == '_UNKN') { if (el == '_UNKN') {
// Unknown element. // Unknown element.
@@ -247,3 +283,5 @@ function draftyFormatter(style, data, values, key) {
return values; return values;
} }
}; };
export default injectIntl(ChatMessage);
+17
View File
@@ -45,6 +45,11 @@ const messages = defineMessages({
defaultMessage: 'Unmute', defaultMessage: 'Unmute',
description: 'Turn notifications on' description: 'Turn notifications on'
}, },
reply: {
id: 'menu_item_reply',
defaultMessage: 'Reply',
description: 'Reply to message'
},
topic_delete: { topic_delete: {
id: 'menu_item_delete_topic', id: 'menu_item_delete_topic',
defaultMessage: 'Delete', defaultMessage: 'Delete',
@@ -171,6 +176,13 @@ class ContextMenu extends React.Component {
return this.retryMessage(params, errorHandler); return this.retryMessage(params, errorHandler);
} }
}, },
'menu_item_reply': {
id: 'menu_item_reply',
title: formatMessage(messages.reply),
handler: (params, errorHandler) => {
return this.replyToMessage(params, errorHandler);
}
},
'topic_unmute': { 'topic_unmute': {
id: 'topic_unmute', id: 'topic_unmute',
title: formatMessage(messages.unmute), title: formatMessage(messages.unmute),
@@ -411,6 +423,11 @@ class ContextMenu extends React.Component {
return result; return result;
} }
replyToMessage(params, errorHandler) {
console.log("Reply to message", params);
params.pickReply({ seq: params.seq, content: params.content })
}
render() { render() {
let count = 0; let count = 0;
let menu = []; let menu = [];
+29
View File
@@ -50,6 +50,8 @@ class SendMessage extends React.PureComponent {
this.handleSend = this.handleSend.bind(this); this.handleSend = this.handleSend.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleMessageTyping = this.handleMessageTyping.bind(this); this.handleMessageTyping = this.handleMessageTyping.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -141,6 +143,17 @@ class SendMessage extends React.PureComponent {
this.setState(newState); this.setState(newState);
} }
handleQuoteClick(e) {
e.preventDefault();
e.stopPropagation();
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;
this.props.onQuoteClick(replyToSeq);
}
}
render() { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
const prompt = this.props.disabled ? const prompt = this.props.disabled ?
@@ -148,7 +161,22 @@ class SendMessage extends React.PureComponent {
(this.props.messagePrompt ? (this.props.messagePrompt ?
formatMessage(messages[this.props.messagePrompt]) : formatMessage(messages[this.props.messagePrompt]) :
formatMessage(messages.type_new_message)); formatMessage(messages.type_new_message));
let quote = null;
if (this.props.replyTo) {
quote = Drafty.format(this.props.replyTo.content, this.props.onFormatQuote, this);
}
return ( return (
<div id="send-message-wrapper">
{quote ?
<div className="reply-quote-preview">
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
{quote}
</div>
:
null}
<div id="send-message-panel"> <div id="send-message-panel">
{!this.props.disabled ? {!this.props.disabled ?
<> <>
@@ -182,6 +210,7 @@ class SendMessage extends React.PureComponent {
<div id="writing-disabled">{prompt}</div> <div id="writing-disabled">{prompt}</div>
} }
</div> </div>
</div>
); );
} }
}; };