[GH-5915] Clicking on @mention of a user in a post shows the profile popover (#6129)

* post message at-mention profile pop-over

* remove hover effect to special mentions

* make non-clickable the non-existing at-mention username

* fix "video call" position

* use usernameMap instead of initially defined liteUsernameMap

* update per comments
Этот коммит содержится в:
Saturnino Abril
2017-05-01 23:08:54 +09:00
коммит произвёл Harrison Healey
родитель 597641545d
Коммит 83f819451a
20 изменённых файлов: 281 добавлений и 102 удалений

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

@@ -2,7 +2,7 @@
// See License.txt for license information. // See License.txt for license information.
import MultiSelect from 'components/multiselect/multiselect.jsx'; import MultiSelect from 'components/multiselect/multiselect.jsx';
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import {addUsersToTeam} from 'actions/team_actions.jsx'; import {addUsersToTeam} from 'actions/team_actions.jsx';
import {searchUsersNotInTeam} from 'actions/user_actions.jsx'; import {searchUsersNotInTeam} from 'actions/user_actions.jsx';

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

@@ -2,7 +2,7 @@
// See License.txt for license information. // See License.txt for license information.
import MultiSelect from 'components/multiselect/multiselect.jsx'; import MultiSelect from 'components/multiselect/multiselect.jsx';
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import {searchUsers} from 'actions/user_actions.jsx'; import {searchUsers} from 'actions/user_actions.jsx';
import {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_actions.jsx'; import {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_actions.jsx';

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';

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

@@ -3,7 +3,7 @@
import PostHeader from './post_header.jsx'; import PostHeader from './post_header.jsx';
import PostBody from './post_body.jsx'; import PostBody from './post_body.jsx';
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes; const ActionTypes = Constants.ActionTypes;

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import UserProfile from 'components/user_profile.jsx'; import UserProfile from 'components/profile_popover/username_profile_popover.jsx';
import PostInfo from './post_info.jsx'; import PostInfo from './post_info.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';

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

@@ -3,6 +3,9 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {Parser, ProcessNodeDefinitions} from 'html-to-react';
import AtMentionProfile from 'components/profile_popover/atmention_profile_popover.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import * as PostUtils from 'utils/post_utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx';
@@ -88,6 +91,38 @@ export default class PostMessageView extends React.Component {
); );
} }
postMessageHtmlToComponent(html) {
const parser = new Parser();
const attrib = 'data-mention';
const processNodeDefinitions = new ProcessNodeDefinitions(React);
function isValidNode() {
return true;
}
const processingInstructions = [
{
replaceChildren: true,
shouldProcessNode: (node) => node.attribs && node.attribs[attrib] && this.props.usernameMap.hasOwnProperty(node.attribs[attrib]),
processNode: (node) => {
const username = node.attribs[attrib];
return (
<AtMentionProfile
user={this.props.usernameMap[username]}
username={username}
/>
);
}
},
{
shouldProcessNode: () => true,
processNode: processNodeDefinitions.processDefaultNode
}
];
return parser.parseWithInstructions(html, isValidNode, processingInstructions);
}
render() { render() {
if (this.props.post.state === Constants.POST_DELETED) { if (this.props.post.state === Constants.POST_DELETED) {
return this.renderDeletedPost(); return this.renderDeletedPost();
@@ -111,14 +146,17 @@ export default class PostMessageView extends React.Component {
return <div>{renderedSystemMessage}</div>; return <div>{renderedSystemMessage}</div>;
} }
const htmlFormattedText = TextFormatting.formatText(this.props.post.message, options);
const postMessageComponent = this.postMessageHtmlToComponent(htmlFormattedText);
return ( return (
<div> <div>
<span <span
id={this.props.isLastPost ? 'lastPostMessageText' : null} id={this.props.isLastPost ? 'lastPostMessageText' : null}
className='post-message__text' className='post-message__text'
onClick={Utils.handleFormattedTextClick} onClick={Utils.handleFormattedTextClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, options)}}
/> />
{postMessageComponent}
{this.renderEditedIndicator()} {this.renderEditedIndicator()}
</div> </div>
); );

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

@@ -0,0 +1,95 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ProfilePopover from './profile_popover.jsx';
import * as Utils from 'utils/utils.jsx';
import Client from 'client/web_client.jsx';
import {OverlayTrigger} from 'react-bootstrap';
import React from 'react';
export default class AtMentionProfile extends React.Component {
constructor(props) {
super(props);
this.hideProfilePopover = this.hideProfilePopover.bind(this);
}
shouldComponentUpdate(nextProps) {
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true;
}
if (nextProps.overwriteImage !== this.props.overwriteImage) {
return true;
}
if (nextProps.disablePopover !== this.props.disablePopover) {
return true;
}
if (nextProps.displayNameType !== this.props.displayNameType) {
return true;
}
if (nextProps.status !== this.props.status) {
return true;
}
if (nextProps.isBusy !== this.props.isBusy) {
return true;
}
return false;
}
hideProfilePopover() {
this.refs.overlay.hide();
}
render() {
let profileImg = '';
if (this.props.user) {
profileImg = Client.getUsersRoute() + '/' + this.props.user.id + '/image?time=' + this.props.user.last_picture_update;
}
if (this.props.disablePopover) {
return <a className='mention-link'>{'@' + this.props.username}</a>;
}
return (
<OverlayTrigger
ref='overlay'
trigger='click'
placement='right'
rootClose={true}
overlay={
<ProfilePopover
user={this.props.user}
src={profileImg}
status={this.props.status}
isBusy={this.props.isBusy}
hide={this.hideProfilePopover}
/>
}
>
<a className='mention-link'>{'@' + this.props.username}</a>
</OverlayTrigger>
);
}
}
AtMentionProfile.defaultProps = {
overwriteImage: '',
disablePopover: false
};
AtMentionProfile.propTypes = {
user: React.PropTypes.object.isRequired,
username: React.PropTypes.string.isRequired,
overwriteImage: React.PropTypes.string,
disablePopover: React.PropTypes.bool,
displayNameType: React.PropTypes.string,
status: React.PropTypes.string,
isBusy: React.PropTypes.bool
};

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

