diff options
20 files changed, 278 insertions, 99 deletions
diff --git a/webapp/components/add_users_to_team/add_users_to_team.jsx b/webapp/components/add_users_to_team/add_users_to_team.jsx index ae6fd8c4e..ee22bbed5 100644 --- a/webapp/components/add_users_to_team/add_users_to_team.jsx +++ b/webapp/components/add_users_to_team/add_users_to_team.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. 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 {searchUsersNotInTeam} from 'actions/user_actions.jsx'; diff --git a/webapp/components/more_direct_channels/more_direct_channels.jsx b/webapp/components/more_direct_channels/more_direct_channels.jsx index 50e2c4e48..743236ce6 100644 --- a/webapp/components/more_direct_channels/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels/more_direct_channels.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. 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 {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_actions.jsx'; diff --git a/webapp/components/popover_list_members/popover_list_members.jsx b/webapp/components/popover_list_members/popover_list_members.jsx index cf6042943..458ae8f24 100644 --- a/webapp/components/popover_list_members/popover_list_members.jsx +++ b/webapp/components/popover_list_members/popover_list_members.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // 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 UserStore from 'stores/user_store.jsx'; diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index f5c96d2bc..cf25b28e4 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -3,7 +3,7 @@ import PostHeader from './post_header.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'; const ActionTypes = Constants.ActionTypes; diff --git a/webapp/components/post_view/components/post_header.jsx b/webapp/components/post_view/components/post_header.jsx index 9de0b7e79..e19285963 100644 --- a/webapp/components/post_view/components/post_header.jsx +++ b/webapp/components/post_view/components/post_header.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // 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 {FormattedMessage} from 'react-intl'; diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/components/post_message_view.jsx index 5b0790f36..db522c974 100644 --- a/webapp/components/post_view/components/post_message_view.jsx +++ b/webapp/components/post_view/components/post_message_view.jsx @@ -3,6 +3,9 @@ import React from 'react'; 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 * 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() { if (this.props.post.state === Constants.POST_DELETED) { return this.renderDeletedPost(); @@ -111,14 +146,17 @@ export default class PostMessageView extends React.Component { return <div>{renderedSystemMessage}</div>; } + const htmlFormattedText = TextFormatting.formatText(this.props.post.message, options); + const postMessageComponent = this.postMessageHtmlToComponent(htmlFormattedText); + return ( <div> <span id={this.props.isLastPost ? 'lastPostMessageText' : null} className='post-message__text' onClick={Utils.handleFormattedTextClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, options)}} /> + {postMessageComponent} {this.renderEditedIndicator()} </div> ); diff --git a/webapp/components/profile_popover/atmention_profile_popover.jsx b/webapp/components/profile_popover/atmention_profile_popover.jsx new file mode 100644 index 000000000..47c625f64 --- /dev/null +++ b/webapp/components/profile_popover/atmention_profile_popover.jsx @@ -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 +}; diff --git a/webapp/components/profile_picture.jsx b/webapp/components/profile_popover/picture_profile_popover.jsx index b7ee08785..2c2b91b25 100644 --- a/webapp/components/profile_picture.jsx +++ b/webapp/components/profile_popover/picture_profile_popover.jsx @@ -1,10 +1,11 @@ // Copyright (c) 2016-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 React from 'react'; -import StatusIcon from './status_icon.jsx'; +import StatusIcon from 'components/status_icon.jsx'; import {OverlayTrigger} from 'react-bootstrap'; export default class ProfilePicture extends React.Component { diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover/profile_popover.jsx index 63bd99ac4..a32b7904b 100644 --- a/webapp/components/profile_popover.jsx +++ b/webapp/components/profile_popover/profile_popover.jsx @@ -23,11 +23,19 @@ export default class ProfilePopover extends React.Component { this.initWebrtc = this.initWebrtc.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 = { currentUserId: UserStore.getCurrentId(), loadingDMChannel: -1 }; } + shouldComponentUpdate(nextProps) { if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { return true; @@ -102,19 +110,63 @@ export default class ProfilePopover extends React.Component { } } - render() { - const popoverProps = Object.assign({}, this.props); - delete popoverProps.user; - delete popoverProps.src; - delete popoverProps.status; - delete popoverProps.isBusy; - delete popoverProps.hide; - - let webrtc; - const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + generateImage(src) { + return ( + <img + className='user-popover__image' + src={src} + height='128' + width='128' + key='user-popover-image' + /> + ); + } - const webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW); + 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 webrtcEnabled = global.mm_config.EnableWebrtc === 'true' && userMedia && Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW); if (webrtcEnabled && this.props.user.id !== this.state.currentUserId) { const isOnline = this.props.status !== UserStatuses.OFFLINE; let webrtcMessage; @@ -141,7 +193,7 @@ export default class ProfilePopover extends React.Component { ); } - webrtc = ( + return ( <div data-toggle='tooltip' key='makeCall' @@ -160,54 +212,15 @@ export default class ProfilePopover extends React.Component { ); } - var dataContent = []; - 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> - ); - } + return ''; + } - if (this.props.user.position) { - const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH); - dataContent.push( - <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> - ); - } + generateEmail() { + const email = this.props.user.hasOwnProperty('email') ? this.props.user.email : ''; + const showEmail = (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser()); - const email = this.props.user.email; - if (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser()) { - dataContent.push( + if (email !== '' && showEmail) { + return ( <div data-toggle='tooltip' title={email} @@ -223,8 +236,12 @@ export default class ProfilePopover extends React.Component { ); } + return ''; + } + + generateDirectMessage() { if (this.props.user.id !== UserStore.getCurrentId()) { - dataContent.push( + return ( <div data-toggle='tooltip' key='user-popover-dm' @@ -243,16 +260,27 @@ export default class ProfilePopover extends React.Component { </a> </div> ); - dataContent.push(webrtc); } + return ''; + } + + render() { return ( <Popover - {...popoverProps} + arrowOffsetLeft={this.props.arrowOffsetLeft} + arrowOffsetTop={this.props.arrowOffsetTop} + positionLeft={this.props.positionLeft} + positionTop={this.props.positionTop} title={'@' + this.props.user.username} id='user-profile-popover' > - {dataContent} + {this.generateImage(this.props.src)} + {this.generateFullname()} + {this.generatePosition()} + {this.generateEmail()} + {this.generateDirectMessage()} + {this.generateWebrtc()} </Popover> ); } diff --git a/webapp/components/user_profile.jsx b/webapp/components/profile_popover/username_profile_popover.jsx index 37993094b..37993094b 100644 --- a/webapp/components/user_profile.jsx +++ b/webapp/components/profile_popover/username_profile_popover.jsx diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 10cd5fb55..d7b899b33 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -1,11 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // 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 PendingPostOptions from 'components/post_view/components/pending_post_options.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 RhsDropdown from 'components/rhs_dropdown.jsx'; diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 41dd92e91..32d03e524 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -1,11 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // 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 PostMessageContainer from 'components/post_view/components/post_message_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 RhsDropdown from 'components/rhs_dropdown.jsx'; diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index 09ea8c427..846840e40 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -3,9 +3,9 @@ import $ from 'jquery'; 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 ProfilePicture from './profile_picture.jsx'; +import ProfilePicture from './profile_popover/picture_profile_popover.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx index 3a7fc5d1c..e4e937432 100644 --- a/webapp/components/user_list_row.jsx +++ b/webapp/components/user_list_row.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // 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 PreferenceStore from 'stores/preference_store.jsx'; diff --git a/webapp/package.json b/webapp/package.json index e7203f0d6..d3d12a1fb 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -15,6 +15,7 @@ "flux": "3.1.2", "font-awesome": "4.7.0", "highlight.js": "9.10.0", + "html-to-react": "1.2.7", "inobounce": "0.1.4", "intl": "1.2.5", "jasny-bootstrap": "3.1.3", diff --git a/webapp/tests/utils/formatting_at_mentions.test.jsx b/webapp/tests/utils/formatting_at_mentions.test.jsx index d64b42c3f..51f9bef65 100644 --- a/webapp/tests/utils/formatting_at_mentions.test.jsx +++ b/webapp/tests/utils/formatting_at_mentions.test.jsx @@ -50,30 +50,38 @@ 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 + it('Not at mentions', function() { + assert.equal( + TextFormatting.autolinkAtMentions('user@host', new Map(), {user: {}, host: {}}), + 'user@host' + ); + + assert.equal( + TextFormatting.autolinkAtMentions('user@email.com', new Map(), {user: {}, email: {}}), + 'user@email.com' + ); + assert.equal( TextFormatting.autolinkAtMentions('@user', new Map(), {}), - '$MM_ATMENTION0', - 'should imply user exists and replace mention with token' + '@user' ); assert.equal( TextFormatting.autolinkAtMentions('@user.', new Map(), {}), - '$MM_ATMENTION0.', + '@user.', 'should assume username doesn\'t end in punctuation' ); - }); - it('Not at mentions', function() { assert.equal( - TextFormatting.autolinkAtMentions('user@host', new Map(), {user: {}, host: {}}), - 'user@host' + TextFormatting.autolinkAtMentions('@will', new Map(), {william: {}}), + '@will', + 'should return same text without token' ); assert.equal( - TextFormatting.autolinkAtMentions('user@email.com', new Map(), {user: {}, email: {}}), - 'user@email.com' + TextFormatting.autolinkAtMentions('@william', new Map(), {will: {}}), + '@william', + 'should return same text without token' ); }); }); diff --git a/webapp/tests/utils/formatting_hashtags.test.jsx b/webapp/tests/utils/formatting_hashtags.test.jsx index 1740a8ce7..0aadd6626 100644 --- a/webapp/tests/utils/formatting_hashtags.test.jsx +++ b/webapp/tests/utils/formatting_hashtags.test.jsx @@ -166,7 +166,7 @@ describe('TextFormatting.Hashtags', function() { }; assert.equal( 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( diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index 31ba8708d..cc5071047 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -5,14 +5,14 @@ import * as Utils from './utils.jsx'; import ChannelInviteModal from 'components/channel_invite_modal'; import EditChannelHeaderModal from 'components/edit_channel_header_modal.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 UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; import * as GlobalActions from 'actions/global_actions.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'; diff --git a/webapp/utils/text_formatting.jsx b/webapp/utils/text_formatting.jsx index c2c71a4e1..bd718b363 100644 --- a/webapp/utils/text_formatting.jsx +++ b/webapp/utils/text_formatting.jsx @@ -166,8 +166,13 @@ export function autolinkAtMentions(text, tokens, usernameMap) { const index = tokens.size; 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, { - value: `<a class='mention-link' href='#' data-mention='${username}'>${mention}</a>`, + value: tokenValue, originalText: mention }); return alias; @@ -181,8 +186,7 @@ export function autolinkAtMentions(text, tokens, usernameMap) { const truncated = usernameLower.substring(0, 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) || !punctuation.test(truncated[truncated.length - 1])) { + if (mentionExists(truncated) && (c === usernameLower.length || punctuation.test(usernameLower[c]))) { const alias = addToken(truncated, '@' + truncated); return prefix + alias + suffix; } diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index f56b9bb09..9b29c5362 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -989,12 +989,16 @@ export function changeOpacity(oldColor, opacity) { } export function getFullName(user) { - if (user.first_name && user.last_name) { - return user.first_name + ' ' + user.last_name; - } else if (user.first_name) { - return user.first_name; - } else if (user.last_name) { - return user.last_name; + if (user !== null && typeof user !== 'undefined' && typeof user === 'object') { + const firstName = user.hasOwnProperty('first_name') ? user.first_name : ''; + const lastName = user.hasOwnProperty('last_name') ? user.last_name : ''; + if (firstName && lastName) { + return firstName + ' ' + lastName; + } else if (firstName) { + return firstName; + } else if (lastName) { + return lastName; + } } return ''; |