summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorDavid Meza <dmeza@users.noreply.github.com>2017-07-31 07:24:13 -0500
committerJoram Wilander <jwawilander@gmail.com>2017-07-31 08:24:13 -0400
commitf740698dbe06816921d2a20eea876c9ca7b515ed (patch)
treedf5c9f4f076c6294da8246275a97133f59b5a82f /webapp
parent22fa48f455f15be7a7528501431841a2c7d84c85 (diff)
downloadchat-f740698dbe06816921d2a20eea876c9ca7b515ed.tar.gz
chat-f740698dbe06816921d2a20eea876c9ca7b515ed.tar.bz2
chat-f740698dbe06816921d2a20eea876c9ca7b515ed.zip
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.
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/global_actions.jsx8
-rw-r--r--webapp/components/at_mention/at_mention.jsx11
-rw-r--r--webapp/components/create_comment.jsx2
-rw-r--r--webapp/components/create_post.jsx1
-rw-r--r--webapp/components/post_view/post/post.jsx1
-rw-r--r--webapp/components/post_view/post_body/post_body.jsx1
-rw-r--r--webapp/components/post_view/post_header/post_header.jsx1
-rw-r--r--webapp/components/post_view/post_message_view/post_message_view.jsx24
-rw-r--r--webapp/components/profile_picture.jsx10
-rw-r--r--webapp/components/profile_popover.jsx32
-rw-r--r--webapp/components/rhs_comment.jsx12
-rw-r--r--webapp/components/rhs_root_post.jsx14
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx45
-rw-r--r--webapp/components/textbox.jsx10
-rw-r--r--webapp/components/user_profile.jsx10
-rw-r--r--webapp/stores/search_store.jsx2
-rw-r--r--webapp/stores/suggestion_store.jsx18
-rw-r--r--webapp/utils/channel_intro_messages.jsx2
18 files changed, 183 insertions, 21 deletions
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}
/>
</div>
);
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 <AtMention mentionName={mentionName}/>;
+ return (
+ <AtMention
+ mentionName={mentionName}
+ isRHS={this.props.isRHS}
+ hasMention={this.props.hasMention}
+ />
+ );
}
},
{
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={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>}
+ key='user-popover-fullname'
>
<div
className='overflow--ellipsis text-nowrap padding-bottom'
@@ -247,10 +263,15 @@ export default class ProfilePopover extends React.Component {
dataContent.push(webrtc);
}
+ let title = `@${this.props.user.username}`;
+ if (this.props.hasMention) {
+ title = <a onClick={this.handleMentionKeyClick}>{title}</a>;
+ }
+
return (
<Popover
{...popoverProps}
- title={'@' + this.props.user.username}
+ title={title}
id='user-profile-popover'
>
{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 {
<div className='post__body' >
<div className={postClass}>
{failedPostOptions}
- <PostMessageContainer post={post}/>
+ <PostMessageContainer
+ post={post}
+ isRHS={true}
+ hasMention={true}
+ />
</div>
{fileAttachment}
<ReactionListContainer post={post}/>
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 {
<div className={postClass}>
<PostBodyAdditionalContent
post={post}
- message={<PostMessageContainer post={post}/>}
+ message={
+ <PostMessageContainer
+ post={post}
+ isRHS={true}
+ hasMention={true}
+ />
+ }
previewCollapsed={this.props.previewCollapsed}
/>
</div>
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}
/>
<div
ref='preview'
diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx
index d8549cc7b..d4d900e6a 100644
--- a/webapp/components/user_profile.jsx
+++ b/webapp/components/user_profile.jsx
@@ -82,6 +82,8 @@ export default class UserProfile extends React.Component {
status={this.props.status}
isBusy={this.props.isBusy}
hide={this.hideProfilePopover}
+ isRHS={this.props.isRHS}
+ hasMention={this.props.hasMention}
/>
}
>
@@ -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}
/>
</div>
<div className='channel-intro-profile'>
@@ -121,6 +122,7 @@ export function createDMIntroMessage(channel, centeredIntro) {
<UserProfile
user={teammate}
disablePopover={false}
+ hasMention={true}
/>
</strong>
</div>