From f740698dbe06816921d2a20eea876c9ca7b515ed Mon Sep 17 00:00:00 2001 From: David Meza Date: Mon, 31 Jul 2017 07:24:13 -0500 Subject: PLT-6486 Add an `@username` button to the profile popover, that puts the username in the post when clicked (#6349) * PLT-6486 Add an `@username` button to the profile popover, that puts the username in the post when clicked * PLT-6486 Display `@username` mention on the right text area on center or RHS. * Disable @mentions from profile popover on searches, mentions and pinned posts. Fix js errors. * Control undefined post in SearchStore that causes an exception. --- webapp/actions/global_actions.jsx | 8 ++++ webapp/components/at_mention/at_mention.jsx | 11 +++++- webapp/components/create_comment.jsx | 2 + webapp/components/create_post.jsx | 1 + webapp/components/post_view/post/post.jsx | 1 + .../components/post_view/post_body/post_body.jsx | 1 + .../post_view/post_header/post_header.jsx | 1 + .../post_message_view/post_message_view.jsx | 24 ++++++++++-- webapp/components/profile_picture.jsx | 10 ++++- webapp/components/profile_popover.jsx | 32 ++++++++++++++- webapp/components/rhs_comment.jsx | 12 +++++- webapp/components/rhs_root_post.jsx | 14 ++++++- webapp/components/suggestion/suggestion_box.jsx | 45 +++++++++++++++++++--- webapp/components/textbox.jsx | 10 ++++- webapp/components/user_profile.jsx | 10 ++++- webapp/stores/search_store.jsx | 2 +- webapp/stores/suggestion_store.jsx | 18 +++++++++ webapp/utils/channel_intro_messages.jsx | 2 + 18 files changed, 183 insertions(+), 21 deletions(-) (limited to 'webapp') diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index 2b65beffd..dc5617dde 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -577,3 +577,11 @@ export function postListScrollChange(forceScrollToBottom = false) { value: forceScrollToBottom }); } + +export function emitPopoverMentionKeyClick(isRHS, mentionKey) { + AppDispatcher.handleViewAction({ + type: ActionTypes.POPOVER_MENTION_KEY_CLICK, + isRHS, + mentionKey + }); +} diff --git a/webapp/components/at_mention/at_mention.jsx b/webapp/components/at_mention/at_mention.jsx index 3e2c7bdbc..9bb2d2aad 100644 --- a/webapp/components/at_mention/at_mention.jsx +++ b/webapp/components/at_mention/at_mention.jsx @@ -11,9 +11,16 @@ import {OverlayTrigger} from 'react-bootstrap'; export default class AtMention extends React.PureComponent { static propTypes = { mentionName: PropTypes.string.isRequired, - usersByUsername: PropTypes.object.isRequired + usersByUsername: PropTypes.object.isRequired, + isRHS: PropTypes.bool, + hasMention: PropTypes.bool }; + static defaultProps = { + isRHS: false, + hasMention: false + } + constructor(props) { super(props); @@ -76,6 +83,8 @@ export default class AtMention extends React.PureComponent { user={user} src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)} hide={this.hideProfilePopover} + isRHS={this.props.isRHS} + hasMention={this.props.hasMention} /> } > diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx index a7edb6bc4..9f7592f3e 100644 --- a/webapp/components/create_comment.jsx +++ b/webapp/components/create_comment.jsx @@ -617,6 +617,8 @@ export default class CreateComment extends React.Component { emojiEnabled={window.mm_config.EnableEmojiPicker === 'true'} initialText='' channelId={this.props.channelId} + isRHS={true} + popoverMentionKeyClick={true} id='reply_textbox' ref='textbox' /> diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index 1ad1dda4f..328f182a3 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -774,6 +774,7 @@ export default class CreatePost extends React.Component { emojiEnabled={window.mm_config.EnableEmojiPicker === 'true'} createMessage={Utils.localizeMessage('create_post.write', 'Write a message...')} channelId={this.state.channelId} + popoverMentionKeyClick={true} id='post_textbox' ref='textbox' /> diff --git a/webapp/components/post_view/post/post.jsx b/webapp/components/post_view/post/post.jsx index 4491d888c..25d23c690 100644 --- a/webapp/components/post_view/post/post.jsx +++ b/webapp/components/post_view/post/post.jsx @@ -210,6 +210,7 @@ export default class Post extends React.PureComponent { status={status} user={this.props.user} isBusy={this.props.isBusy} + hasMention={true} /> ); diff --git a/webapp/components/post_view/post_body/post_body.jsx b/webapp/components/post_view/post_body/post_body.jsx index 2f8f86d82..d21192330 100644 --- a/webapp/components/post_view/post_body/post_body.jsx +++ b/webapp/components/post_view/post_body/post_body.jsx @@ -163,6 +163,7 @@ export default class PostBody extends React.PureComponent { lastPostCount={this.props.lastPostCount} post={this.props.post} compactDisplay={this.props.compactDisplay} + hasMention={true} /> ); diff --git a/webapp/components/post_view/post_header/post_header.jsx b/webapp/components/post_view/post_header/post_header.jsx index 562bd2b82..0715f047c 100644 --- a/webapp/components/post_view/post_header/post_header.jsx +++ b/webapp/components/post_view/post_header/post_header.jsx @@ -91,6 +91,7 @@ export default class PostHeader extends React.PureComponent { displayNameType={this.props.displayNameType} status={this.props.status} isBusy={this.props.isBusy} + hasMention={true} /> ); let botIndicator; diff --git a/webapp/components/post_view/post_message_view/post_message_view.jsx b/webapp/components/post_view/post_message_view/post_message_view.jsx index 1c6035df9..348748450 100644 --- a/webapp/components/post_view/post_message_view/post_message_view.jsx +++ b/webapp/components/post_view/post_message_view/post_message_view.jsx @@ -66,12 +66,24 @@ export default class PostMessageView extends React.PureComponent { /** * Set to render post body compactly */ - compactDisplay: PropTypes.bool + compactDisplay: PropTypes.bool, + + /** + * Flags if the post_message_view is for the RHS (Reply). + */ + isRHS: PropTypes.bool, + + /** + * Flags if the post_message_view is for the RHS (Reply). + */ + hasMention: PropTypes.bool }; static defaultProps = { options: {}, - mentionKeys: [] + mentionKeys: [], + isRHS: false, + hasMention: false }; renderDeletedPost() { @@ -116,7 +128,13 @@ export default class PostMessageView extends React.PureComponent { processNode: (node) => { const mentionName = node.attribs[attrib]; - return ; + return ( + + ); } }, { diff --git a/webapp/components/profile_picture.jsx b/webapp/components/profile_picture.jsx index ef1435491..fbaa46127 100644 --- a/webapp/components/profile_picture.jsx +++ b/webapp/components/profile_picture.jsx @@ -62,6 +62,8 @@ export default class ProfilePicture extends React.Component { status={this.props.status} isBusy={this.props.isBusy} hide={this.hideProfilePopover} + isRHS={this.props.isRHS} + hasMention={this.props.hasMention} /> } > @@ -93,7 +95,9 @@ export default class ProfilePicture extends React.Component { ProfilePicture.defaultProps = { width: '36', - height: '36' + height: '36', + isRHS: false, + hasMention: false }; ProfilePicture.propTypes = { src: PropTypes.string.isRequired, @@ -101,5 +105,7 @@ ProfilePicture.propTypes = { width: PropTypes.string, height: PropTypes.string, user: PropTypes.object, - isBusy: PropTypes.bool + isBusy: PropTypes.bool, + isRHS: PropTypes.bool, + hasMention: PropTypes.bool }; diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover.jsx index 3c57f41b6..9e7d7636a 100644 --- a/webapp/components/profile_popover.jsx +++ b/webapp/components/profile_popover.jsx @@ -24,6 +24,7 @@ export default class ProfilePopover extends React.Component { this.initWebrtc = this.initWebrtc.bind(this); this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); + this.handleMentionKeyClick = this.handleMentionKeyClick.bind(this); this.state = { currentUserId: UserStore.getCurrentId(), loadingDMChannel: -1 @@ -103,6 +104,18 @@ export default class ProfilePopover extends React.Component { } } + handleMentionKeyClick(e) { + e.preventDefault(); + + if (!this.props.user) { + return; + } + if (this.props.hide) { + this.props.hide(); + } + GlobalActions.emitPopoverMentionKeyClick(this.props.isRHS, this.props.user.username); + } + render() { const popoverProps = Object.assign({}, this.props); delete popoverProps.user; @@ -110,6 +123,8 @@ export default class ProfilePopover extends React.Component { delete popoverProps.status; delete popoverProps.isBusy; delete popoverProps.hide; + delete popoverProps.isRHS; + delete popoverProps.hasMention; let webrtc; const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; @@ -179,6 +194,7 @@ export default class ProfilePopover extends React.Component { delayShow={Constants.WEBRTC_TIME_DELAY} placement='top' overlay={{fullname}} + key='user-popover-fullname' >
{title}; + } + return ( {dataContent} @@ -259,11 +280,18 @@ export default class ProfilePopover extends React.Component { } } +ProfilePopover.defaultProps = { + isRHS: false, + hasMention: false +}; + ProfilePopover.propTypes = Object.assign({ src: PropTypes.string.isRequired, user: PropTypes.object.isRequired, status: PropTypes.string, isBusy: PropTypes.bool, - hide: PropTypes.func + hide: PropTypes.func, + isRHS: PropTypes.bool, + hasMention: PropTypes.bool }, Popover.propTypes); delete ProfilePopover.propTypes.id; diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 3813fe5a0..25ea4f9ca 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -223,6 +223,8 @@ export default class RhsComment extends React.Component { user={this.props.user} status={status} isBusy={this.props.isBusy} + isRHS={true} + hasMention={true} /> ); @@ -291,6 +293,8 @@ export default class RhsComment extends React.Component { height='36' user={this.props.user} isBusy={this.props.isBusy} + isRHS={true} + hasMention={true} /> ); @@ -327,6 +331,8 @@ export default class RhsComment extends React.Component { status={status} user={this.props.user} isBusy={this.props.isBusy} + isRHS={true} + hasMention={true} /> ); } @@ -447,7 +453,11 @@ export default class RhsComment extends React.Component {
{failedPostOptions} - +
{fileAttachment} diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 1a28dc008..af0c8c0d5 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -256,6 +256,8 @@ export default class RhsRootPost extends React.Component { user={user} status={this.props.status} isBusy={this.props.isBusy} + isRHS={true} + hasMention={true} /> ); let botIndicator; @@ -308,6 +310,8 @@ export default class RhsRootPost extends React.Component { height='36' user={this.props.user} isBusy={this.props.isBusy} + isRHS={true} + hasMention={true} /> ); @@ -344,6 +348,8 @@ export default class RhsRootPost extends React.Component { status={status} user={this.props.user} isBusy={this.props.isBusy} + isRHS={true} + hasMention={true} /> ); } @@ -417,7 +423,13 @@ export default class RhsRootPost extends React.Component {
} + message={ + + } previewCollapsed={this.props.previewCollapsed} />
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx index 93e8b2583..e9dc698aa 100644 --- a/webapp/components/suggestion/suggestion_box.jsx +++ b/webapp/components/suggestion/suggestion_box.jsx @@ -70,14 +70,25 @@ export default class SuggestionBox extends React.Component { /** * Function called when an item is selected */ - onItemSelected: PropTypes.func + onItemSelected: PropTypes.func, + + /** + * Flags if the suggestion_box is for the RHS (Reply). + */ + isRHS: PropTypes.bool, + + /** + * Function called when @mention is clicked + */ + popoverMentionKeyClick: PropTypes.bool } static defaultProps = { type: 'input', listStyle: 'top', renderDividers: false, - completeOnTab: true + completeOnTab: true, + isRHS: false } constructor(props) { @@ -85,6 +96,7 @@ export default class SuggestionBox extends React.Component { this.handleBlur = this.handleBlur.bind(this); + this.handlePopoverMentionKeyClick = this.handlePopoverMentionKeyClick.bind(this); this.handleCompleteWord = this.handleCompleteWord.bind(this); this.handleChange = this.handleChange.bind(this); this.handleCompositionStart = this.handleCompositionStart.bind(this); @@ -102,10 +114,16 @@ export default class SuggestionBox extends React.Component { } componentDidMount() { + if (this.props.popoverMentionKeyClick) { + SuggestionStore.addPopoverMentionKeyClickListener(this.props.isRHS, this.handlePopoverMentionKeyClick); + } SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged); } componentWillUnmount() { + if (this.props.popoverMentionKeyClick) { + SuggestionStore.removePopoverMentionKeyClickListener(this.props.isRHS, this.handlePopoverMentionKeyClick); + } SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged); SuggestionStore.unregisterSuggestionBox(this.suggestionId); @@ -121,7 +139,8 @@ export default class SuggestionBox extends React.Component { getTextbox() { if (this.props.type === 'textarea') { - return this.refs.textbox.getDOMNode(); + const node = this.refs.textbox.getDOMNode(); + return node; } return this.refs.textbox; @@ -179,7 +198,18 @@ export default class SuggestionBox extends React.Component { this.composing = false; } - handleCompleteWord(term, matchedPretext) { + handlePopoverMentionKeyClick(mentionKey) { + let insertText = '@' + mentionKey; + + // if the current text does not end with a whitespace, then insert a space + if (this.refs.textbox.value && (/[^\s]$/).test(this.refs.textbox.value)) { + insertText = ' ' + insertText; + } + + this.handleCompleteWord(insertText, '', false); + } + + handleCompleteWord(term, matchedPretext, shouldEmitWordSuggestion = true) { const textbox = this.getTextbox(); const caret = textbox.selectionEnd; const text = this.props.value; @@ -232,8 +262,9 @@ export default class SuggestionBox extends React.Component { provider.handleCompleteWord(term, matchedPretext); } } - - GlobalActions.emitCompleteWordSuggestion(this.suggestionId); + if (shouldEmitWordSuggestion) { + GlobalActions.emitCompleteWordSuggestion(this.suggestionId); + } } handleKeyDown(e) { @@ -288,6 +319,8 @@ export default class SuggestionBox extends React.Component { Reflect.deleteProperty(props, 'onChange'); // We use onInput instead of onChange on the actual input Reflect.deleteProperty(props, 'onItemSelected'); Reflect.deleteProperty(props, 'completeOnTab'); + Reflect.deleteProperty(props, 'isRHS'); + Reflect.deleteProperty(props, 'popoverMentionKeyClick'); const childProps = { ref: 'textbox', diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx index 536b1a115..3db87d106 100644 --- a/webapp/components/textbox.jsx +++ b/webapp/components/textbox.jsx @@ -36,11 +36,15 @@ export default class Textbox extends React.Component { supportsCommands: PropTypes.bool.isRequired, handlePostError: PropTypes.func, suggestionListStyle: PropTypes.string, - emojiEnabled: PropTypes.bool + emojiEnabled: PropTypes.bool, + isRHS: PropTypes.bool, + popoverMentionKeyClick: React.PropTypes.bool }; static defaultProps = { - supportsCommands: true + supportsCommands: true, + isRHS: false, + popoverMentionKeyClick: false }; constructor(props) { @@ -296,6 +300,8 @@ export default class Textbox extends React.Component { channelId={this.props.channelId} value={this.props.value} renderDividers={true} + isRHS={this.props.isRHS} + popoverMentionKeyClick={this.props.popoverMentionKeyClick} />
} > @@ -99,7 +101,9 @@ UserProfile.defaultProps = { user: {}, overwriteName: '', overwriteImage: '', - disablePopover: false + disablePopover: false, + isRHS: false, + hasMention: false }; UserProfile.propTypes = { user: PropTypes.object, @@ -108,5 +112,7 @@ UserProfile.propTypes = { disablePopover: PropTypes.bool, displayNameType: PropTypes.string, status: PropTypes.string, - isBusy: PropTypes.bool + isBusy: PropTypes.bool, + isRHS: PropTypes.bool, + hasMention: PropTypes.bool }; diff --git a/webapp/stores/search_store.jsx b/webapp/stores/search_store.jsx index 5dfea6867..d57c630cb 100644 --- a/webapp/stores/search_store.jsx +++ b/webapp/stores/search_store.jsx @@ -122,7 +122,7 @@ class SearchStoreClass extends EventEmitter { updatePost(post) { const results = this.getSearchResults(); - if (results == null) { + if (!post || results == null) { return; } diff --git a/webapp/stores/suggestion_store.jsx b/webapp/stores/suggestion_store.jsx index d1f5a64f6..902886ed7 100644 --- a/webapp/stores/suggestion_store.jsx +++ b/webapp/stores/suggestion_store.jsx @@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes; const COMPLETE_WORD_EVENT = 'complete_word'; const PRETEXT_CHANGED_EVENT = 'pretext_changed'; const SUGGESTIONS_CHANGED_EVENT = 'suggestions_changed'; +const POPOVER_MENTION_KEY_CLICK_EVENT = 'popover_mention_key_click'; class SuggestionStore extends EventEmitter { constructor() { @@ -27,6 +28,10 @@ class SuggestionStore extends EventEmitter { this.removeCompleteWordListener = this.removeCompleteWordListener.bind(this); this.emitCompleteWord = this.emitCompleteWord.bind(this); + this.addPopoverMentionKeyClickListener = this.addPopoverMentionKeyClickListener.bind(this); + this.removePopoverMentionKeyClickListener = this.removePopoverMentionKeyClickListener.bind(this); + this.emitPopoverMentionKeyClick = this.emitPopoverMentionKeyClick.bind(this); + this.handleEventPayload = this.handleEventPayload.bind(this); this.dispatchToken = AppDispatcher.register(this.handleEventPayload); @@ -71,6 +76,16 @@ class SuggestionStore extends EventEmitter { this.emit(COMPLETE_WORD_EVENT + id, term, matchedPretext); } + addPopoverMentionKeyClickListener(id, callback) { + this.on(POPOVER_MENTION_KEY_CLICK_EVENT + id, callback); + } + removePopoverMentionKeyClickListener(id, callback) { + this.removeListener(POPOVER_MENTION_KEY_CLICK_EVENT + id, callback); + } + emitPopoverMentionKeyClick(isRHS, mentionKey) { + this.emit(POPOVER_MENTION_KEY_CLICK_EVENT + isRHS, mentionKey); + } + registerSuggestionBox(id) { this.suggestions.set(id, { pretext: '', @@ -304,6 +319,9 @@ class SuggestionStore extends EventEmitter { this.completeWord(id, other.term, other.matchedPretext); } break; + case ActionTypes.POPOVER_MENTION_KEY_CLICK: + this.emitPopoverMentionKeyClick(other.isRHS, other.mentionKey); + break; } } } diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index 6390f615c..baf6c4fb1 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -114,6 +114,7 @@ export function createDMIntroMessage(channel, centeredIntro) { width='50' height='50' user={teammate} + hasMention={true} />
@@ -121,6 +122,7 @@ export function createDMIntroMessage(channel, centeredIntro) {
-- cgit v1.2.3-1-g7c22