@@ -1,10 +1,11 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import ProfilePopover from './profile_popover.jsx'; import ProfilePopover from './profile_popover.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import React from 'react'; import React from 'react';
import StatusIcon from './status_icon.jsx'; import StatusIcon from 'components/status_icon.jsx';
import {OverlayTrigger} from 'react-bootstrap'; import {OverlayTrigger} from 'react-bootstrap';
export default class ProfilePicture extends React.Component { export default class ProfilePicture extends React.Component {

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

@@ -23,11 +23,19 @@ 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.generateImage = this.generateImage.bind(this);
this.generateFullname = this.generateFullname.bind(this);
this.generatePosition = this.generatePosition.bind(this);
this.generateWebrtc = this.generateWebrtc.bind(this);
this.generateEmail = this.generateEmail.bind(this);
this.generateDirectMessage = this.generateDirectMessage.bind(this);
this.state = { this.state = {
currentUserId: UserStore.getCurrentId(), currentUserId: UserStore.getCurrentId(),
loadingDMChannel: -1 loadingDMChannel: -1
}; };
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true; return true;
@@ -102,19 +110,63 @@ export default class ProfilePopover extends React.Component {
} }
} }
render() { generateImage(src) {
const popoverProps = Object.assign({}, this.props); return (
delete popoverProps.user; <img
delete popoverProps.src; className='user-popover__image'
delete popoverProps.status; src={src}
delete popoverProps.isBusy; height='128'
delete popoverProps.hide; width='128'
key='user-popover-image'
/>
);
}
let webrtc; generateFullname() {
const fullname = Utils.getFullName(this.props.user);
if (fullname) {
return (
<OverlayTrigger
delayShow={Constants.WEBRTC_TIME_DELAY}
placement='top'
overlay={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>}
>
<div
className='overflow--ellipsis text-nowrap padding-bottom'
>
{fullname}
</div>
</OverlayTrigger>
);
}
return '';
}
generatePosition() {
if (this.props.user.hasOwnProperty('position')) {
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH);
return (
<OverlayTrigger
delayShow={Constants.WEBRTC_TIME_DELAY}
placement='top'
overlay={<Tooltip id='positionTooltip'>{position}</Tooltip>}
>
<div
className='overflow--ellipsis text-nowrap padding-bottom'
>
{position}
</div>
</OverlayTrigger>
);
}
return '';
}
generateWebrtc() {
const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW); const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW);
if (webrtcEnabled && this.props.user.id !== this.state.currentUserId) { if (webrtcEnabled && this.props.user.id !== this.state.currentUserId) {
const isOnline = this.props.status !== UserStatuses.OFFLINE; const isOnline = this.props.status !== UserStatuses.OFFLINE;
let webrtcMessage; let webrtcMessage;
@@ -141,7 +193,7 @@ export default class ProfilePopover extends React.Component {
); );
} }
webrtc = ( return (
<div <div
data-toggle='tooltip' data-toggle='tooltip'
key='makeCall' key='makeCall'
@@ -160,54 +212,15 @@ export default class ProfilePopover extends React.Component {
); );
} }
var dataContent = []; return '';
dataContent.push(
<img
className='user-popover__image'
src={this.props.src}
height='128'
width='128'
key='user-popover-image'
/>
);
const fullname = Utils.getFullName(this.props.user);
if (fullname) {
dataContent.push(
<OverlayTrigger
delayShow={Constants.WEBRTC_TIME_DELAY}
placement='top'
overlay={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>}
>
<div
className='overflow--ellipsis text-nowrap padding-bottom'
>
{fullname}
</div>
</OverlayTrigger>
);
} }
if (this.props.user.position) { generateEmail() {
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH); const email = this.props.user.hasOwnProperty('email') ? this.props.user.email : '';
dataContent.push( const showEmail = (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser());
<OverlayTrigger
delayShow={Constants.WEBRTC_TIME_DELAY}
placement='top'
overlay={<Tooltip id='positionTooltip'>{position}</Tooltip>}
>
<div
className='overflow--ellipsis text-nowrap padding-bottom'
>
{position}
</div>
</OverlayTrigger>
);
}
const email = this.props.user.email; if (email !== '' && showEmail) {
if (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser()) { return (
dataContent.push(
<div <div
data-toggle='tooltip' data-toggle='tooltip'
title={email} title={email}
@@ -223,8 +236,12 @@ export default class ProfilePopover extends React.Component {
); );
} }
return '';
}
generateDirectMessage() {
if (this.props.user.id !== UserStore.getCurrentId()) { if (this.props.user.id !== UserStore.getCurrentId()) {
dataContent.push( return (
<div <div
data-toggle='tooltip' data-toggle='tooltip'
key='user-popover-dm' key='user-popover-dm'
@@ -243,16 +260,27 @@ export default class ProfilePopover extends React.Component {
</a> </a>
</div> </div>
); );
dataContent.push(webrtc);
} }
return '';
}
render() {
return ( return (
<Popover <Popover
{...popoverProps} arrowOffsetLeft={this.props.arrowOffsetLeft}
arrowOffsetTop={this.props.arrowOffsetTop}
positionLeft={this.props.positionLeft}
positionTop={this.props.positionTop}
title={'@' + this.props.user.username} title={'@' + this.props.user.username}
id='user-profile-popover' id='user-profile-popover'
> >
{dataContent} {this.generateImage(this.props.src)}
{this.generateFullname()}
{this.generatePosition()}
{this.generateEmail()}
{this.generateDirectMessage()}
{this.generateWebrtc()}
</Popover> </Popover>
); );
} }

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

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import UserProfile from './user_profile.jsx'; import UserProfile from './profile_popover/username_profile_popover.jsx';
import FileAttachmentListContainer from './file_attachment_list_container.jsx'; import FileAttachmentListContainer from './file_attachment_list_container.jsx';
import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx'; import PendingPostOptions from 'components/post_view/components/pending_post_options.jsx';
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx'; import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
import RhsDropdown from 'components/rhs_dropdown.jsx'; import RhsDropdown from 'components/rhs_dropdown.jsx';

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

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import UserProfile from './user_profile.jsx'; import UserProfile from './profile_popover/username_profile_popover.jsx';
import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx'; import PostBodyAdditionalContent from 'components/post_view/components/post_body_additional_content.jsx';
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import FileAttachmentListContainer from './file_attachment_list_container.jsx'; import FileAttachmentListContainer from './file_attachment_list_container.jsx';
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx'; import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
import RhsDropdown from 'components/rhs_dropdown.jsx'; import RhsDropdown from 'components/rhs_dropdown.jsx';

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

