diff --git a/css/base.css b/css/base.css index 1df47a00..e0efe80f 100644 --- a/css/base.css +++ b/css/base.css @@ -2528,3 +2528,16 @@ ul.tabbar li.active > a { .mention { font-weight: bolder; } + +/* Message forwarding. */ + +.forward-menu { + position: absolute; + z-index: 4; + max-height: 80%; + border-radius: 0.2rem; + padding-top: 0.35rem; + padding-bottom: 0.35rem; + box-shadow: 0 1px 6px rgba(0, 0, 0, .45); + background-color: white; +} diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index ca4f66f2..d2b7d22d 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -214,6 +214,17 @@ class MessagesView extends React.Component { }; } else if (nextProps.topic != prevState.topic) { const topic = nextProps.tinode.getTopic(nextProps.topic); + + let reply = null; + if (nextProps.forwardedMessage) { + // We are forwarding a message. Show preview. + const preview = nextProps.forwardedMessage.preview; + reply = { + content: preview, + seq: null + }; + } + nextState = { topic: nextProps.topic, docPreview: null, @@ -222,7 +233,7 @@ class MessagesView extends React.Component { typingIndicator: false, scrollPosition: 0, fetchingMessages: false, - reply: null + reply: reply }; if (topic) { @@ -665,14 +676,20 @@ class MessagesView extends React.Component { // sendMessage sends the message with an optional subscription to topic first. sendMessage(msg, uploadCompletionPromise, uploader) { let head; - if (this.state.reply && this.state.reply.content) { + if (this.props.forwardedMessage) { + // We are forwarding a message. + msg = this.props.forwardedMessage.msg; + head = this.props.forwardedMessage.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); } msg = Drafty.append(this.state.reply.content, msg); - this.setState({reply: null}); + this.handleCancelReply(); } this.props.sendMessage(msg, uploadCompletionPromise, uploader, head); } @@ -825,7 +842,7 @@ class MessagesView extends React.Component { uploader.cancel(); } - handlePickReply(seq, content) { + handlePickReply(seq, content, senderId, senderName) { this.setState({reply: null}); if (!seq || !content) { @@ -843,23 +860,6 @@ class MessagesView extends React.Component { Drafty.wrapInto(this.props.intl.formatMessage(messages.invalid_content), 'EM')); } - // Get the author. - const topic = this.props.tinode.getTopic(this.state.topic); - const msg = topic.findMessage(seq); - let senderName, senderId; - if (msg) { - senderId = msg.from || 'chan'; - if (senderId != this.props.myUserId) { - const topic = this.props.tinode.getTopic(this.state.topic); - const user = topic.userDesc(senderId); - if (user && user.public) { - senderName = user.public.fn; - } - } else { - senderName = this.props.myUserName; - } - } - this.setState({ reply: { content: Drafty.quote(senderName, senderId, content), @@ -869,7 +869,8 @@ class MessagesView extends React.Component { } handleCancelReply() { - this.setState({reply: null}) + this.setState({reply: null}); + this.props.onCancelForwardMessage(); } handleQuoteClick(replyToSeq) { @@ -964,18 +965,14 @@ class MessagesView extends React.Component { const isReply = !(thisFrom == this.props.myUserId); const deliveryStatus = topic.msgStatus(msg, true); - let userName, userAvatar, userFrom; - if (groupTopic) { - const user = topic.userDesc(thisFrom); - if (user && user.public) { - userName = user.public.fn; - userAvatar = makeImageUrl(user.public.photo); - } - userFrom = thisFrom; - chatBoxClass='chat-box group'; - } else { - chatBoxClass='chat-box'; + let userFrom = thisFrom, userName, userAvatar; + const user = topic.userDesc(thisFrom); + if (user && user.public) { + userName = user.public.fn; + userAvatar = makeImageUrl(user.public.photo); } + //userFrom = thisFrom; + chatBoxClass = groupTopic ? 'chat-box group' : 'chat-box'; // Ref for this chat message. const ref = this.getOrCreateMessageRef(msg.seq); @@ -993,6 +990,7 @@ class MessagesView extends React.Component { timestamp={msg.ts} response={isReply} seq={msg.seq} + isGroup={groupTopic} userFrom={userFrom} userName={userName} userAvatar={userAvatar} @@ -1106,11 +1104,12 @@ class MessagesView extends React.Component { : + : + null + } Tinode.MESSAGE_STATUS_FAILED && this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) { menuItems.push('menu_item_reply'); + menuItems.push('menu_item_forward'); } this.props.showContextMenu({ seq: this.props.seq, content: this.props.content, + userFrom: this.props.userFrom, userName: this.props.userName, y: e.pageY, x: e.pageX, pickReply: this.props.pickReply }, menuItems); } @@ -103,7 +105,7 @@ class BaseChatMessage extends React.PureComponent { (this.props.sequence + ' ' + (this.props.response ? 'left' : 'right')); const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble'; const avatar = this.props.deleted ? null : (this.props.userAvatar || true); - const fullDisplay = (this.props.userFrom && this.props.response && !this.props.deleted && + const fullDisplay = (this.props.isGroup && this.props.response && !this.props.deleted && (this.props.sequence == 'single' || this.props.sequence == 'last')); let content = this.props.content; @@ -143,7 +145,7 @@ class BaseChatMessage extends React.PureComponent { return (
  • - {this.props.userFrom && this.props.response ? + {this.props.isGroup && this.props.response ?
    {fullDisplay ? { + return this.forwardMessage(params, errorHandler); + } + }, 'topic_unmute': { id: 'topic_unmute', title: formatMessage(messages.unmute), @@ -428,7 +440,13 @@ class ContextMenu extends React.Component { } replyToMessage(params, errorHandler) { - params.pickReply(params.seq, params.content, errorHandler); + params.pickReply(params.seq, params.content, params.userFrom, params.userName, errorHandler); + } + + forwardMessage(params, errorHandler) { + //params.pickReply({ seq: params.seq, content: params.content }); + //this.props. + console.log('forwarding msg ', params); } render() { diff --git a/src/widgets/forward-menu.jsx b/src/widgets/forward-menu.jsx new file mode 100644 index 00000000..f03eabf9 --- /dev/null +++ b/src/widgets/forward-menu.jsx @@ -0,0 +1,97 @@ +// Forward Menu: message forwarding popup/dropdown menu. +import React from 'react'; + +import Tinode from 'tinode-sdk'; + +import ContactList from './contact-list.jsx'; +import SearchContacts from './search-contacts.jsx'; + +import HashNavigation from '../lib/navigation.js'; +import { REM_SIZE } from '../config.js'; + +export default class ForwardMenu extends React.Component { + constructor(props) { + super(props); + + this.selfRef = React.createRef(); + this.handlePageClick = this.handlePageClick.bind(this); + this.handleEscapeKey = this.handleEscapeKey.bind(this); + + this.handleSearchContacts = this.handleSearchContacts.bind(this); + this.handleContactSelected = this.handleContactSelected.bind(this); + } + + componentDidMount() { + document.addEventListener('mousedown', this.handlePageClick, false); + document.addEventListener('keyup', this.handleEscapeKey, false); + + this.props.onInitFind(); + } + + componentWillUnmount() { + document.removeEventListener('mousedown', this.handlePageClick, false); + document.removeEventListener('keyup', this.handleEscapeKey, false); + } + + handlePageClick(e) { + if (this.selfRef.current.contains(e.target)) { + return; + } + e.preventDefault(); + e.stopPropagation(); + // Not forwarding the message. + this.props.hide(false); + } + + handleEscapeKey(e) { + if (e.keyCode === 27) { + // Not forwarding the message. + this.props.hide(false); + } + } + + handleSearchContacts(query) { + this.props.onSearchContacts(query); + } + + handleContactSelected(uid) { + this.props.onCreateTopic(uid); + this.props.hide(true); + } + + render() { + // Ensure that menu is inside the app-container. + const hSize = 20 * REM_SIZE; + const vSize = REM_SIZE * (0.7 + this.props.contacts.length * 3); + const left = (this.props.bounds.right - this.props.clickAt.x < hSize) ? + (this.props.clickAt.x - this.props.bounds.left - hSize) : + (this.props.clickAt.x - this.props.bounds.left); + const top = (this.props.bounds.bottom - this.props.clickAt.y < vSize) ? + (this.props.clickAt.y - this.props.bounds.top - vSize) : + (this.props.clickAt.y - this.props.bounds.top); + + const style = { + left: left + 'px', + top: top + 'px', + maxWidth: hSize + 'px' + }; + + return ( +
    +
    + + +
    +
    + ); + } +}