diff options
Diffstat (limited to 'web/react')
-rw-r--r-- | web/react/components/admin_console/email_settings.jsx | 67 | ||||
-rw-r--r-- | web/react/components/delete_post_modal.jsx | 1 | ||||
-rw-r--r-- | web/react/components/member_list_item.jsx | 2 | ||||
-rw-r--r-- | web/react/components/member_list_team_item.jsx | 2 | ||||
-rw-r--r-- | web/react/components/popover_list_members.jsx | 46 | ||||
-rw-r--r-- | web/react/components/post.jsx | 7 | ||||
-rw-r--r-- | web/react/components/posts_view.jsx | 67 | ||||
-rw-r--r-- | web/react/components/posts_view_container.jsx | 4 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 4 | ||||
-rw-r--r-- | web/react/components/user_profile.jsx | 6 | ||||
-rw-r--r-- | web/react/components/view_image.jsx | 34 | ||||
-rw-r--r-- | web/react/stores/post_store.jsx | 16 | ||||
-rw-r--r-- | web/react/utils/constants.jsx | 1 | ||||
-rw-r--r-- | web/react/utils/markdown.jsx | 124 | ||||
-rw-r--r-- | web/react/utils/text_formatting.jsx | 2 | ||||
-rw-r--r-- | web/react/utils/utils.jsx | 18 |
16 files changed, 270 insertions, 131 deletions
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index d0565a0e0..238ace3da 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -18,6 +18,7 @@ export default class EmailSettings extends React.Component { this.state = { sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications, + sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications, saveNeeded: false, serverError: null, emailSuccess: null, @@ -36,6 +37,14 @@ export default class EmailSettings extends React.Component { s.sendEmailNotifications = false; } + if (action === 'sendPushNotifications_true') { + s.sendPushNotifications = true; + } + + if (action === 'sendPushNotifications_false') { + s.sendPushNotifications = false; + } + this.setState(s); } @@ -43,11 +52,12 @@ export default class EmailSettings extends React.Component { var config = this.props.config; config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked; config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked; + config.EmailSettings.SendPushlNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked; config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked; - config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked; config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim(); config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim(); config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim(); + config.EmailSettings.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).value.trim(); config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim(); config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim(); config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim(); @@ -526,6 +536,61 @@ export default class EmailSettings extends React.Component { </div> <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='sendPushNotifications' + > + {'Send Push Notifications: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='sendPushNotifications' + value='true' + ref='sendPushNotifications' + defaultChecked={this.props.config.EmailSettings.SendPushNotifications} + onChange={this.handleChange.bind(this, 'sendPushNotifications_true')} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='sendPushNotifications' + value='false' + defaultChecked={!this.props.config.EmailSettings.SendPushNotifications} + onChange={this.handleChange.bind(this, 'sendPushNotifications_false')} + /> + {'false'} + </label> + <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send iOS and Android push notifications through the push notification server.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='PushNotificationServer' + > + {'Push Notification Server:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='PushNotificationServer' + ref='PushNotificationServer' + placeholder='E.g.: "https://push.mattermost.com"' + defaultValue={this.props.config.EmailSettings.PushNotificationServer} + onChange={this.handleChange} + disabled={!this.state.sendPushNotifications} + /> + <p className='help-text'>{'Location of the push notification server.'}</p> + </div> + </div> + + <div className='form-group'> <div className='col-sm-12'> {serverError} <button diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 3c4b17905..5e89a0893 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -152,6 +152,7 @@ export default class DeletePostModal extends React.Component { type='button' className='btn btn-danger' onClick={this.handleDelete} + autoFocus='autofocus' > {'Delete'} </button> diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index 390d25f2e..f5d5ab28b 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -110,7 +110,7 @@ export default class MemberListItem extends React.Component { height='36' width='36' /> - <div className='member-name'>{member.username}</div> + <div className='member-name'>{Utils.displayUsername(member.id)}</div> <div className='member-description'>{member.email}</div> </td> <td className='td--action lg'>{invite}</td> diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 27fb6a4c1..316fad01a 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -174,7 +174,7 @@ export default class MemberListTeamItem extends React.Component { height='36' width='36' /> - <span className='member-name'>{Utils.getDisplayName(user)}</span> + <span className='member-name'>{Utils.displayUsername(user.id)}</span> <span className='member-email'>{email}</span> <div className='dropdown member-drop'> <a diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index b5000141a..f4cb542e4 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -5,6 +5,7 @@ import UserStore from '../stores/user_store.jsx'; var Popover = ReactBootstrap.Popover; var Overlay = ReactBootstrap.Overlay; import * as Utils from '../utils/utils.jsx'; +import Constants from '../utils/constants.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -68,7 +69,7 @@ export default class PopoverListMembers extends React.Component { } render() { - let popoverHtml = []; + const popoverHtml = []; const members = this.props.members; const teamMembers = UserStore.getProfilesUsernameMap(); const currentUserId = UserStore.getCurrentId(); @@ -76,35 +77,13 @@ export default class PopoverListMembers extends React.Component { if (members && teamMembers) { members.sort((a, b) => { - return a.username.localeCompare(b.username); + const aName = Utils.displayUsername(a.id); + const bName = Utils.displayUsername(b.id); + + return aName.localeCompare(bName); }); members.forEach((m, i) => { - const details = []; - - const fullName = Utils.getFullName(m); - if (fullName) { - details.push( - <span - key={`${m.id}__full-name`} - className='full-name' - > - {fullName} - </span> - ); - } - - if (m.nickname) { - const separator = fullName ? ' - ' : ''; - details.push( - <span - key={`${m.nickname}__nickname`} - > - {separator + m.nickname} - </span> - ); - } - let button = ''; if (currentUserId !== m.id && ch.type !== 'D') { button = ( @@ -118,7 +97,12 @@ export default class PopoverListMembers extends React.Component { ); } - if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) { + let name = ''; + if (teamMembers[m.username]) { + name = Utils.displayUsername(teamMembers[m.username].id); + } + + if (name && teamMembers[m.username].delete_at <= 0) { popoverHtml.push( <div className='text-nowrap' @@ -135,7 +119,7 @@ export default class PopoverListMembers extends React.Component { <div className='more-name' > - {m.username} + {name} </div> </div> <div @@ -157,8 +141,8 @@ export default class PopoverListMembers extends React.Component { count = members.length; } - if (count > 20) { - countText = '20+'; + if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) { + countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+'; } else if (count > 0) { countText = count.toString(); } diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 278261e22..66d8c507a 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -87,6 +87,10 @@ export default class Post extends React.Component { return true; } + if (nextProps.displayNameType !== this.props.displayNameType) { + return true; + } + if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) { return true; } @@ -224,5 +228,6 @@ Post.propTypes = { sameRoot: React.PropTypes.bool, hideProfilePic: React.PropTypes.bool, isLastComment: React.PropTypes.bool, - shouldHighlight: React.PropTypes.bool + shouldHighlight: React.PropTypes.bool, + displayNameType: React.PropTypes.string }; diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index 5e374b877..242b26b91 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -2,15 +2,18 @@ // See License.txt for license information. import UserStore from '../stores/user_store.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import * as Utils from '../utils/utils.jsx'; import Post from './post.jsx'; import Constants from '../utils/constants.jsx'; +const Preferences = Constants.Preferences; export default class PostsView extends React.Component { constructor(props) { super(props); + this.updateState = this.updateState.bind(this); this.handleScroll = this.handleScroll.bind(this); this.isAtBottom = this.isAtBottom.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); @@ -22,6 +25,8 @@ export default class PostsView extends React.Component { this.jumpToPostNode = null; this.wasAtBottom = true; this.scrollHeight = 0; + + this.state = {displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value}; } static get SCROLL_TYPE_FREE() { return 1; @@ -38,6 +43,9 @@ export default class PostsView extends React.Component { static get SCROLL_TYPE_POST() { return 5; } + updateState() { + this.setState({displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value}); + } isAtBottom() { return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight); } @@ -94,22 +102,53 @@ export default class PostsView extends React.Component { const prevPostIsComment = Utils.isComment(prevPost); const postFromWebhook = Boolean(post.props && post.props.from_webhook); const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook); + let prevWebhookName = ''; + if (prevPost.props && prevPost.props.override_username) { + prevWebhookName = prevPost.props.override_username; + } + let curWebhookName = ''; + if (post.props && post.props.override_username) { + curWebhookName = post.props.override_username; + } + + // consider posts from the same user if: + // the previous post was made by the same user as the current post, + // the previous post was made within 5 minutes of the current post, + // the previous post and current post are both from webhooks or both not, + // the previous post and current post have the same webhook usernames + if (prevPost.user_id === post.user_id && + post.create_at - prevPost.create_at <= 1000 * 60 * 5 && + postFromWebhook === prevPostFromWebhook && + prevWebhookName === curWebhookName) { + sameUser = true; + } - sameUser = prevPost.user_id === post.user_id && postFromWebhook === prevPostFromWebhook && - post.create_at - prevPost.create_at <= 1000 * 60 * 5; - sameRoot = (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) || (!postIsComment && !prevPostIsComment && sameUser); + // consider posts from the same root if: + // the current post is a comment, + // the current post has the same root as the previous post + if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) { + sameRoot = true; + } + + // consider posts from the same root if: + // the current post is not a comment, + // the previous post is not a comment, + // the previous post is from the same user + if (!postIsComment && !prevPostIsComment && sameUser) { + sameRoot = true; + } // hide the profile pic if: // the previous post was made by the same user as the current post, // the previous post is not a comment, // the current post is not a comment, - // the current post is not from a webhook - // and the previous post is not from a webhook - if ((prevPost.user_id === post.user_id) && + // the previous post and current post are both from webhooks or both not, + // the previous post and current post have the same webhook usernames + if (prevPost.user_id === post.user_id && !prevPostIsComment && !postIsComment && - !postFromWebhook && - !prevPostFromWebhook) { + postFromWebhook === prevPostFromWebhook && + prevWebhookName === curWebhookName) { hideProfilePic = true; } } @@ -135,6 +174,7 @@ export default class PostsView extends React.Component { isLastComment={isLastComment} shouldHighlight={shouldHighlight} onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + displayNameType={this.state.displayNameType} /> ); @@ -240,16 +280,20 @@ export default class PostsView extends React.Component { this.updateScrolling(); } window.addEventListener('resize', this.handleResize); + $(this.refs.postlist).perfectScrollbar(); + PreferenceStore.addChangeListener(this.updateState); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); + PreferenceStore.removeChangeListener(this.updateState); } componentDidUpdate() { if (this.props.postList != null) { this.updateScrolling(); } + $(this.refs.postlist).perfectScrollbar('update'); } - shouldComponentUpdate(nextProps) { + shouldComponentUpdate(nextProps, nextState) { if (this.props.isActive !== nextProps.isActive) { return true; } @@ -268,6 +312,9 @@ export default class PostsView extends React.Component { if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) { return true; } + if (nextState.displayNameType !== this.state.displayNameType) { + return true; + } return false; } @@ -326,7 +373,7 @@ export default class PostsView extends React.Component { return ( <div ref='postlist' - className={'post-list-holder-by-time ' + activeClass} + className={'ps-container post-list-holder-by-time ' + activeClass} onScroll={this.handleScroll} > <div className='post-list__table'> diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 367d3687e..6d6694fec 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -99,10 +99,12 @@ export default class PostsViewContainer extends React.Component { if (newIndex === -1) { newIndex = channels.length; channels.push(channelId); - postLists[newIndex] = this.getChannelPosts(channelId); atTop[newIndex] = PostStore.getVisibilityAtTop(channelId); } + // make sure we have the latest posts from the store + postLists[newIndex] = this.getChannelPosts(channelId); + this.setState({ currentChannelIndex: newIndex, currentLastViewed: lastViewed, diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 30422ff7d..b4c037183 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -173,6 +173,10 @@ export default class Sidebar extends React.Component { this.updateScrollbar(); window.addEventListener('resize', this.handleResize); + + if ($(window).width() > 768) { + $('.nav-pills__container').perfectScrollbar(); + } } shouldComponentUpdate(nextProps, nextState) { if (!Utils.areObjectsEqual(nextState, this.state)) { diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 438c0bc82..ea104fedb 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -54,9 +54,11 @@ export default class UserProfile extends React.Component { } } render() { - var name = this.state.profile.username; + var name = Utils.displayUsername(this.state.profile.id); if (this.props.overwriteName) { name = this.props.overwriteName; + } else if (!name) { + name = '...'; } if (this.props.disablePopover) { @@ -107,7 +109,7 @@ export default class UserProfile extends React.Component { rootClose={true} overlay={ <Popover - title={this.state.profile.username} + title={name} id='user-profile-popover' > {dataContent} diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 91f4b3bdc..2b505607e 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -424,23 +424,27 @@ export default class ViewImageModal extends React.Component { > <div className={'image-wrapper ' + bgClass} - onMouseEnter={this.onMouseEnterImage} - onMouseLeave={this.onMouseLeaveImage} - onClick={(e) => e.stopPropagation()} + onClick={this.props.onModalDismissed} > <div - className={closeButtonClass} - onClick={this.props.onModalDismissed} - /> - {content} - <ViewImagePopoverBar - show={this.state.showFooter} - fileId={this.state.imgId} - totalFiles={this.props.filenames.length} - filename={name} - fileURL={fileUrl} - getPublicLink={this.getPublicLink} - /> + onMouseEnter={this.onMouseEnterImage} + onMouseLeave={this.onMouseLeaveImage} + onClick={(e) => e.stopPropagation()} + > + <div + className={closeButtonClass} + onClick={this.props.onModalDismissed} + /> + {content} + <ViewImagePopoverBar + show={this.state.showFooter} + fileId={this.state.imgId} + totalFiles={this.props.filenames.length} + filename={name} + fileURL={fileUrl} + getPublicLink={this.getPublicLink} + /> + </div> </div> {leftArrow} {rightArrow} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 24b0d0dd0..2212edadb 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -39,6 +39,7 @@ class PostStoreClass extends EventEmitter { this.makePostsInfo = this.makePostsInfo.bind(this); + this.getPost = this.getPost.bind(this); this.getAllPosts = this.getAllPosts.bind(this); this.getEarliestPost = this.getEarliestPost.bind(this); this.getLatestPost = this.getLatestPost.bind(this); @@ -160,6 +161,17 @@ class PostStoreClass extends EventEmitter { } } + getPost(channelId, postId) { + const posts = this.postsInfo[channelId].postList; + let post = null; + + if (posts.posts.hasOwnProperty(postId)) { + post = Object.assign({}, posts.posts[postId]); + } + + return post; + } + getAllPosts(id) { if (this.postsInfo.hasOwnProperty(id)) { return Object.assign({}, this.postsInfo[id].postList); @@ -199,7 +211,7 @@ class PostStoreClass extends EventEmitter { postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order); } - // Add delteted posts + // Add deleted posts if (this.postsInfo[id].hasOwnProperty('deletedPosts')) { Object.assign(postList.posts, this.postsInfo[id].deletedPosts); @@ -554,7 +566,7 @@ class PostStoreClass extends EventEmitter { return 0; } getCommentCount(post) { - const posts = this.getPosts(post.channel_id).posts; + const posts = this.getAllPosts(post.channel_id).posts; let commentCount = 0; for (const id in posts) { diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 2009e07dd..cbeab18ba 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -137,6 +137,7 @@ export default { ], MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], MAX_DMS: 20, + MAX_CHANNEL_POPOVER_COUNT: 20, DM_CHANNEL: 'D', OPEN_CHANNEL: 'O', PRIVATE_CHANNEL: 'P', diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index b0ec64bfd..f2721c81d 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -223,6 +223,16 @@ class MattermostMarkdownRenderer extends marked.Renderer { return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`; } + listitem(text) { + const taskListReg = /^\[([ |xX])\] /; + const isTaskList = taskListReg.exec(text); + + if (isTaskList) { + return `<li>${'<input type="checkbox" disabled="disabled" ' + (isTaskList[1] === ' ' ? '' : 'checked="checked" ') + '/> '}${text.replace(taskListReg, '')}</li>`; + } + return `<li>${text}</li>`; + } + text(txt) { return TextFormatting.doFormatText(txt, this.formattingOptions); } @@ -361,78 +371,78 @@ class MattermostLexer extends marked.Lexer { // list cap = this.rules.list.exec(src); if (cap) { + src = src.substring(cap[0].length); const bull = cap[2]; - let l = cap[0].length; + + this.tokens.push({ + type: 'list_start', + ordered: bull.length > 1 + }); // Get each top-level item. cap = cap[0].match(this.rules.item); - if (cap.length > 1) { - src = src.substring(l); - - this.tokens.push({ - type: 'list_start', - ordered: bull.length > 1 - }); - - let next = false; - l = cap.length; - - for (let i = 0; i < l; i++) { - let item = cap[i]; - - // Remove the list item's bullet - // so it is seen as the next token. - let space = item.length; - item = item.replace(/^ *([*+-]|\d+\.) +/, ''); - - // Outdent whatever the - // list item contains. Hacky. - if (~item.indexOf('\n ')) { - space -= item.length; - item = this.options.pedantic ? item.replace(/^ {1,4}/gm, '') : item.replace(new RegExp('^ \{1,' + space + '\}', 'gm'), ''); - } + let next = false; + const l = cap.length; + let i = 0; + + for (; i < l; i++) { + let item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + let space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = this.options.pedantic ? + item.replace(/^ {1,4}/gm, '') : + item.replace(new RegExp('^ {1,' + space + '}', 'gm'), ''); + } - // Determine whether the next list item belongs here. - // Backpedal if it does not belong in this list. - if (this.options.smartLists && i !== l - 1) { - const bullet = /(?:[*+-]|\d+\.)/; - const b = bullet.exec(cap[i + 1])[0]; - if (bull !== b && !(bull.length > 1 && b.length > 1)) { - src = cap.slice(i + 1).join('\n') + src; - i = l - 1; - } + // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + if (this.options.smartLists && i !== l - 1) { + const b = this.rules.bullet.exec(cap[i + 1])[0]; + if (bull !== b && !(bull.length > 1 && b.length > 1)) { + src = cap.slice(i + 1).join('\n') + src; + i = l - 1; } + } - // Determine whether item is loose or not. - // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ - // for discount behavior. - let loose = next || (/\n\n(?!\s*$)/).test(item); - if (i !== l - 1) { - next = item.charAt(item.length - 1) === '\n'; - if (!loose) { - loose = next; - } + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + let loose = next || (/\n\n(?!\s*$)/).test(item); + if (i !== l - 1) { + next = item.charAt(item.length - 1) === '\n'; + if (!loose) { + loose = next; } - - this.tokens.push({ - type: loose ? 'loose_item_start' : 'list_item_start' - }); - - // Recurse. - this.token(item, false, bq); - - this.tokens.push({ - type: 'list_item_end' - }); } this.tokens.push({ - type: 'list_end' + type: loose ? + 'loose_item_start' : + 'list_item_start' }); - continue; + // Recurse. + this.token(item, false, bq); + + this.tokens.push({ + type: 'list_item_end' + }); } + + this.tokens.push({ + type: 'list_end' + }); + + continue; } // html diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 3a912fd75..f0bd46f9d 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -188,7 +188,7 @@ function highlightCurrentMentions(text, tokens) { const newAlias = `MM_SELFMENTION${index}`; newTokens.set(newAlias, { - value: `<span class='mention-highlight'>${alias}</span>` + token.extraText, + value: `<span class='mention-highlight'>${alias}</span>` + (token.extraText || ''), originalText: token.originalText }); output = output.replace(alias, newAlias); diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 80c377d7f..aa9146183 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -854,7 +854,7 @@ export function isMobile() { export function isComment(post) { if ('root_id' in post) { - return post.root_id !== ''; + return post.root_id !== '' && post.root_id != null; } return false; } @@ -981,13 +981,15 @@ export function displayUsername(userId) { const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value; let username = ''; - if (nameFormat === 'nickname_full_name') { - username = user.nickname || getFullName(user); - } else if (nameFormat === 'full_name') { - username = getFullName(user); - } - if (!username.trim().length) { - username = user.username; + if (user) { + if (nameFormat === 'nickname_full_name') { + username = user.nickname || getFullName(user); + } else if (nameFormat === 'full_name') { + username = getFullName(user); + } + if (!username.trim().length) { + username = user.username; + } } return username; |