@@ -3,9 +3,9 @@
import $ from 'jquery'; import $ from 'jquery';
import PostMessageContainer from 'components/post_view/components/post_message_container.jsx'; import PostMessageContainer from 'components/post_view/components/post_message_container.jsx';
import UserProfile from './user_profile.jsx'; import UserProfile from './profile_popover/username_profile_popover.jsx';
import FileAttachmentListContainer from './file_attachment_list_container.jsx'; import FileAttachmentListContainer from './file_attachment_list_container.jsx';
import ProfilePicture from './profile_picture.jsx'; import ProfilePicture from './profile_popover/picture_profile_popover.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';

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

@@ -15,6 +15,7 @@
"flux": "3.1.2", "flux": "3.1.2",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"highlight.js": "9.10.0", "highlight.js": "9.10.0",
"html-to-react": "1.2.7",
"inobounce": "0.1.4", "inobounce": "0.1.4",
"intl": "1.2.5", "intl": "1.2.5",
"jasny-bootstrap": "3.1.3", "jasny-bootstrap": "3.1.3",

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

@@ -50,21 +50,6 @@ describe('TextFormatting.AtMentions', function() {
); );
}); });
it('Implied at mentions', function() {
// PLT-4454 Assume users exist for things that look like at mentions until we support the new mention syntax
assert.equal(
TextFormatting.autolinkAtMentions('@user', new Map(), {}),
'$MM_ATMENTION0',
'should imply user exists and replace mention with token'
);
assert.equal(
TextFormatting.autolinkAtMentions('@user.', new Map(), {}),
'$MM_ATMENTION0.',
'should assume username doesn\'t end in punctuation'
);
});
it('Not at mentions', function() { it('Not at mentions', function() {
assert.equal( assert.equal(
TextFormatting.autolinkAtMentions('user@host', new Map(), {user: {}, host: {}}), TextFormatting.autolinkAtMentions('user@host', new Map(), {user: {}, host: {}}),
@@ -75,5 +60,28 @@ describe('TextFormatting.AtMentions', function() {
TextFormatting.autolinkAtMentions('user@email.com', new Map(), {user: {}, email: {}}), TextFormatting.autolinkAtMentions('user@email.com', new Map(), {user: {}, email: {}}),
'user@email.com' 'user@email.com'
); );
assert.equal(
TextFormatting.autolinkAtMentions('@user', new Map(), {}),
'@user'
);
assert.equal(
TextFormatting.autolinkAtMentions('@user.', new Map(), {}),
'@user.',
'should assume username doesn\'t end in punctuation'
);
assert.equal(
TextFormatting.autolinkAtMentions('@will', new Map(), {william: {}}),
'@will',
'should return same text without token'
);
assert.equal(
TextFormatting.autolinkAtMentions('@william', new Map(), {will: {}}),
'@william',
'should return same text without token'
);
}); });
}); });

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

