editing once works, editing twice does not

This commit is contained in:
or-else
2022-11-19 16:21:24 -08:00
parent a99ad81196
commit d32c1636ce
24 changed files with 489 additions and 79 deletions
+2
View File
@@ -97,6 +97,8 @@ export const MESSAGE_PREVIEW_LENGTH = 80;
export const QUOTED_REPLY_LENGTH = 30;
// Length of a preview of a forwarded message.
export const FORWARDED_PREVIEW_LENGTH = 84;
// Length of preview for a message being edited.
export const EDIT_PREVIEW_LENGTH = 60;
// Minimum duration of an audio recording in milliseconds (2 sec).
export const MIN_DURATION = 2_000;
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Bearbeiten",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Bearbeitung",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", bearbeitet",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Edit",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Editing",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", edited",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Editar",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Edición",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", editado",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+12
View File
@@ -243,6 +243,10 @@
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty"
},
"editing_message": {
"defaultMessage": "Editing",
"description": "Title over message editing preview"
},
"email_dative": {
"defaultMessage": "email",
"description": "Dative case of 'email', i.e. 'email' in 'by email'"
@@ -480,6 +484,10 @@
"defaultMessage": "Delete",
"description": "Delete entire topic"
},
"menu_item_edit": {
"defaultMessage": "Edit",
"description": "Edit message"
},
"menu_item_edit_permissions": {
"defaultMessage": "Edit permissions",
"description": "Menu item [Edit permissions]"
@@ -524,6 +532,10 @@
"defaultMessage": "Video call",
"description": "Start video call"
},
"message_edited_marker": {
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited"
},
"message_sending": {
"defaultMessage": "sending...",
"description": "Message being sent, in place of time stamp"
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Modifier",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Édition",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", édité",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "편집",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "교정하다",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": " 수정됨",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Editează",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Editare",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", editat",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Изменить",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Редактирование",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", изменено",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "編輯",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "編輯",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ",編輯",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -1451,5 +1451,26 @@
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "编辑",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "编辑",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ",编辑",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+61 -17
View File
@@ -19,7 +19,7 @@ import LogoView from './logo-view.jsx';
import MetaMessage from '../widgets/meta-message.jsx';
import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
@@ -63,6 +63,11 @@ const messages = defineMessages({
defaultMessage: 'invalid content',
description: 'Shown when the message is unreadable'
},
editing_message: {
id: 'editing_message',
defaultMessage: 'Editing',
description: 'Title over message editing preview'
}
});
// Checks if the access permissions are granted but not yet accepted.
@@ -125,6 +130,7 @@ class MessagesView extends React.Component {
this.goToLatestMessage = this.goToLatestMessage.bind(this);
this.handlePickReply = this.handlePickReply.bind(this);
this.handleEditMessage = this.handleEditMessage.bind(this);
this.handleCancelReply = this.handleCancelReply.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.handleCallHangup = this.handleCallHangup.bind(this);
@@ -237,6 +243,7 @@ class MessagesView extends React.Component {
peerMessagingDisabled: false,
channel: false,
reply: null,
contentToEdit: null,
showGoToLastButton: false
};
} else if (nextProps.topic != prevState.topic) {
@@ -753,14 +760,19 @@ class MessagesView extends React.Component {
msg = this.props.forwardMessage.msg;
head = this.props.forwardMessage.head;
this.handleCancelReply();
} else if (this.state.reply && this.state.reply.content) {
// We are replying to a message in this topic.
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);
} else if (this.state.reply) {
if (this.state.reply.editing) {
// Editing an existing message.
head = {replace: ':' + this.state.reply.seq};
} else if (this.state.reply.content) {
// Replying to a message in this topic.
// Turn it into Drafty so we can make a quoted Drafty object later.
head = {reply: '' + this.state.reply.seq};
if (typeof msg == 'string') {
msg = Drafty.parse(msg);
}
msg = Drafty.append(Drafty.sanitizeEntities(this.state.reply.content), msg);
}
msg = Drafty.append(Drafty.sanitizeEntities(this.state.reply.content), msg);
this.handleCancelReply();
}
this.props.sendMessage(msg, uploadCompletionPromise, uploader, head);
@@ -769,7 +781,7 @@ class MessagesView extends React.Component {
// Retry sending a message.
retrySend(pub) {
this.props.sendMessage(pub.content, undefined, undefined, pub.head)
.then(() => {
.then(_ => {
// All good. Remove the original message draft from the cache.
const topic = this.props.tinode.getTopic(this.state.topic);
topic.delMessagesList([pub.seq], true);
@@ -977,9 +989,8 @@ class MessagesView extends React.Component {
// senderId: UID of the sender of the source message.
// senderName: full name of the sender of the original message.
handlePickReply(seq, content, senderId, senderName) {
this.setState({reply: null});
if (!seq || !content) {
this.setState({reply: null});
return;
}
@@ -1001,8 +1012,38 @@ class MessagesView extends React.Component {
this.props.onCancelForwardMessage();
}
// seq: seq ID of the message to edit.
// context: message content.
handleEditMessage(seq, content) {
if (!seq || !content) {
this.setState({reply: null});
return;
}
content = typeof content == 'string' ? Drafty.init(content) : content;
const editable = Drafty.toMarkdown(content);
if (Drafty.isValid(content)) {
content = Drafty.replyContent(content, EDIT_PREVIEW_LENGTH);
} else {
// /!\ invalid content.
content = Drafty.append(Drafty.init('\u26A0 '),
Drafty.wrapInto(this.props.intl.formatMessage(messages.invalid_content), 'EM'));
}
this.setState({
reply: {
content: Drafty.quote(this.props.intl.formatMessage(messages.message_editing), null, content),
seq: seq,
editing: true
},
contentToEdit: editable
});
this.props.onCancelForwardMessage();
}
handleCancelReply() {
this.setState({reply: null});
this.setState({reply: null, contentToEdit: null});
this.props.onCancelForwardMessage();
}
@@ -1011,7 +1052,7 @@ class MessagesView extends React.Component {
if (ref && ref.current) {
ref.current.scrollIntoView({block: "center", behavior: "smooth"});
ref.current.classList.add('flash');
setTimeout(() => { ref.current.classList.remove('flash') } , 1000);
setTimeout(_ => {ref.current.classList.remove('flash')} , 1000);
} else {
console.error("Unresolved message ref", replyToSeq);
}
@@ -1157,6 +1198,8 @@ class MessagesView extends React.Component {
tinode={this.props.tinode}
content={msg.content}
mimeType={msg.head && msg.head.mime}
replyToSeq={replyToSeq}
edited={msg.head && msg.head.replace && !msg.head.webrtc}
timestamp={msg.ts}
response={isReply}
seq={msg.seq}
@@ -1168,17 +1211,17 @@ class MessagesView extends React.Component {
sequence={sequence}
received={deliveryStatus}
uploader={msg._uploader}
userIsWriter={this.state.isWriter}
viewportWidth={this.props.viewportWidth} // Used by `formatter`.
showContextMenu={this.handleShowMessageContextMenu}
onImagePreview={this.handleImagePostview}
onFormResponse={this.handleFormResponse}
onError={this.props.onError}
onCancelUpload={this.handleCancelUpload}
pickReply={this.handlePickReply}
replyToSeq={replyToSeq}
editMessage={this.handleEditMessage}
onQuoteClick={this.handleQuoteClick}
onError={this.props.onError}
ref={ref}
userIsWriter={this.state.isWriter}
key={msg.seq} />
);
}
@@ -1287,13 +1330,14 @@ class MessagesView extends React.Component {
topicName={this.state.topic}
noInput={!!this.props.forwardMessage}
disabled={!this.state.isWriter || this.state.deleted}
reply={this.state.reply}
initMessage={this.state.contentToEdit}
onKeyPress={this.sendKeyPress}
onSendMessage={this.sendMessage}
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImage}
onAttachAudio={this.props.forwardMessage ? null : this.sendAudioAttachment}
onError={this.props.onError}
reply={this.state.reply}
onQuoteClick={this.handleQuoteClick}
onCancelReply={this.handleCancelReply} />}
</>
+6 -1
View File
@@ -80,6 +80,9 @@ class BaseChatMessage extends React.PureComponent {
this.props.received > Tinode.MESSAGE_STATUS_FAILED &&
this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) {
menuItems.push('menu_item_reply');
if (!this.props.response) {
menuItems.push('menu_item_edit');
}
}
menuItems.push('menu_item_forward');
@@ -90,7 +93,8 @@ class BaseChatMessage extends React.PureComponent {
userName: this.props.userName,
y: e.pageY,
x: e.pageX,
pickReply: this.props.pickReply
pickReply: this.props.pickReply,
editMessage: this.props.editMessage,
}, menuItems);
}
@@ -175,6 +179,7 @@ class BaseChatMessage extends React.PureComponent {
</div>
{this.props.timestamp ?
<ReceivedMarker
edited={this.props.edited}
timestamp={this.props.timestamp}
received={this.props.received} />
: null}
+16
View File
@@ -55,6 +55,11 @@ const messages = defineMessages({
defaultMessage: 'Forward',
description: 'Forward message'
},
edit: {
id: 'menu_item_edit',
defaultMessage: 'Edit',
description: 'Edit message'
},
topic_delete: {
id: 'menu_item_delete_topic',
defaultMessage: 'Delete',
@@ -193,6 +198,13 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.forward),
handler: () => {} /* the action is taken directly in tinode-web */
},
'menu_item_edit': {
id: 'menu_item_edit',
title: formatMessage(messages.edit),
handler: (params, errorHandler) => {
return this.editMessage(params, errorHandler);
}
},
'topic_unmute': {
id: 'topic_unmute',
title: formatMessage(messages.unmute),
@@ -437,6 +449,10 @@ class ContextMenu extends React.Component {
params.pickReply(params.seq, params.content, params.userFrom, params.userName, errorHandler);
}
editMessage(params, errorHandler) {
params.editMessage(params.seq, params.content, errorHandler);
}
render() {
const menu = [];
let count = 0;
+7 -1
View File
@@ -16,6 +16,11 @@ const messages = defineMessages({
id: 'message_sending_failed',
defaultMessage: 'failed',
description: 'Failed to send message, in place of time stamp'
},
message_edited_marker: {
id: 'message_edited_marker',
defaultMessage: ', edited',
description: 'Marker indicating that the message was edited'
}
});
@@ -33,10 +38,11 @@ class ReceivedMarker extends React.PureComponent {
const icon = deliveryMarker(this.props.received);
const marker = icon ? <i className={'material-icons small ' + icon.color}>{icon.name}</i> : null;
const edited = this.props.edited ? formatMessage(messages.message_edited_marker) : null;
return (
<span className="timestamp">
{timestamp}{'\u00a0'}{marker}
{timestamp}{edited}{'\u00a0'}{marker}
</span>
);
}
+19 -8
View File
@@ -83,7 +83,14 @@ class SendMessage extends React.PureComponent {
}
if (prevProps.topicName != this.props.topicName) {
this.setState({message: '', audioRec: false, quote: null});
this.setState({message: this.props.initMessage || '', audioRec: false, quote: null});
} else if (prevProps.initMessage != this.props.initMessage) {
const msg = this.props.initMessage || '';
this.setState({message: msg}, _ => {
// If there is text, scroll to bottom and set caret to the end.
this.messageEditArea.scrollTop = this.messageEditArea.scrollHeight;
this.messageEditArea.setSelectionRange(msg.length, msg.length);
});
}
if (prevProps.reply != this.props.reply) {
this.setState({quote: this.formatReply()});
@@ -193,10 +200,13 @@ class SendMessage extends React.PureComponent {
formatMessage(messages[this.props.messagePrompt]) :
formatMessage(messages.type_new_message));
const sendIcon = (this.props.reply && this.props.reply.editing) ?
'check_circle' : 'send';
const quote = this.state.quote ?
(<div id="reply-quote-preview">
<div className="cancel">
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
<a href="#" onClick={e => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
</div>
{this.state.quote}
</div>) : null;
@@ -209,10 +219,10 @@ class SendMessage extends React.PureComponent {
<>
{this.props.onAttachFile && !this.state.audioRec ?
<>
<a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image">
<a href="#" onClick={e => {e.preventDefault(); this.attachImage.click();}} title="Add image">
<i className="material-icons secondary">photo</i>
</a>
<a href="#" onClick={(e) => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
<a href="#" onClick={e => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
<i className="material-icons secondary">attach_file</i>
</a>
</>
@@ -225,13 +235,14 @@ class SendMessage extends React.PureComponent {
onDeleted={_ => this.setState({audioRec: false})}
onFinished={this.handleAttachAudio}/> :
<textarea id="sendMessage" placeholder={prompt}
value={this.state.message} onChange={this.handleMessageTyping}
value={this.state.message}
onChange={this.handleMessageTyping}
onKeyPress={this.handleKeyPress}
ref={(ref) => {this.messageEditArea = ref;}}
autoFocus />)}
{this.state.message || !audioEnabled ?
<a href="#" onClick={this.handleSend} title="Send">
<i className="material-icons">send</i>
<i className="material-icons">{sendIcon}</i>
</a> :
!this.state.audioRec ?
<a href="#" onClick={e => {e.preventDefault(); this.setState({audioRec: true})}} title="Voice">
@@ -239,9 +250,9 @@ class SendMessage extends React.PureComponent {
</a> :
null
}
<input type="file" ref={(ref) => {this.attachFile = ref;}}
<input type="file" ref={ref => {this.attachFile = ref}}
onChange={this.handleAttachFile} style={{display: 'none'}} />
<input type="file" ref={(ref) => {this.attachImage = ref;}} accept="image/*"
<input type="file" ref={ref => {this.attachImage = ref}} accept="image/*"
onChange={this.handleAttachImage} style={{display: 'none'}} />
</>
: