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.
Этот коммит содержится в:
David Meza
2017-07-31 07:24:13 -05:00
коммит произвёл Joram Wilander
родитель 22fa48f455
Коммит f740698dbe
18 изменённых файлов: 183 добавлений и 21 удалений

Просмотреть файл

@@ -577,3 +577,11 @@ export function postListScrollChange(forceScrollToBottom = false) {
value: forceScrollToBottom value: forceScrollToBottom
}); });
} }
export function emitPopoverMentionKeyClick(isRHS, mentionKey) {
AppDispatcher.handleViewAction({
type: ActionTypes.POPOVER_MENTION_KEY_CLICK,
isRHS,
mentionKey
});
}

Просмотреть файл

@@ -11,9 +11,16 @@ import {OverlayTrigger} from 'react-bootstrap';
export default class AtMention extends React.PureComponent { export default class AtMention extends React.PureComponent {
static propTypes = { static propTypes = {
mentionName: PropTypes.string.isRequired, 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) { constructor(props) {
super(props); super(props);
@@ -76,6 +83,8 @@ export default class AtMention extends React.PureComponent {
user={user} user={user}
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)} src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
hide={this.hideProfilePopover} hide={this.hideProfilePopover}
isRHS={this.props.isRHS}
hasMention={this.props.hasMention}
/> />
} }
> >

Просмотреть файл

@@ -617,6 +617,8 @@ export default class CreateComment extends React.Component {
emojiEnabled={window.mm_config.EnableEmojiPicker === 'true'} emojiEnabled={window.mm_config.EnableEmojiPicker === 'true'}
initialText='' initialText=''
channelId={this.props.channelId} channelId={this.props.channelId}
isRHS={true}
popoverMentionKeyClick={true}
id='reply_textbox' id='reply_textbox'
ref='textbox' ref='textbox'
/> />

Просмотреть файл

@@ -774,6 +774,7 @@ export default class CreatePost extends React.Component {
emojiEnabled={window.mm_config.EnableEmojiPicker === 'true'} emojiEnabled={window.mm_config.EnableEmojiPicker === 'true'}
createMessage={Utils.localizeMessage('create_post.write', 'Write a message...')} createMessage={Utils.localizeMessage('create_post.write', 'Write a message...')}
channelId={this.state.channelId} channelId={this.state.channelId}
popoverMentionKeyClick={true}
id='post_textbox' id='post_textbox'
ref='textbox' ref='textbox'
/> />

Просмотреть файл

@@ -210,6 +210,7 @@ export default class Post extends React.PureComponent {
status={status} status={status}
user={this.props.user} user={this.props.user}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
hasMention={true}
/> />
); );

Просмотреть файл

@@ -163,6 +163,7 @@ export default class PostBody extends React.PureComponent {
lastPostCount={this.props.lastPostCount} lastPostCount={this.props.lastPostCount}
post={this.props.post} post={this.props.post}
compactDisplay={this.props.compactDisplay} compactDisplay={this.props.compactDisplay}
hasMention={true}
/> />
</div> </div>
); );

Просмотреть файл

@@ -91,6 +91,7 @@ export default class PostHeader extends React.PureComponent {
displayNameType={this.props.displayNameType} displayNameType={this.props.displayNameType}
status={this.props.status} status={this.props.status}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
hasMention={true}
/> />
); );
let botIndicator; let botIndicator;

Просмотреть файл

@@ -66,12 +66,24 @@ export default class PostMessageView extends React.PureComponent {
/** /**
* Set to render post body compactly * 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 = { static defaultProps = {
options: {}, options: {},
mentionKeys: [] mentionKeys: [],
isRHS: false,
hasMention: false
}; };
renderDeletedPost() { renderDeletedPost() {
@@ -116,7 +128,13 @@ export default class PostMessageView extends React.PureComponent {
processNode: (node) => { processNode: (node) => {
const mentionName = node.attribs[attrib]; const mentionName = node.attribs[attrib];
return <AtMention mentionName={mentionName}/>; return (
<AtMention
mentionName={mentionName}
isRHS={this.props.isRHS}
hasMention={this.props.hasMention}
/>
);
} }
}, },
{ {

Просмотреть файл

@@ -62,6 +62,8 @@ export default class ProfilePicture extends React.Component {
status={this.props.status} status={this.props.status}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
hide={this.hideProfilePopover} hide={this.hideProfilePopover}
isRHS={this.props.isRHS}
hasMention={this.props.hasMention}
/> />
} }
> >
@@ -93,7 +95,9 @@ export default class ProfilePicture extends React.Component {
ProfilePicture.defaultProps = { ProfilePicture.defaultProps = {
width: '36', width: '36',
height: '36' height: '36',
isRHS: false,
hasMention: false
}; };
ProfilePicture.propTypes = { ProfilePicture.propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
@@ -101,5 +105,7 @@ ProfilePicture.propTypes = {
width: PropTypes.string, width: PropTypes.string,
height: PropTypes.string, height: PropTypes.string,
user: PropTypes.object, user: PropTypes.object,
isBusy: PropTypes.bool isBusy: PropTypes.bool,
isRHS: PropTypes.bool,
hasMention: PropTypes.bool
}; };

Просмотреть файл

@@ -24,6 +24,7 @@ export default class ProfilePopover extends React.Component {
this.initWebrtc = this.initWebrtc.bind(this); this.initWebrtc = this.initWebrtc.bind(this);
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.handleMentionKeyClick = this.handleMentionKeyClick.bind(this);
this.state = { this.state = {
currentUserId: UserStore.getCurrentId(), currentUserId: UserStore.getCurrentId(),
loadingDMChannel: -1 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() { render() {
const popoverProps = Object.assign({}, this.props); const popoverProps = Object.assign({}, this.props);
delete popoverProps.user; delete popoverProps.user;
@@ -110,6 +123,8 @@ export default class ProfilePopover extends React.Component {
delete popoverProps.status; delete popoverProps.status;
delete popoverProps.isBusy; delete popoverProps.isBusy;
delete popoverProps.hide; delete popoverProps.hide;
delete popoverProps.isRHS;
delete popoverProps.hasMention;
let webrtc; let webrtc;
const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
@@ -179,6 +194,7 @@ export default class ProfilePopover extends React.Component {
delayShow={Constants.WEBRTC_TIME_DELAY} delayShow={Constants.WEBRTC_TIME_DELAY}
placement='top' placement='top'
overlay={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>} overlay={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>}
key='user-popover-fullname'
> >
<div <div
className='overflow--ellipsis text-nowrap padding-bottom' className='overflow--ellipsis text-nowrap padding-bottom'
@@ -247,10 +263,15 @@ export default class ProfilePopover extends React.Component {
dataContent.push(webrtc); dataContent.push(webrtc);
} }
let title = `@${this.props.user.username}`;
if (this.props.hasMention) {
title = <a onClick={this.handleMentionKeyClick}>{title}</a>;
}
return ( return (
<Popover <Popover
{...popoverProps} {...popoverProps}
title={'@' + this.props.user.username} title={title}
id='user-profile-popover' id='user-profile-popover'
> >
{dataContent} {dataContent}
@@ -259,11 +280,18 @@ export default class ProfilePopover extends React.Component {
} }
} }
ProfilePopover.defaultProps = {
isRHS: false,
hasMention: false
};
ProfilePopover.propTypes = Object.assign({ ProfilePopover.propTypes = Object.assign({
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
user: PropTypes.object.isRequired, user: PropTypes.object.isRequired,
status: PropTypes.string, status: PropTypes.string,
isBusy: PropTypes.bool, isBusy: PropTypes.bool,
hide: PropTypes.func hide: PropTypes.func,
isRHS: PropTypes.bool,
hasMention: PropTypes.bool
}, Popover.propTypes); }, Popover.propTypes);
delete ProfilePopover.propTypes.id; delete ProfilePopover.propTypes.id;

Просмотреть файл

@@ -223,6 +223,8 @@ export default class RhsComment extends React.Component {
user={this.props.user} user={this.props.user}
status={status} status={status}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/> />
); );
@@ -291,6 +293,8 @@ export default class RhsComment extends React.Component {
height='36' height='36'
user={this.props.user} user={this.props.user}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/> />
); );
@@ -327,6 +331,8 @@ export default class RhsComment extends React.Component {
status={status} status={status}
user={this.props.user} user={this.props.user}
isBusy={this.props.isBusy} 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='post__body' >
<div className={postClass}> <div className={postClass}>
{failedPostOptions} {failedPostOptions}
<PostMessageContainer post={post}/> <PostMessageContainer
post={post}
isRHS={true}
hasMention={true}
/>
</div> </div>
{fileAttachment} {fileAttachment}
<ReactionListContainer post={post}/> <ReactionListContainer post={post}/>

Просмотреть файл

@@ -256,6 +256,8 @@ export default class RhsRootPost extends React.Component {
user={user} user={user}
status={this.props.status} status={this.props.status}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/> />
); );
let botIndicator; let botIndicator;
@@ -308,6 +310,8 @@ export default class RhsRootPost extends React.Component {
height='36' height='36'
user={this.props.user} user={this.props.user}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/> />
); );
@@ -344,6 +348,8 @@ export default class RhsRootPost extends React.Component {
status={status} status={status}
user={this.props.user} user={this.props.user}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
isRHS={true}
hasMention={true}
/> />
); );
} }
@@ -417,7 +423,13 @@ export default class RhsRootPost extends React.Component {
<div className={postClass}> <div className={postClass}>
<PostBodyAdditionalContent <PostBodyAdditionalContent
post={post} post={post}
message={<PostMessageContainer post={post}/>} message={
<PostMessageContainer
post={post}
isRHS={true}
hasMention={true}
/>
}
previewCollapsed={this.props.previewCollapsed} previewCollapsed={this.props.previewCollapsed}
/> />
</div> </div>

Просмотреть файл

@@ -70,14 +70,25 @@ export default class SuggestionBox extends React.Component {
/** /**
* Function called when an item is selected * 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 = { static defaultProps = {
type: 'input', type: 'input',
listStyle: 'top', listStyle: 'top',
renderDividers: false, renderDividers: false,
completeOnTab: true completeOnTab: true,
isRHS: false
} }
constructor(props) { constructor(props) {
@@ -85,6 +96,7 @@ export default class SuggestionBox extends React.Component {
this.handleBlur = this.handleBlur.bind(this); this.handleBlur = this.handleBlur.bind(this);
this.handlePopoverMentionKeyClick = this.handlePopoverMentionKeyClick.bind(this);
this.handleCompleteWord = this.handleCompleteWord.bind(this); this.handleCompleteWord = this.handleCompleteWord.bind(this);
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.handleCompositionStart = this.handleCompositionStart.bind(this); this.handleCompositionStart = this.handleCompositionStart.bind(this);
@@ -102,10 +114,16 @@ export default class SuggestionBox extends React.Component {
} }
componentDidMount() { componentDidMount() {
if (this.props.popoverMentionKeyClick) {
SuggestionStore.addPopoverMentionKeyClickListener(this.props.isRHS, this.handlePopoverMentionKeyClick);
}
SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged); SuggestionStore.addPretextChangedListener(this.suggestionId, this.handlePretextChanged);
} }
componentWillUnmount() { componentWillUnmount() {
if (this.props.popoverMentionKeyClick) {
SuggestionStore.removePopoverMentionKeyClickListener(this.props.isRHS, this.handlePopoverMentionKeyClick);
}
SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged); SuggestionStore.removePretextChangedListener(this.suggestionId, this.handlePretextChanged);
SuggestionStore.unregisterSuggestionBox(this.suggestionId); SuggestionStore.unregisterSuggestionBox(this.suggestionId);
@@ -121,7 +139,8 @@ export default class SuggestionBox extends React.Component {
getTextbox() { getTextbox() {
if (this.props.type === 'textarea') { if (this.props.type === 'textarea') {
return this.refs.textbox.getDOMNode(); const node = this.refs.textbox.getDOMNode();
return node;
} }
return this.refs.textbox; return this.refs.textbox;
@@ -179,7 +198,18 @@ export default class SuggestionBox extends React.Component {
this.composing = false; 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 textbox = this.getTextbox();
const caret = textbox.selectionEnd; const caret = textbox.selectionEnd;
const text = this.props.value; const text = this.props.value;
@@ -232,8 +262,9 @@ export default class SuggestionBox extends React.Component {
provider.handleCompleteWord(term, matchedPretext); provider.handleCompleteWord(term, matchedPretext);
} }
} }
if (shouldEmitWordSuggestion) {
GlobalActions.emitCompleteWordSuggestion(this.suggestionId); GlobalActions.emitCompleteWordSuggestion(this.suggestionId);
}
} }
handleKeyDown(e) { 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, 'onChange'); // We use onInput instead of onChange on the actual input
Reflect.deleteProperty(props, 'onItemSelected'); Reflect.deleteProperty(props, 'onItemSelected');
Reflect.deleteProperty(props, 'completeOnTab'); Reflect.deleteProperty(props, 'completeOnTab');
Reflect.deleteProperty(props, 'isRHS');
Reflect.deleteProperty(props, 'popoverMentionKeyClick');
const childProps = { const childProps = {
ref: 'textbox', ref: 'textbox',

Просмотреть файл

@@ -36,11 +36,15 @@ export default class Textbox extends React.Component {
supportsCommands: PropTypes.bool.isRequired, supportsCommands: PropTypes.bool.isRequired,
handlePostError: PropTypes.func, handlePostError: PropTypes.func,
suggestionListStyle: PropTypes.string, suggestionListStyle: PropTypes.string,
emojiEnabled: PropTypes.bool emojiEnabled: PropTypes.bool,
isRHS: PropTypes.bool,
popoverMentionKeyClick: React.PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
supportsCommands: true supportsCommands: true,
isRHS: false,
popoverMentionKeyClick: false
}; };
constructor(props) { constructor(props) {
@@ -296,6 +300,8 @@ export default class Textbox extends React.Component {
channelId={this.props.channelId} channelId={this.props.channelId}
value={this.props.value} value={this.props.value}
renderDividers={true} renderDividers={true}
isRHS={this.props.isRHS}
popoverMentionKeyClick={this.props.popoverMentionKeyClick}
/> />
<div <div
ref='preview' ref='preview'

Просмотреть файл

@@ -82,6 +82,8 @@ export default class UserProfile extends React.Component {
status={this.props.status} status={this.props.status}
isBusy={this.props.isBusy} isBusy={this.props.isBusy}
hide={this.hideProfilePopover} hide={this.hideProfilePopover}
isRHS={this.props.isRHS}
hasMention={this.props.hasMention}
/> />
} }
> >
@@ -99,7 +101,9 @@ UserProfile.defaultProps = {
user: {}, user: {},
overwriteName: '', overwriteName: '',
overwriteImage: '', overwriteImage: '',
disablePopover: false disablePopover: false,
isRHS: false,
hasMention: false
}; };
UserProfile.propTypes = { UserProfile.propTypes = {
user: PropTypes.object, user: PropTypes.object,
@@ -108,5 +112,7 @@ UserProfile.propTypes = {
disablePopover: PropTypes.bool, disablePopover: PropTypes.bool,
displayNameType: PropTypes.string, displayNameType: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
isBusy: PropTypes.bool isBusy: PropTypes.bool,
isRHS: PropTypes.bool,
hasMention: PropTypes.bool
}; };

Просмотреть файл

@@ -122,7 +122,7 @@ class SearchStoreClass extends EventEmitter {
updatePost(post) { updatePost(post) {
const results = this.getSearchResults(); const results = this.getSearchResults();
if (results == null) { if (!post || results == null) {
return; return;
} }

Просмотреть файл

@@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes;
const COMPLETE_WORD_EVENT = 'complete_word'; const COMPLETE_WORD_EVENT = 'complete_word';
const PRETEXT_CHANGED_EVENT = 'pretext_changed'; const PRETEXT_CHANGED_EVENT = 'pretext_changed';
const SUGGESTIONS_CHANGED_EVENT = 'suggestions_changed'; const SUGGESTIONS_CHANGED_EVENT = 'suggestions_changed';
const POPOVER_MENTION_KEY_CLICK_EVENT = 'popover_mention_key_click';
class SuggestionStore extends EventEmitter { class SuggestionStore extends EventEmitter {
constructor() { constructor() {
@@ -27,6 +28,10 @@ class SuggestionStore extends EventEmitter {
this.removeCompleteWordListener = this.removeCompleteWordListener.bind(this); this.removeCompleteWordListener = this.removeCompleteWordListener.bind(this);
this.emitCompleteWord = this.emitCompleteWord.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.handleEventPayload = this.handleEventPayload.bind(this);
this.dispatchToken = AppDispatcher.register(this.handleEventPayload); this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
@@ -71,6 +76,16 @@ class SuggestionStore extends EventEmitter {
this.emit(COMPLETE_WORD_EVENT + id, term, matchedPretext); 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) { registerSuggestionBox(id) {
this.suggestions.set(id, { this.suggestions.set(id, {
pretext: '', pretext: '',
@@ -304,6 +319,9 @@ class SuggestionStore extends EventEmitter {
this.completeWord(id, other.term, other.matchedPretext); this.completeWord(id, other.term, other.matchedPretext);
} }
break; break;
case ActionTypes.POPOVER_MENTION_KEY_CLICK:
this.emitPopoverMentionKeyClick(other.isRHS, other.mentionKey);
break;
} }
} }
} }

Просмотреть файл

@@ -114,6 +114,7 @@ export function createDMIntroMessage(channel, centeredIntro) {
width='50' width='50'
height='50' height='50'
user={teammate} user={teammate}
hasMention={true}
/> />
</div> </div>
<div className='channel-intro-profile'> <div className='channel-intro-profile'>
@@ -121,6 +122,7 @@ export function createDMIntroMessage(channel, centeredIntro) {
<UserProfile <UserProfile
user={teammate} user={teammate}
disablePopover={false} disablePopover={false}
hasMention={true}
/> />
</strong> </strong>
</div> </div>