@@ -166,7 +166,7 @@ describe('TextFormatting.Hashtags', function() {
}; };
assert.equal( assert.equal(
TextFormatting.formatText('#@test', options).trim(), TextFormatting.formatText('#@test', options).trim(),
"<p>#<a class='mention-link' href='#' data-mention='test'>@test</a></p>" "<p>#<span data-mention='test'><a class='mention-link' href='#'>@test</a></span></p>"
); );
assert.equal( assert.equal(

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

@@ -5,14 +5,14 @@ import * as Utils from './utils.jsx';
import ChannelInviteModal from 'components/channel_invite_modal'; import ChannelInviteModal from 'components/channel_invite_modal';
import EditChannelHeaderModal from 'components/edit_channel_header_modal.jsx'; import EditChannelHeaderModal from 'components/edit_channel_header_modal.jsx';
import ToggleModalButton from 'components/toggle_modal_button.jsx'; import ToggleModalButton from 'components/toggle_modal_button.jsx';
import UserProfile from 'components/user_profile.jsx'; import UserProfile from 'components/profile_popover/username_profile_popover.jsx';
import ChannelStore from 'stores/channel_store.jsx'; import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx'; import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import * as GlobalActions from 'actions/global_actions.jsx'; import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'client/web_client.jsx'; import Client from 'client/web_client.jsx';
import ProfilePicture from 'components/profile_picture.jsx'; import ProfilePicture from 'components/profile_popover/picture_profile_popover.jsx';
import {showManagementOptions} from './channel_utils.jsx'; import {showManagementOptions} from './channel_utils.jsx';

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

@@ -166,8 +166,13 @@ export function autolinkAtMentions(text, tokens, usernameMap) {
const index = tokens.size; const index = tokens.size;
const alias = `$MM_ATMENTION${index}`; const alias = `$MM_ATMENTION${index}`;
let tokenValue = `<span data-mention='${username}'><a class='mention-link' href='#'>${mention}</a></span>`;
if (Constants.SPECIAL_MENTIONS.indexOf(username) >= 0) {
tokenValue = mention;
}
tokens.set(alias, { tokens.set(alias, {
value: `<a class='mention-link' href='#' data-mention='${username}'>${mention}</a>`, value: tokenValue,
originalText: mention originalText: mention
}); });
return alias; return alias;
@@ -181,8 +186,7 @@ export function autolinkAtMentions(text, tokens, usernameMap) {
const truncated = usernameLower.substring(0, c); const truncated = usernameLower.substring(0, c);
const suffix = usernameLower.substring(c); const suffix = usernameLower.substring(c);
// If we've found a username or run out of punctuation to trim off, render it as an at mention if (mentionExists(truncated) && (c === usernameLower.length || punctuation.test(usernameLower[c]))) {
if (mentionExists(truncated) || !punctuation.test(truncated[truncated.length - 1])) {
const alias = addToken(truncated, '@' + truncated); const alias = addToken(truncated, '@' + truncated);
return prefix + alias + suffix; return prefix + alias + suffix;
} }

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

@@ -989,12 +989,16 @@ export function changeOpacity(oldColor, opacity) {
} }
export function getFullName(user) { export function getFullName(user) {
if (user.first_name && user.last_name) { if (user !== null && typeof user !== 'undefined' && typeof user === 'object') {
return user.first_name + ' ' + user.last_name; const firstName = user.hasOwnProperty('first_name') ? user.first_name : '';
} else if (user.first_name) { const lastName = user.hasOwnProperty('last_name') ? user.last_name : '';
return user.first_name; if (firstName && lastName) {
} else if (user.last_name) { return firstName + ' ' + lastName;
return user.last_name; } else if (firstName) {
return firstName;
} else if (lastName) {
return lastName;
}
} }
return ''; return '';