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.
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
22fa48f455
Коммит
f740698dbe
@@ -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>
|
||||
|
||||
Ссылка в новой задаче
Block a user