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
});
}
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 {
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}
/>
}
>

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

@@ -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'
/>

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

@@ -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'
/>

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

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

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

@@ -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>
);

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

@@ -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;

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

@@ -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}
/>
);
}
},
{

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

@@ -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
};

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

@@ -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;

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

@@ -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}/>

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

@@ -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>

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

@@ -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',

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

@@ -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'

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

@@ -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
};

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

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

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

@@ -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;
}
}
}

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

@@ -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>