diff options
Diffstat (limited to 'web/react')
36 files changed, 674 insertions, 724 deletions
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 1a0c9c6d5..95b4caa12 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -254,7 +254,7 @@ export default class ActivityLogModal extends React.Component { <p className='session-help-text'> <FormattedMessage id='activity_log.sessionsDescription' - defaultMessage="Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the 'Logout' button below to end a session." + defaultMessage="Sessions are created when you log in to a new browser on a device. Sessions let you use Mattermost without having to log in again for a time period specified by the System Admin. If you want to log out sooner, use the 'Logout' button below to end a session." /> </p> {content} diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx index 3332f37ef..fdbe912ef 100644 --- a/web/react/components/admin_console/license_settings.jsx +++ b/web/react/components/admin_console/license_settings.jsx @@ -203,8 +203,7 @@ class LicenseSettings extends React.Component { <p className='help-text'> <FormattedHTMLMessage id='admin.license.uploadDesc' - defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> - to learn more about the benefits of Enterprise Edition or to purchase a key.' + defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.' /> </p> </div> diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index f232d4633..047c7eb8d 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -287,9 +287,7 @@ class ServiceSettings extends React.Component { <p className='help-text'> <FormattedHTMLMessage id='admin.service.googleDescription' - defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at - <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. - Leaving the field blank disables the automatic generation of YouTube video previews from links.' + defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.' /> </p> </div> diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx index 6cb749075..97c615768 100644 --- a/web/react/components/center_panel.jsx +++ b/web/react/components/center_panel.jsx @@ -27,20 +27,24 @@ export default class CenterPanel extends React.Component { this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onChannelChange = this.onChannelChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); this.state = { showTutorialScreens: tutorialStep === TutorialSteps.INTRO_SCREENS, - showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS + showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS, + user: UserStore.getCurrentUser() }; } componentDidMount() { PreferenceStore.addChangeListener(this.onPreferenceChange); ChannelStore.addChangeListener(this.onChannelChange); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { PreferenceStore.removeChangeListener(this.onPreferenceChange); ChannelStore.removeChangeListener(this.onChannelChange); + UserStore.removeChangeListener(this.onUserChange); } onPreferenceChange() { const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); @@ -49,6 +53,9 @@ export default class CenterPanel extends React.Component { onChannelChange() { this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS}); } + onUserChange() { + this.setState({user: UserStore.getCurrentUser()}); + } render() { const channel = ChannelStore.getCurrent(); var handleClick = null; @@ -108,7 +115,9 @@ export default class CenterPanel extends React.Component { className='app__content' > <div id='channel-header'> - <ChannelHeader/> + <ChannelHeader + user={this.state.user} + /> </div> {postsContainer} {createPost} diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index b5eb546cf..727f84e8e 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -11,6 +11,7 @@ import ChannelInviteModal from './channel_invite_modal.jsx'; import ChannelMembersModal from './channel_members_modal.jsx'; import ChannelNotificationsModal from './channel_notifications_modal.jsx'; import DeleteChannelModal from './delete_channel_modal.jsx'; +import RenameChannelModal from './rename_channel_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -39,10 +40,13 @@ export default class ChannelHeader extends React.Component { this.onListenerChange = this.onListenerChange.bind(this); this.handleLeave = this.handleLeave.bind(this); this.searchMentions = this.searchMentions.bind(this); + this.showRenameChannelModal = this.showRenameChannelModal.bind(this); + this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; state.showMembersModal = false; + state.showRenameChannelModal = false; this.state = state; } getStateFromStores() { @@ -51,7 +55,6 @@ export default class ChannelHeader extends React.Component { return { channel: ChannelStore.getCurrent(), memberChannel: ChannelStore.getCurrentMember(), - memberTeam: UserStore.getCurrentUser(), users: extraInfo.members, userCount: extraInfo.member_count, searchVisible: SearchStore.getSearchResults() !== null @@ -61,14 +64,12 @@ export default class ChannelHeader extends React.Component { ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addExtraInfoChangeListener(this.onListenerChange); SearchStore.addSearchChangeListener(this.onListenerChange); - UserStore.addChangeListener(this.onListenerChange); PreferenceStore.addChangeListener(this.onListenerChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); SearchStore.removeSearchChangeListener(this.onListenerChange); - UserStore.removeChangeListener(this.onListenerChange); PreferenceStore.removeChangeListener(this.onListenerChange); } onListenerChange() { @@ -97,11 +98,11 @@ export default class ChannelHeader extends React.Component { searchMentions(e) { e.preventDefault(); - const user = UserStore.getCurrentUser(); + const user = this.props.user; let terms = ''; if (user.notify_props && user.notify_props.mention_keys) { - const termKeys = UserStore.getCurrentMentionKeys(); + const termKeys = UserStore.getMentionKeys(user.id); if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) { termKeys.splice(termKeys.indexOf('@all'), 1); @@ -120,6 +121,18 @@ export default class ChannelHeader extends React.Component { is_mention_search: true }); } + showRenameChannelModal(e) { + e.preventDefault(); + + this.setState({ + showRenameChannelModal: true + }); + } + hideRenameChannelModal() { + this.setState({ + showRenameChannelModal: false + }); + } render() { if (this.state.channel === null) { return null; @@ -150,8 +163,8 @@ export default class ChannelHeader extends React.Component { </Popover> ); let channelTitle = channel.display_name; - const currentId = UserStore.getCurrentId(); - const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.memberTeam.roles); + const currentId = this.props.user.id; + const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.props.user.roles); const isDirect = (this.state.channel.type === 'D'); if (isDirect) { @@ -326,11 +339,7 @@ export default class ChannelHeader extends React.Component { <a role='menuitem' href='#' - data-toggle='modal' - data-target='#rename_channel' - data-display={channel.display_name} - data-name={channel.name} - data-channelid={channel.id} + onClick={this.showRenameChannelModal} > <FormattedMessage id='channel_header.rename' @@ -470,7 +479,16 @@ export default class ChannelHeader extends React.Component { onModalDismissed={() => this.setState({showMembersModal: false})} channel={channel} /> + <RenameChannelModal + show={this.state.showRenameChannelModal} + onHide={this.hideRenameChannelModal} + channel={channel} + /> </div> ); } } + +ChannelHeader.propTypes = { + user: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx index 6591de687..7048434f8 100644 --- a/web/react/components/channel_notifications_modal.jsx +++ b/web/react/components/channel_notifications_modal.jsx @@ -153,7 +153,7 @@ export default class ChannelNotificationsModal extends React.Component { /> <FormattedMessage id='channel_notifications.globalDefault' - defaultMessage='Global default ({notifyLevel}' + defaultMessage='Global default ({notifyLevel})' values={{ notifyLevel: (globalNotifyLevelName) }} diff --git a/web/react/components/claim/email_to_sso.jsx b/web/react/components/claim/email_to_sso.jsx index 87e86697c..c3eea9495 100644 --- a/web/react/components/claim/email_to_sso.jsx +++ b/web/react/components/claim/email_to_sso.jsx @@ -84,7 +84,7 @@ class EmailToSSO extends React.Component { <p> <FormattedMessage id='claim.email_to_sso.ssoType' - defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO. You must already have a valid {type} account' + defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO' values={{ type: Utils.toTitleCase(this.props.type) }} diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 773d0b420..f8e3e406a 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -68,7 +68,7 @@ export default class DeletePostModal extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - results: null + postId: null }); } else if (selectedPost.id === this.state.post.id && this.state.root_id) { if (selectedPost.root_id && selectedPost.root_id.length > 0 && selectedList.posts[selectedPost.root_id]) { @@ -77,7 +77,7 @@ export default class DeletePostModal extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - post_list: selectedList + postId: selectedPost.root_id }); AppDispatcher.handleServerAction({ diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 380790b8f..581b8e0b5 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -101,12 +101,10 @@ export default class Login extends React.Component { <div> {loginMessage} <div className='or__container'> - <span> - <FormattedMessage - id='login.or' - defaultMessage='or' - /> - </span> + <FormattedMessage + id='login.or' + defaultMessage='or' + /> </div> </div> ); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index a06fb449b..93fe6c05a 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -5,11 +5,11 @@ import EditChannelHeaderModal from './edit_channel_header_modal.jsx'; import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx'; import MessageWrapper from './message_wrapper.jsx'; import NotifyCounts from './notify_counts.jsx'; -import ChannelMembersModal from './channel_members_modal.jsx'; import ChannelInfoModal from './channel_info_modal.jsx'; import ChannelInviteModal from './channel_invite_modal.jsx'; import ChannelNotificationsModal from './channel_notifications_modal.jsx'; import DeleteChannelModal from './delete_channel_modal.jsx'; +import RenameChannelModal from './rename_channel_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import UserStore from '../stores/user_store.jsx'; @@ -39,6 +39,8 @@ export default class Navbar extends React.Component { this.showSearch = this.showSearch.bind(this); this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this); + this.showRenameChannelModal = this.showRenameChannelModal.bind(this); + this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); this.createCollapseButtons = this.createCollapseButtons.bind(this); this.createDropdown = this.createDropdown.bind(this); @@ -47,6 +49,7 @@ export default class Navbar extends React.Component { state.showEditChannelPurposeModal = false; state.showEditChannelHeaderModal = false; state.showMembersModal = false; + state.showRenameChannelModal = false; this.state = state; } getStateFromStores() { @@ -90,7 +93,7 @@ export default class Navbar extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - results: null + postId: null }); if (e.target.className !== 'navbar-toggle' && e.target.className !== 'icon-bar') { @@ -128,6 +131,18 @@ export default class Navbar extends React.Component { showEditChannelHeaderModal: true }); } + showRenameChannelModal(e) { + e.preventDefault(); + + this.setState({ + showRenameChannelModal: true + }); + } + hideRenameChannelModal() { + this.setState({ + showRenameChannelModal: false + }); + } createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) { if (channel) { var viewInfoOption = ( @@ -253,11 +268,7 @@ export default class Navbar extends React.Component { <a role='menuitem' href='#' - data-toggle='modal' - data-target='#rename_channel' - data-display={channel.display_name} - data-name={channel.name} - data-channelid={channel.id} + onClick={this.showRenameChannelModal} > <FormattedMessage id='navbar.rename' @@ -410,6 +421,7 @@ export default class Navbar extends React.Component { var editChannelHeaderModal = null; var editChannelPurposeModal = null; + let renameChannelModal = null; if (channel) { popoverContent = ( @@ -492,6 +504,14 @@ export default class Navbar extends React.Component { channel={channel} /> ); + + renameChannelModal = ( + <RenameChannelModal + show={this.state.showRenameChannelModal} + onHide={this.hideRenameChannelModal} + channel={channel} + /> + ); } var collapseButtons = this.createCollapseButtons(currentId); @@ -524,11 +544,7 @@ export default class Navbar extends React.Component { </nav> {editChannelHeaderModal} {editChannelPurposeModal} - <ChannelMembersModal - show={this.state.showMembersModal} - onModalDismissed={() => this.setState({showMembersModal: false})} - channel={{channel}} - /> + {renameChannelModal} </div> ); } diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 3619a9f8f..889d4311e 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -3,15 +3,18 @@ import PostHeader from './post_header.jsx'; import PostBody from './post_body.jsx'; -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import Constants from '../utils/constants.jsx'; + import UserStore from '../stores/user_store.jsx'; import PostStore from '../stores/post_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; -import * as client from '../utils/client.jsx'; + +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -var ActionTypes = Constants.ActionTypes; -import * as utils from '../utils/utils.jsx'; +import * as Utils from '../utils/utils.jsx'; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; export default class Post extends React.Component { constructor(props) { @@ -26,13 +29,9 @@ export default class Post extends React.Component { handleCommentClick(e) { e.preventDefault(); - var data = {}; - data.order = [this.props.post.id]; - data.posts = this.props.posts; - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - post_list: data + postId: Utils.getRootId(this.props.post) }); AppDispatcher.handleServerAction({ @@ -48,14 +47,14 @@ export default class Post extends React.Component { e.preventDefault(); var post = this.props.post; - client.createPost(post, post.channel_id, + Client.createPost(post, post.channel_id, (data) => { AsyncClient.getPosts(); var channel = ChannelStore.get(post.channel_id); var member = ChannelStore.getMember(post.channel_id); member.msg_count = channel.total_msg_count; - member.last_viewed_at = utils.getTimestamp(); + member.last_viewed_at = Utils.getTimestamp(); ChannelStore.setChannelMember(member); AppDispatcher.handleServerAction({ @@ -75,7 +74,7 @@ export default class Post extends React.Component { this.forceUpdate(); } shouldComponentUpdate(nextProps) { - if (!utils.areObjectsEqual(nextProps.post, this.props.post)) { + if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } @@ -99,6 +98,10 @@ export default class Post extends React.Component { return true; } + if (nextProps.hasProfiles !== this.props.hasProfiles) { + return true; + } + return false; } getCommentCount(props) { @@ -125,6 +128,7 @@ export default class Post extends React.Component { const post = this.props.post; const parentPost = this.props.parentPost; const posts = this.props.posts; + const user = this.props.user || {}; if (!post.props) { post.props = {}; @@ -152,15 +156,13 @@ export default class Post extends React.Component { } let currentUserCss = ''; - if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook && !utils.isSystemMessage(post)) { + if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) { currentUserCss = 'current--user'; } - const userProfile = UserStore.getProfile(post.user_id); - - let timestamp = UserStore.getCurrentUser().update_at; - if (userProfile) { - timestamp = userProfile.update_at; + let timestamp = user.update_at; + if (timestamp == null) { + timestamp = UserStore.getCurrentUser().update_at; } let sameUserClass = ''; @@ -174,18 +176,18 @@ export default class Post extends React.Component { } let systemMessageClass = ''; - if (utils.isSystemMessage(post)) { + if (Utils.isSystemMessage(post)) { systemMessageClass = 'post--system'; } let profilePic = null; if (!this.props.hideProfilePic) { - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex(); if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; } - } else if (utils.isSystemMessage(post)) { + } else if (Utils.isSystemMessage(post)) { src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE; } @@ -215,6 +217,7 @@ export default class Post extends React.Component { handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} sameUser={this.props.sameUser} + user={this.props.user} /> <PostBody post={post} @@ -223,6 +226,7 @@ export default class Post extends React.Component { posts={posts} handleCommentClick={this.handleCommentClick} retryPost={this.retryPost} + hasProfiles={this.props.hasProfiles} /> </div> </div> @@ -233,13 +237,15 @@ export default class Post extends React.Component { } Post.propTypes = { - post: React.PropTypes.object, + post: React.PropTypes.object.isRequired, posts: React.PropTypes.object, parentPost: React.PropTypes.object, + user: React.PropTypes.object, sameUser: React.PropTypes.bool, sameRoot: React.PropTypes.bool, hideProfilePic: React.PropTypes.bool, isLastComment: React.PropTypes.bool, shouldHighlight: React.PropTypes.bool, - displayNameType: React.PropTypes.string + displayNameType: React.PropTypes.string, + hasProfiles: React.PropTypes.bool }; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index d71ac6ec7..70cf86748 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -6,13 +6,9 @@ import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import * as Emoji from '../utils/emoticons.jsx'; import Constants from '../utils/constants.jsx'; -const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; -import YoutubeVideo from './youtube_video.jsx'; - -import providers from './providers.json'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -31,22 +27,7 @@ class PostBody extends React.Component { constructor(props) { super(props); - this.isImgLoading = false; - - this.handleUserChange = this.handleUserChange.bind(this); this.parseEmojis = this.parseEmojis.bind(this); - this.createEmbed = this.createEmbed.bind(this); - this.createImageEmbed = this.createImageEmbed.bind(this); - this.loadImg = this.loadImg.bind(this); - - const linkData = Utils.extractLinks(this.props.post.message); - const profiles = UserStore.getProfiles(); - - this.state = { - links: linkData.links, - post: this.props.post, - hasUserProfiles: profiles && Object.keys(profiles).length > 1 - }; } getAllChildNodes(nodeIn) { @@ -72,132 +53,8 @@ class PostBody extends React.Component { }); } - componentWillMount() { - if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) { - this.embed = this.createEmbed(this.state.links[0]); - } - } - componentDidMount() { this.parseEmojis(); - - UserStore.addChangeListener(this.handleUserChange); - } - - componentDidUpdate() { - this.parseEmojis(); - } - - componentWillUnmount() { - UserStore.removeChangeListener(this.handleUserChange); - } - - handleUserChange() { - if (!this.state.hasProfiles) { - const profiles = UserStore.getProfiles(); - - this.setState({hasProfiles: profiles && Object.keys(profiles).length > 1}); - } - } - - componentWillReceiveProps(nextProps) { - const linkData = Utils.extractLinks(nextProps.post.message); - if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) { - this.embed = this.createEmbed(linkData.links[0]); - } - this.setState({ - links: linkData.links - }); - } - - createEmbed(link) { - const post = this.state.post; - - if (!link) { - if (post.type === 'oEmbed') { - post.props.oEmbedLink = ''; - post.type = ''; - } - return null; - } - - const trimmedLink = link.trim(); - - if (Utils.isFeatureEnabled(PreReleaseFeatures.EMBED_PREVIEW)) { - const provider = this.getOembedProvider(trimmedLink); - if (provider != null) { - post.props.oEmbedLink = trimmedLink; - post.type = 'oEmbed'; - this.setState({post, provider}); - return ''; - } - } - - if (YoutubeVideo.isYoutubeLink(link)) { - return ( - <YoutubeVideo - channelId={post.channel_id} - link={link} - /> - ); - } - - for (let i = 0; i < Constants.IMAGE_TYPES.length; i++) { - const imageType = Constants.IMAGE_TYPES[i]; - const suffix = link.substring(link.length - (imageType.length + 1)); - if (suffix === '.' + imageType || suffix === '=' + imageType) { - return this.createImageEmbed(link, this.state.imgLoaded); - } - } - - return null; - } - - getOembedProvider(link) { - for (let i = 0; i < providers.length; i++) { - for (let j = 0; j < providers[i].patterns.length; j++) { - if (link.match(providers[i].patterns[j])) { - return providers[i]; - } - } - } - return null; - } - - loadImg(src) { - if (this.isImgLoading) { - return; - } - - this.isImgLoading = true; - - const img = new Image(); - img.onload = ( - () => { - this.embed = this.createImageEmbed(src, true); - this.setState({imgLoaded: true}); - } - ); - img.src = src; - } - - createImageEmbed(link, isLoaded) { - if (!isLoaded) { - this.loadImg(link); - return ( - <img - className='img-div placeholder' - height='500px' - /> - ); - } - - return ( - <img - className='img-div' - src={link} - /> - ); } render() { @@ -312,6 +169,7 @@ class PostBody extends React.Component { } let message; + let additionalContent = null; if (this.props.post.state === Constants.POST_DELETED) { message = ( <FormattedMessage @@ -326,6 +184,10 @@ class PostBody extends React.Component { dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message)}} /> ); + + additionalContent = ( + <PostBodyAdditionalContent post={this.props.post}/> + ); } return ( @@ -340,12 +202,8 @@ class PostBody extends React.Component { {loading} {message} </div> - <PostBodyAdditionalContent - post={this.state.post} - provider={this.state.provider} - /> {fileAttachmentHolder} - {this.embed} + {additionalContent} </div> </div> ); @@ -357,7 +215,8 @@ PostBody.propTypes = { post: React.PropTypes.object.isRequired, parentPost: React.PropTypes.object, retryPost: React.PropTypes.func.isRequired, - handleCommentClick: React.PropTypes.func.isRequired + handleCommentClick: React.PropTypes.func.isRequired, + hasProfiles: React.PropTypes.bool }; export default injectIntl(PostBody); diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx index 4871eea4f..a76c59fb3 100644 --- a/web/react/components/post_body_additional_content.jsx +++ b/web/react/components/post_body_additional_content.jsx @@ -3,72 +3,103 @@ import PostAttachmentList from './post_attachment_list.jsx'; import PostAttachmentOEmbed from './post_attachment_oembed.jsx'; +import PostImage from './post_image.jsx'; +import YoutubeVideo from './youtube_video.jsx'; + +import Constants from '../utils/constants.jsx'; +import OEmbedProviders from './providers.json'; +import * as Utils from '../utils/utils.jsx'; export default class PostBodyAdditionalContent extends React.Component { constructor(props) { super(props); this.getSlackAttachment = this.getSlackAttachment.bind(this); - this.getOembedAttachment = this.getOembedAttachment.bind(this); - this.getComponent = this.getComponent.bind(this); + this.getOEmbedProvider = this.getOEmbedProvider.bind(this); } - componentWillMount() { - this.setState({type: this.props.post.type, shouldRender: Boolean(this.props.post.type)}); + shouldComponentUpdate(nextProps) { + if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { + return true; + } + + return false; } getSlackAttachment() { - const attachments = this.props.post.props && this.props.post.props.attachments || []; + let attachments = []; + if (this.props.post.props && this.props.post.props.attachments) { + attachments = this.props.post.props.attachments; + } + return ( <PostAttachmentList - key={'post_body_additional_content' + this.props.post.id} attachments={attachments} /> ); } - getOembedAttachment() { - const link = this.props.post.props && this.props.post.props.oEmbedLink || ''; - return ( - <PostAttachmentOEmbed - key={'post_body_additional_content' + this.props.post.id} - provider={this.props.provider} - link={link} - /> - ); + getOEmbedProvider(link) { + for (let i = 0; i < OEmbedProviders.length; i++) { + for (let j = 0; j < OEmbedProviders[i].patterns.length; j++) { + if (link.match(OEmbedProviders[i].patterns[j])) { + return OEmbedProviders[i]; + } + } + } + + return null; } - getComponent() { - switch (this.props.post.type) { - case 'slack_attachment': + render() { + if (this.props.post.type === 'slack_attachment') { return this.getSlackAttachment(); - case 'oEmbed': - return this.getOembedAttachment(); - default: - return ''; } - } - render() { - let content = []; + const link = Utils.extractLinks(this.props.post.message)[0]; + if (!link) { + return null; + } - if (this.props.post.type) { - const component = this.getComponent(); + if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) { + const provider = this.getOEmbedProvider(link); - if (component) { - content = component; + if (provider) { + return ( + <PostAttachmentOEmbed + provider={provider} + link={link} + /> + ); } } - return ( - <div> - {content} - </div> - ); + if (YoutubeVideo.isYoutubeLink(link)) { + return ( + <YoutubeVideo + channelId={this.props.post.channel_id} + link={link} + /> + ); + } + + for (let i = 0; i < Constants.IMAGE_TYPES.length; i++) { + const imageType = Constants.IMAGE_TYPES[i]; + const suffix = link.substring(link.length - (imageType.length + 1)); + if (suffix === '.' + imageType || suffix === '=' + imageType) { + return ( + <PostImage + channelId={this.props.post.channel_id} + link={link} + /> + ); + } + } + + return null; } } PostBodyAdditionalContent.propTypes = { - post: React.PropTypes.object.isRequired, - provider: React.PropTypes.object + post: React.PropTypes.object.isRequired }; diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx index be22989a6..1f5c15aa9 100644 --- a/web/react/components/post_deleted_modal.jsx +++ b/web/react/components/post_deleted_modal.jsx @@ -38,7 +38,7 @@ export default class PostDeletedModal extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - results: null + postId: null }); this.props.onHide(); diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index c52391daa..2803fe387 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -13,16 +13,17 @@ export default class PostHeader extends React.Component { this.state = {}; } render() { - var post = this.props.post; + const post = this.props.post; + const user = this.props.user; - let userProfile = <UserProfile userId={post.user_id}/>; + let userProfile = <UserProfile user={user}/>; let botIndicator; if (post.props && post.props.from_webhook) { if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( <UserProfile - userId={post.user_id} + user={user} overwriteName={post.props.override_username} disablePopover={true} /> @@ -33,7 +34,7 @@ export default class PostHeader extends React.Component { } else if (Utils.isSystemMessage(post)) { userProfile = ( <UserProfile - userId={''} + user={{}} overwriteName={Constants.SYSTEM_MESSAGE_PROFILE_NAME} overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE} disablePopover={true} @@ -68,6 +69,7 @@ PostHeader.defaultProps = { }; PostHeader.propTypes = { post: React.PropTypes.object, + user: React.PropTypes.object, commentCount: React.PropTypes.number, isLastComment: React.PropTypes.bool, handleCommentClick: React.PropTypes.func, diff --git a/web/react/components/post_image.jsx b/web/react/components/post_image.jsx new file mode 100644 index 000000000..da4a25794 --- /dev/null +++ b/web/react/components/post_image.jsx @@ -0,0 +1,81 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +export default class PostImageEmbed extends React.Component { + constructor(props) { + super(props); + + this.handleLoadComplete = this.handleLoadComplete.bind(this); + this.handleLoadError = this.handleLoadError.bind(this); + + this.state = { + loaded: false, + errored: false + }; + } + + componentWillMount() { + this.loadImg(this.props.link); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.link !== this.props.link) { + this.setState({ + loaded: false, + errored: false + }); + } + } + + componentDidUpdate(prevProps) { + if (!this.state.loaded && prevProps.link !== this.props.link) { + this.loadImg(this.props.link); + } + } + + loadImg(src) { + const img = new Image(); + img.onload = this.handleLoadComplete; + img.onerror = this.handleLoadError; + img.src = src; + } + + handleLoadComplete() { + this.setState({ + loaded: true + }); + } + + handleLoadError() { + this.setState({ + errored: true, + loaded: true + }); + } + + render() { + if (this.state.errored) { + return null; + } + + if (!this.state.loaded) { + return ( + <img + className='img-div placeholder' + height='500px' + /> + ); + } + + return ( + <img + className='img-div' + src={this.props.link} + /> + ); + } +} + +PostImageEmbed.propTypes = { + link: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index 15810d9cf..1ea7711ea 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -145,6 +145,7 @@ export default class PostsView extends React.Component { const postCtls = []; let previousPostDay = new Date(0); const userId = UserStore.getCurrentId(); + const profiles = this.props.profiles || {}; let renderedLastViewed = false; @@ -228,6 +229,13 @@ export default class PostsView extends React.Component { const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id); + let profile; + if (UserStore.getCurrentId() === post.user_id) { + profile = UserStore.getCurrentUser(); + } else { + profile = profiles[post.user_id]; + } + const postCtl = ( <Post key={keyPrefix + 'postKey'} @@ -242,6 +250,8 @@ export default class PostsView extends React.Component { shouldHighlight={shouldHighlight} onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} + hasProfiles={profiles && Object.keys(profiles).length > 1} + user={profile} /> ); @@ -413,6 +423,9 @@ export default class PostsView extends React.Component { if (this.state.isScrolling !== nextState.isScrolling) { return true; } + if (!Utils.areObjectsEqual(this.props.profiles, nextProps.profiles)) { + return true; + } return false; } @@ -513,6 +526,7 @@ PostsView.defaultProps = { PostsView.propTypes = { isActive: React.PropTypes.bool, postList: React.PropTypes.object, + profiles: React.PropTypes.object, scrollPostId: React.PropTypes.string, scrollType: React.PropTypes.number, postViewScrolled: React.PropTypes.func.isRequired, diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 972342021..1b14e8681 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -6,6 +6,7 @@ import LoadingScreen from './loading_screen.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; +import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import * as EventHelpers from '../dispatcher/event_helpers.jsx'; @@ -24,11 +25,13 @@ export default class PostsViewContainer extends React.Component { this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); + this.onUserChange = this.onUserChange.bind(this); const currentChannelId = ChannelStore.getCurrentId(); const state = { scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null + scrollPost: null, + profiles: JSON.parse(JSON.stringify(UserStore.getProfiles())) }; if (currentChannelId) { Object.assign(state, { @@ -54,12 +57,14 @@ export default class PostsViewContainer extends React.Component { ChannelStore.addLeaveListener(this.onChannelLeave); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeLeaveListener(this.onChannelLeave); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.removeChangeListener(this.onUserChange); } handlePostsViewJumpRequest(type, post) { switch (type) { @@ -135,6 +140,9 @@ export default class PostsViewContainer extends React.Component { atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId); this.setState({postLists, atTop}); } + onUserChange() { + this.setState({profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))}); + } getChannelPosts(id) { return PostStore.getVisiblePosts(id); } @@ -180,6 +188,7 @@ export default class PostsViewContainer extends React.Component { showMoreMessagesBottom={false} introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} + profiles={this.state.profiles} /> ); if (!postLists[i] && isActive) { diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index c467c0d87..e96ff0db2 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -4,11 +4,12 @@ import * as Utils from '../utils/utils.jsx'; import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import ChannelStore from '../stores/channel_store.jsx'; import Constants from '../utils/constants.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +const Modal = ReactBootstrap.Modal; + const holders = defineMessages({ required: { id: 'rename_channel.required', @@ -44,33 +45,81 @@ export default class RenameChannelModal extends React.Component { constructor(props) { super(props); + this.handleShow = this.handleShow.bind(this); + this.handleHide = this.handleHide.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.handleCancel = this.handleCancel.bind(this); + this.onNameChange = this.onNameChange.bind(this); this.onDisplayNameChange = this.onDisplayNameChange.bind(this); this.displayNameKeyUp = this.displayNameKeyUp.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleShow = this.handleShow.bind(this); - this.handleShown = this.handleShown.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); this.state = { - displayName: '', - channelName: '', - channelId: '', + displayName: props.channel.display_name, + channelName: props.channel.name, serverError: '', nameError: '', displayNameError: '', invalid: false }; } - handleSubmit(e) { - e.preventDefault(); - if (this.state.channelId.length !== 26) { - return; + componentWillReceiveProps(nextProps) { + if (!Utils.areObjectsEqual(nextProps.channel, this.props.channel)) { + this.setState({ + displayName: nextProps.channel.display_name, + channelName: nextProps.channel.name + }); } + } + + shouldComponentUpdate(nextProps, nextState) { + if (!nextProps.show && !this.props.show) { + return false; + } + + if (!Utils.areObjectsEqual(nextState, this.state)) { + return true; + } + + if (!Utils.areObjectsEqual(nextProps, this.props)) { + return true; + } + + return false; + } + + componentDidUpdate(prevProps) { + if (!prevProps.show && this.props.show) { + this.handleShow(); + } + } + + handleShow() { + const textbox = ReactDOM.findDOMNode(this.refs.displayName); + textbox.focus(); + Utils.placeCaretAtEnd(textbox); + } + + handleHide(e) { + if (e) { + e.preventDefault(); + } + + this.props.onHide(); + + this.setState({ + serverError: '', + nameError: '', + displayNameError: '', + invalid: false + }); + } - const channel = ChannelStore.get(this.state.channelId); + handleSubmit(e) { + e.preventDefault(); + + const channel = Object.assign({}, this.props.channel); const oldName = channel.name; const oldDisplayName = channel.displayName; const state = {serverError: ''}; @@ -110,29 +159,40 @@ export default class RenameChannelModal extends React.Component { return; } - Client.updateChannel(channel, + Client.updateChannel( + channel, () => { - $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide'); - AsyncClient.getChannel(channel.id); Utils.updateAddressBar(channel.name); - ReactDOM.findDOMNode(this.refs.displayName).value = ''; - ReactDOM.findDOMNode(this.refs.channelName).value = ''; + this.handleHide(); }, (err) => { - state.serverError = err.message; - state.invalid = true; - this.setState(state); + this.setState({ + serverError: err.message, + invalid: true + }); } ); } + + handleCancel(e) { + this.setState({ + displayName: this.props.channel.display_name, + channelName: this.props.channel.name + }); + + this.handleHide(e); + } + onNameChange() { this.setState({channelName: ReactDOM.findDOMNode(this.refs.channelName).value}); } + onDisplayNameChange() { this.setState({displayName: ReactDOM.findDOMNode(this.refs.displayName).value}); } + displayNameKeyUp() { if (this.state.channelName !== Constants.DEFAULT_CHANNEL) { const displayName = ReactDOM.findDOMNode(this.refs.displayName).value.trim(); @@ -141,32 +201,7 @@ export default class RenameChannelModal extends React.Component { this.setState({channelName: channelName}); } } - handleClose() { - this.setState({ - displayName: '', - channelName: '', - channelId: '', - serverError: '', - nameError: '', - displayNameError: '', - invalid: false - }); - } - handleShow(e) { - const button = $(e.relatedTarget); - this.setState({displayName: button.attr('data-display'), channelName: button.attr('data-name'), channelId: button.attr('data-channelid')}); - } - handleShown() { - $('#rename_channel #display_name').focus(); - } - componentDidMount() { - $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.handleShow); - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); - $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown); - } - componentWillUnmount() { - $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); - } + render() { let displayNameError = null; let displayNameClass = 'form-group'; @@ -194,112 +229,93 @@ export default class RenameChannelModal extends React.Component { let readOnlyHandleInput = false; if (this.state.channelName === Constants.DEFAULT_CHANNEL) { handleInputLabel += formatMessage(holders.defaultError); - handleInputClass += ' disabled-input'; readOnlyHandleInput = true; } return ( - <div - className='modal fade' - ref='modal' - id='rename_channel' - tabIndex='-1' - role='dialog' - aria-hidden='true' + <Modal + show={this.props.show} + onHide={this.handleCancel} > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - > - <span aria-hidden='true'>{'×'}</span> - <span className='sr-only'> - <FormattedMessage - id='rename_channel.close' - defaultMessage='Close' - /> - </span> - </button> - <h4 className='modal-title'> - <FormattedMessage - id='rename_channel.title' - defaultMessage='Rename Channel' + <Modal.Header closeButton={true}> + <Modal.Title> + <FormattedMessage + id='rename_channel.title' + defaultMessage='Rename Channel' + /> + </Modal.Title> + </Modal.Header> + <form role='form'> + <Modal.Body> + <div className={displayNameClass}> + <label className='control-label'> + <FormattedMessage + id='rename_channel.displayName' + defaultMessage='Display Name' + /> + </label> + <input + onKeyUp={this.displayNameKeyUp} + onChange={this.onDisplayNameChange} + type='text' + ref='displayName' + id='display_name' + className='form-control' + placeholder={formatMessage(holders.displayNameHolder)} + value={this.state.displayName} + maxLength='64' + /> + {displayNameError} + </div> + <div className={nameClass}> + <label className='control-label'>{handleInputLabel}</label> + <input + onChange={this.onNameChange} + type='text' + className={handleInputClass} + ref='channelName' + placeholder={formatMessage(holders.handleHolder)} + value={this.state.channelName} + maxLength='64' + readOnly={readOnlyHandleInput} /> - </h4> + {nameError} </div> - <form role='form'> - <div className='modal-body'> - <div className={displayNameClass}> - <label className='control-label'> - <FormattedMessage - id='rename_channel.displayName' - defaultMessage='Display Name' - /> - </label> - <input - onKeyUp={this.displayNameKeyUp} - onChange={this.onDisplayNameChange} - type='text' - ref='displayName' - id='display_name' - className='form-control' - placeholder={formatMessage(holders.displayNameHolder)} - value={this.state.displayName} - maxLength='64' - /> - {displayNameError} - </div> - <div className={nameClass}> - <label className='control-label'>{handleInputLabel}</label> - <input - onChange={this.onNameChange} - type='text' - className={handleInputClass} - ref='channelName' - placeholder={formatMessage(holders.handleHolder)} - value={this.state.channelName} - maxLength='64' - readOnly={readOnlyHandleInput} - /> - {nameError} - </div> - {serverError} - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - > - <FormattedMessage - id='rename_channel.cancel' - defaultMessage='Cancel' - /> - </button> - <button - onClick={this.handleSubmit} - type='submit' - className='btn btn-primary' - > - <FormattedMessage - id='rename_channel.save' - defaultMessage='Save' - /> - </button> - </div> - </form> - </div> - </div> - </div> + {serverError} + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.handleCancel} + > + <FormattedMessage + id='rename_channel.cancel' + defaultMessage='Cancel' + /> + </button> + <button + onClick={this.handleSubmit} + type='submit' + className='btn btn-primary' + > + <FormattedMessage + id='rename_channel.save' + defaultMessage='Save' + /> + </button> + </Modal.Footer> + </form> + </Modal> ); } } RenameChannelModal.propTypes = { - intl: intlShape.isRequired + intl: intlShape.isRequired, + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired }; -export default injectIntl(RenameChannelModal);
\ No newline at end of file +export default injectIntl(RenameChannelModal); diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 2ebca9b8d..9588809eb 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -194,8 +194,16 @@ class RhsComment extends React.Component { var timestamp = UserStore.getCurrentUser().update_at; - var loading; - var postClass = ''; + let loading; + let postClass = ''; + let message = ( + <div + ref='message_holder' + onClick={TextFormatting.handleClick} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}} + /> + ); + if (post.state === Constants.POST_FAILED) { postClass += ' post-fail'; loading = ( @@ -218,6 +226,13 @@ class RhsComment extends React.Component { src='/static/images/load.gif' /> ); + } else if (this.props.post.state === Constants.POST_DELETED) { + message = ( + <FormattedMessage + id='post_body.deleted' + defaultMessage='(message deleted)' + /> + ); } var dropdown = this.createDropdown(); @@ -246,7 +261,7 @@ class RhsComment extends React.Component { <div> <ul className='post__header'> <li className='col__name'> - <strong><UserProfile userId={post.user_id}/></strong> + <strong><UserProfile user={this.props.user}/></strong> </li> <li className='col'> <time className='post__time'> @@ -268,11 +283,7 @@ class RhsComment extends React.Component { <div className='post__body'> <div className={postClass}> {loading} - <div - ref='message_holder' - onClick={TextFormatting.handleClick} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}} - /> + {message} </div> {fileAttachment} </div> @@ -288,7 +299,8 @@ RhsComment.defaultProps = { }; RhsComment.propTypes = { intl: intlShape.isRequired, - post: React.PropTypes.object + post: React.PropTypes.object, + user: React.PropTypes.object }; export default injectIntl(RhsComment); diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx index cd310df56..4c9f6f3f6 100644 --- a/web/react/components/rhs_header_post.jsx +++ b/web/react/components/rhs_header_post.jsx @@ -27,7 +27,7 @@ export default class RhsHeaderPost extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - results: null + postId: null }); } handleBack(e) { @@ -42,7 +42,7 @@ export default class RhsHeaderPost extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - results: null + postId: null }); } render() { diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 32946ef23..023f3dd2d 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -50,10 +50,10 @@ export default class RhsRootPost extends React.Component { this.parseEmojis(); } render() { - var post = this.props.post; - var currentUser = UserStore.getCurrentUser(); - var isOwner = currentUser.id === post.user_id; - var isAdmin = Utils.isAdmin(currentUser.roles); + const post = this.props.post; + const user = this.props.user; + var isOwner = user.id === post.user_id; + var isAdmin = Utils.isAdmin(user.roles); var timestamp = UserStore.getProfile(post.user_id).update_at; var channel = ChannelStore.get(post.channel_id); @@ -62,9 +62,9 @@ export default class RhsRootPost extends React.Component { type = 'Comment'; } - var currentUserCss = ''; + var userCss = ''; if (UserStore.getCurrentId() === post.user_id) { - currentUserCss = 'current--user'; + userCss = 'current--user'; } var systemMessageClass = ''; @@ -185,14 +185,14 @@ export default class RhsRootPost extends React.Component { ); } - let userProfile = <UserProfile userId={post.user_id}/>; + let userProfile = <UserProfile user={user}/>; let botIndicator; if (post.props && post.props.from_webhook) { if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( <UserProfile - userId={post.user_id} + user={user} overwriteName={post.props.override_username} disablePopover={true} /> @@ -203,7 +203,7 @@ export default class RhsRootPost extends React.Component { } else if (Utils.isSystemMessage(post)) { userProfile = ( <UserProfile - userId={''} + user={{}} overwriteName={Constants.SYSTEM_MESSAGE_PROFILE_NAME} overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE} disablePopover={true} @@ -230,7 +230,7 @@ export default class RhsRootPost extends React.Component { ); return ( - <div className={'post post--root ' + currentUserCss + ' ' + systemMessageClass}> + <div className={'post post--root ' + userCss + ' ' + systemMessageClass}> <div className='post-right-channel__name'>{channelName}</div> <div className='post__content'> <div className='post__img'> @@ -278,10 +278,10 @@ export default class RhsRootPost extends React.Component { } RhsRootPost.defaultProps = { - post: null, commentCount: 0 }; RhsRootPost.propTypes = { - post: React.PropTypes.object, + post: React.PropTypes.object.isRequired, + user: React.PropTypes.object.isRequired, commentCount: React.PropTypes.number }; diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index 667030b3a..4d770287c 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -19,39 +19,25 @@ export default class RhsThread extends React.Component { this.mounted = false; - this.onChange = this.onChange.bind(this); - this.onChangeAll = this.onChangeAll.bind(this); + this.onPostChange = this.onPostChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.forceUpdateInfo = this.forceUpdateInfo.bind(this); this.handleResize = this.handleResize.bind(this); - const state = this.getStateFromStores(); + const state = {}; state.windowWidth = Utils.windowWidth(); state.windowHeight = Utils.windowHeight(); - this.state = state; - } - getStateFromStores() { - var postList = PostStore.getSelectedPost(); - if (!postList || postList.order.length < 1 || !postList.posts[postList.order[0]]) { - return {postList: {}}; - } - - var channelId = postList.posts[postList.order[0]].channel_id; - var pendingPostsList = PostStore.getPendingPosts(channelId); - - if (pendingPostsList) { - for (var pid in pendingPostsList.posts) { - if (pendingPostsList.posts.hasOwnProperty(pid)) { - postList.posts[pid] = pendingPostsList.posts[pid]; - } - } - } + state.selected = PostStore.getSelectedPost(); + state.posts = PostStore.getSelectedPostThread(); + state.profiles = JSON.parse(JSON.stringify(UserStore.getProfiles())); - return {postList: postList}; + this.state = state; } componentDidMount() { - PostStore.addSelectedPostChangeListener(this.onChange); - PostStore.addChangeListener(this.onChangeAll); + PostStore.addSelectedPostChangeListener(this.onPostChange); + PostStore.addChangeListener(this.onPostChange); PreferenceStore.addChangeListener(this.forceUpdateInfo); + UserStore.addChangeListener(this.onUserChange); this.resize(); window.addEventListener('resize', this.handleResize); @@ -65,14 +51,30 @@ export default class RhsThread extends React.Component { this.resize(); } componentWillUnmount() { - PostStore.removeSelectedPostChangeListener(this.onChange); - PostStore.removeChangeListener(this.onChangeAll); + PostStore.removeSelectedPostChangeListener(this.onPostChange); + PostStore.removeChangeListener(this.onPostChange); PreferenceStore.removeChangeListener(this.forceUpdateInfo); + UserStore.removeChangeListener(this.onUserChange); window.removeEventListener('resize', this.handleResize); this.mounted = false; } + shouldComponentUpdate(nextProps, nextState) { + if (!Utils.areObjectsEqual(nextState.posts, this.state.posts)) { + return true; + } + + if (!Utils.areObjectsEqual(nextState.selected, this.state.selected)) { + return true; + } + + if (!Utils.areObjectsEqual(nextState.profiles, this.state.profiles)) { + return true; + } + + return false; + } forceUpdateInfo() { if (this.state.postList) { for (var postId in this.state.postList.posts) { @@ -88,49 +90,14 @@ export default class RhsThread extends React.Component { windowHeight: Utils.windowHeight() }); } - onChange() { - var newState = this.getStateFromStores(); - if (this.mounted && !Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } + onPostChange() { + const selected = PostStore.getSelectedPost(); + const posts = PostStore.getSelectedPostThread(); + this.setState({posts, selected}); } - onChangeAll() { - // if something was changed in the channel like adding a - // comment or post then lets refresh the sidebar list - var currentSelected = PostStore.getSelectedPost(); - if (!currentSelected || currentSelected.order.length === 0 || !currentSelected.posts[currentSelected.order[0]]) { - return; - } - - var currentPosts = PostStore.getVisiblePosts(currentSelected.posts[currentSelected.order[0]].channel_id); - - if (!currentPosts || currentPosts.order.length === 0) { - return; - } - - if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) { - for (var key in currentSelected.posts) { - if (currentSelected.posts.hasOwnProperty(key)) { - var post = currentSelected.posts[key]; - if (post.pending_post_id) { - Reflect.deleteProperty(currentSelected.posts, key); - } - } - } - - for (var postId in currentPosts.posts) { - if (currentPosts.posts.hasOwnProperty(postId)) { - currentSelected.posts[postId] = currentPosts.posts[postId]; - } - } - - PostStore.storeSelectedPost(currentSelected); - } - - var newState = this.getStateFromStores(); - if (this.mounted && !Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } + onUserChange() { + const profiles = JSON.parse(JSON.stringify(UserStore.getProfiles())); + this.setState({profiles}); } resize() { $('.post-right__scroll').scrollTop(100000); @@ -140,29 +107,22 @@ export default class RhsThread extends React.Component { } } render() { - var postList = this.state.postList; + const posts = this.state.posts; + const selected = this.state.selected; + const profiles = this.state.profiles || {}; - if (postList == null || !postList.order) { + if (posts == null || selected == null) { return ( <div></div> ); } - var selectedPost = postList.posts[postList.order[0]]; - var rootPost = null; - - if (selectedPost.root_id === '') { - rootPost = selectedPost; - } else { - rootPost = postList.posts[selectedPost.root_id]; - } - var postsArray = []; - for (var postId in postList.posts) { - if (postList.posts.hasOwnProperty(postId)) { - var cpost = postList.posts[postId]; - if (cpost.root_id === rootPost.id) { + for (const id in posts) { + if (posts.hasOwnProperty(id)) { + const cpost = posts[id]; + if (cpost.root_id === selected.id) { postsArray.push(cpost); } } @@ -199,6 +159,13 @@ export default class RhsThread extends React.Component { searchForm = <SearchBox/>; } + let profile; + if (UserStore.getCurrentId() === selected.user_id) { + profile = UserStore.getCurrentUser(); + } else { + profile = profiles[selected.user_id]; + } + return ( <div className='post-right__container'> <FileUploadOverlay overlayType='right'/> @@ -210,26 +177,33 @@ export default class RhsThread extends React.Component { /> <div className='post-right__scroll'> <RootPost - ref={rootPost.id} - post={rootPost} + ref={selected.id} + post={selected} commentCount={postsArray.length} + user={profile} /> <div className='post-right-comments-container'> {postsArray.map(function mapPosts(comPost) { + let p; + if (UserStore.getCurrentId() === selected.user_id) { + p = UserStore.getCurrentUser(); + } else { + p = profiles[comPost.user_id]; + } return ( <Comment ref={comPost.id} key={comPost.id + 'commentKey'} post={comPost} - selected={(comPost.id === selectedPost.id)} + user={p} /> ); })} </div> <div className='post-create__container'> <CreateComment - channelId={rootPost.channel_id} - rootId={rootPost.id} + channelId={selected.channel_id} + rootId={selected.id} /> </div> </div> diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index 1cdd09cc8..eaf8b5069 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -87,7 +87,7 @@ class SearchBar extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - results: null + postId: null }); } handleUserInput(text) { diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index d10c5be27..55ece2c97 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -40,12 +40,14 @@ export default class SearchResults extends React.Component { this.mounted = false; this.onChange = this.onChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.resize = this.resize.bind(this); this.handleResize = this.handleResize.bind(this); const state = getStateFromStores(); state.windowWidth = Utils.windowWidth(); state.windowHeight = Utils.windowHeight(); + state.profiles = JSON.parse(JSON.stringify(UserStore.getProfiles())); this.state = state; } @@ -53,6 +55,7 @@ export default class SearchResults extends React.Component { this.mounted = true; SearchStore.addSearchChangeListener(this.onChange); ChannelStore.addChangeListener(this.onChange); + UserStore.addChangeListener(this.onUserChange); this.resize(); window.addEventListener('resize', this.handleResize); } @@ -68,6 +71,7 @@ export default class SearchResults extends React.Component { componentWillUnmount() { SearchStore.removeSearchChangeListener(this.onChange); ChannelStore.removeChangeListener(this.onChange); + UserStore.removeChangeListener(this.onUserChange); this.mounted = false; window.removeEventListener('resize', this.handleResize); } @@ -85,6 +89,10 @@ export default class SearchResults extends React.Component { } } + onUserChange() { + this.setState({profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))}); + } + resize() { $('#search-items-container').scrollTop(0); if (this.state.windowWidth > 768) { @@ -101,6 +109,7 @@ export default class SearchResults extends React.Component { } var noResults = (!results || !results.order || !results.order.length); var searchTerm = SearchStore.getSearchTerm(); + const profiles = this.state.profiles || {}; var ctls = null; @@ -140,6 +149,7 @@ export default class SearchResults extends React.Component { key={post.id} channel={this.state.channels.get(post.channel_id)} post={post} + user={profiles[post.user_id]} term={searchTerm} isMentionSearch={this.props.isMentionSearch} /> diff --git a/web/react/components/search_results_header.jsx b/web/react/components/search_results_header.jsx index 7f88eb2c7..20fe342dc 100644 --- a/web/react/components/search_results_header.jsx +++ b/web/react/components/search_results_header.jsx @@ -32,7 +32,7 @@ export default class SearchResultsHeader extends React.Component { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_POST_SELECTED, - results: null + postId: null }); } diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 3363c97f7..05292b7b3 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -36,9 +36,10 @@ export default class SearchResultsItem extends React.Component { } render() { - var channelName = null; + let channelName = null; const channel = this.props.channel; - var timestamp = UserStore.getCurrentUser().update_at; + const timestamp = UserStore.getCurrentUser().update_at; + const user = this.props.user || {}; if (channel) { channelName = channel.display_name; @@ -84,7 +85,7 @@ export default class SearchResultsItem extends React.Component { </div> <div> <ul className='post__header'> - <li className='col__name'><strong><UserProfile userId={this.props.post.user_id}/></strong></li> + <li className='col__name'><strong><UserProfile user={user}/></strong></li> <li className='col'> <time className='search-item-time'> <FormattedDate @@ -135,6 +136,7 @@ export default class SearchResultsItem extends React.Component { SearchResultsItem.propTypes = { post: React.PropTypes.object, + user: React.PropTypes.object, channel: React.PropTypes.object, isMentionSearch: React.PropTypes.bool, term: React.PropTypes.string diff --git a/web/react/components/setting_item_min.jsx b/web/react/components/setting_item_min.jsx index 868b7e1b2..cb2ee0d8f 100644 --- a/web/react/components/setting_item_min.jsx +++ b/web/react/components/setting_item_min.jsx @@ -8,7 +8,7 @@ export default class SettingItemMin extends React.Component { let editButton = null; if (!this.props.disableOpen) { editButton = ( - <li className='col-sm-2 section-edit'> + <li className='col-sm-3 section-edit'> <a className='theme' href='#' @@ -29,9 +29,9 @@ export default class SettingItemMin extends React.Component { className='section-min' onClick={this.props.updateSection} > - <li className='col-sm-10 section-title'>{this.props.title}</li> + <li className='col-sm-9 section-title'>{this.props.title}</li> {editButton} - <li className='col-sm-7 section-describe'>{this.props.describe}</li> + <li className='col-sm-9 section-describe'>{this.props.describe}</li> </ul> ); } diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 740a7b166..dbec3d02d 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -351,12 +351,10 @@ class SignupUserComplete extends React.Component { <div> {signupMessage} <div className='or__container'> - <span> - <FormattedMessage - id='signup_user_completed.or' - defaultMessage='or' - /> - </span> + <FormattedMessage + id='signup_user_completed.or' + defaultMessage='or' + /> </div> </div> ); diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 1e393cfe9..31b2b9907 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import * as Utils from '../utils/utils.jsx'; -import UserStore from '../stores/user_store.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -19,45 +18,15 @@ function nextId() { export default class UserProfile extends React.Component { constructor(props) { super(props); - this.uniqueId = nextId(); - this.onChange = this.onChange.bind(this); - - this.state = this.getStateFromStores(this.props.userId); - } - getStateFromStores(userId) { - var profile = UserStore.getProfile(userId); - - if (profile == null) { - return {profile: {id: '0', username: '...'}}; - } - - return {profile}; } componentDidMount() { - UserStore.addChangeListener(this.onChange); if (!this.props.disablePopover) { $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); } } - componentWillUnmount() { - UserStore.removeChangeListener(this.onChange); - } - onChange(userId) { - if (!userId || userId === this.props.userId) { - var newState = this.getStateFromStores(this.props.userId); - if (!Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } - } - } - componentWillReceiveProps(nextProps) { - if (this.props.userId !== nextProps.userId) { - this.setState(this.getStateFromStores(nextProps.userId)); - } - } render() { - var name = Utils.displayUsername(this.state.profile.id); + var name = Utils.displayUsername(this.props.user.id); if (this.props.overwriteName) { name = this.props.overwriteName; } else if (!name) { @@ -68,7 +37,7 @@ export default class UserProfile extends React.Component { return <div>{name}</div>; } - var profileImg = '/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex(); + var profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at + '&' + Utils.getSessionIndex(); if (this.props.overwriteImage) { profileImg = this.props.overwriteImage; } @@ -100,14 +69,14 @@ export default class UserProfile extends React.Component { dataContent.push( <div data-toggle='tooltip' - title={this.state.profile.email} + title={this.props.user.email} key='user-popover-email' > <a - href={'mailto:' + this.state.profile.email} + href={'mailto:' + this.props.user.email} className='text-nowrap text-lowercase user-popover__email' > - {this.state.profile.email} + {this.props.user.email} </a> </div> ); @@ -139,13 +108,13 @@ export default class UserProfile extends React.Component { } UserProfile.defaultProps = { - userId: '', + user: {}, overwriteName: '', overwriteImage: '', disablePopover: false }; UserProfile.propTypes = { - userId: React.PropTypes.string, + user: React.PropTypes.object.isRequired, overwriteName: React.PropTypes.string, overwriteImage: React.PropTypes.string, disablePopover: React.PropTypes.bool diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index cd229775f..2f2116c2a 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -22,7 +22,7 @@ const holders = defineMessages({ }, usernameRestrictions: { id: 'user.settings.general.usernameRestrictions', - defaultMessage: "'Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." + defaultMessage: "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." }, validEmail: { id: 'user.settings.general.validEmail', diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 61fa91cf8..bc78c049c 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -8,7 +8,6 @@ import * as Client from '../utils/client.jsx'; import GetPostLinkModal from '../components/get_post_link_modal.jsx'; import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; -import RenameChannelModal from '../components/rename_channel_modal.jsx'; import EditPostModal from '../components/edit_post_modal.jsx'; import DeletePostModal from '../components/delete_post_modal.jsx'; import MoreChannelsModal from '../components/more_channels.jsx'; @@ -73,7 +72,6 @@ class Root extends React.Component { <InviteMemberModal/> <ImportThemeModal/> <TeamSettingsModal/> - <RenameChannelModal/> <MoreChannelsModal/> <EditPostModal/> <DeletePostModal/> diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index f5c342163..1dc0dc9bf 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -20,72 +20,7 @@ const SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; class PostStoreClass extends EventEmitter { constructor() { super(); - - this.emitChange = this.emitChange.bind(this); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - - this.emitEditPost = this.emitEditPost.bind(this); - this.addEditPostListener = this.addEditPostListener.bind(this); - this.removeEditPostListener = this.removeEditPostListner.bind(this); - - this.emitPostsViewJump = this.emitPostsViewJump.bind(this); - this.addPostsViewJumpListener = this.addPostsViewJumpListener.bind(this); - this.removePostsViewJumpListener = this.removePostsViewJumpListener.bind(this); - - this.emitPostFocused = this.emitPostFocused.bind(this); - this.addPostFocusedListener = this.addPostFocusedListener.bind(this); - this.removePostFocusedListener = this.removePostFocusedListener.bind(this); - - 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); - this.getVisiblePosts = this.getVisiblePosts.bind(this); - this.getVisibilityAtTop = this.getVisibilityAtTop.bind(this); - this.getVisibilityAtBottom = this.getVisibilityAtBottom.bind(this); - this.requestVisibilityIncrease = this.requestVisibilityIncrease.bind(this); - this.getFocusedPostId = this.getFocusedPostId.bind(this); - - this.storePosts = this.storePosts.bind(this); - this.storePost = this.storePost.bind(this); - this.storeFocusedPost = this.storeFocusedPost.bind(this); - this.checkBounds = this.checkBounds.bind(this); - - this.clearFocusedPost = this.clearFocusedPost.bind(this); - this.clearChannelVisibility = this.clearChannelVisibility.bind(this); - - this.deletePost = this.deletePost.bind(this); - this.removePost = this.removePost.bind(this); - - this.getPendingPosts = this.getPendingPosts.bind(this); - this.storePendingPost = this.storePendingPost.bind(this); - this.removePendingPost = this.removePendingPost.bind(this); - this.clearPendingPosts = this.clearPendingPosts.bind(this); - this.updatePendingPost = this.updatePendingPost.bind(this); - - // These functions are bad and work should be done to remove this system when the RHS dies - this.storeSelectedPost = this.storeSelectedPost.bind(this); - this.getSelectedPost = this.getSelectedPost.bind(this); - this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this); - this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this); - this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this); - this.selectedPost = null; - - this.getEmptyDraft = this.getEmptyDraft.bind(this); - this.storeCurrentDraft = this.storeCurrentDraft.bind(this); - this.getCurrentDraft = this.getCurrentDraft.bind(this); - this.storeDraft = this.storeDraft.bind(this); - this.getDraft = this.getDraft.bind(this); - this.storeCommentDraft = this.storeCommentDraft.bind(this); - this.getCommentDraft = this.getCommentDraft.bind(this); - this.clearDraftUploads = this.clearDraftUploads.bind(this); - this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this); - this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this); - this.getCommentCount = this.getCommentCount.bind(this); - + this.selectedPostId = null; this.postsInfo = {}; this.currentFocusedPostId = null; } @@ -330,6 +265,12 @@ class PostStoreClass extends EventEmitter { } deletePost(post) { + const postInfo = this.postsInfo[post.channel_id]; + if (!postInfo) { + // the post that has been deleted is in a channel that we haven't seen so just ignore it + return; + } + const postList = this.postsInfo[post.channel_id].postList; if (isPostListNull(postList)) { @@ -421,12 +362,59 @@ class PostStoreClass extends EventEmitter { this.emitChange(); } - storeSelectedPost(postList) { - this.selectedPost = postList; + storeSelectedPostId(postId) { + this.selectedPostId = postId; + } + + getSelectedPostId() { + return this.selectedPostId; } getSelectedPost() { - return this.selectedPost; + if (this.selectedPostId == null) { + return null; + } + + for (const k in this.postsInfo) { + if (this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) { + return this.postsInfo[k].postList.posts[this.selectedPostId]; + } + } + + return null; + } + + getSelectedPostThread() { + if (this.selectedPostId == null) { + return null; + } + + let posts; + let pendingPosts; + for (const k in this.postsInfo) { + if (this.postsInfo[k].postList.posts.hasOwnProperty(this.selectedPostId)) { + posts = this.postsInfo[k].postList.posts; + if (this.postsInfo[k].pendingPosts != null) { + pendingPosts = this.postsInfo[k].pendingPosts.posts; + } + } + } + + const threadPosts = {}; + const rootId = this.selectedPostId; + for (const k in posts) { + if (posts[k].root_id === rootId) { + threadPosts[k] = JSON.parse(JSON.stringify(posts[k])); + } + } + + for (const k in pendingPosts) { + if (pendingPosts[k].root_id === rootId) { + threadPosts[k] = JSON.parse(JSON.stringify(pendingPosts[k])); + } + } + + return threadPosts; } emitSelectedPostChange(fromSearch) { @@ -565,7 +553,7 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { PostStore.emitChange(); break; case ActionTypes.RECEIVED_POST_SELECTED: - PostStore.storeSelectedPost(action.post_list); + PostStore.storeSelectedPostId(action.postId); PostStore.emitSelectedPostChange(action.from_search); break; default: diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index dd60e166f..75a87d424 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -17,50 +17,6 @@ const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); - - this.emitChange = this.emitChange.bind(this); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - this.emitSessionsChange = this.emitSessionsChange.bind(this); - this.addSessionsChangeListener = this.addSessionsChangeListener.bind(this); - this.removeSessionsChangeListener = this.removeSessionsChangeListener.bind(this); - this.emitAuditsChange = this.emitAuditsChange.bind(this); - this.addAuditsChangeListener = this.addAuditsChangeListener.bind(this); - this.removeAuditsChangeListener = this.removeAuditsChangeListener.bind(this); - this.emitTeamsChange = this.emitTeamsChange.bind(this); - this.addTeamsChangeListener = this.addTeamsChangeListener.bind(this); - this.removeTeamsChangeListener = this.removeTeamsChangeListener.bind(this); - this.emitStatusesChange = this.emitStatusesChange.bind(this); - this.addStatusesChangeListener = this.addStatusesChangeListener.bind(this); - this.removeStatusesChangeListener = this.removeStatusesChangeListener.bind(this); - this.getCurrentId = this.getCurrentId.bind(this); - this.getCurrentUser = this.getCurrentUser.bind(this); - this.setCurrentUser = this.setCurrentUser.bind(this); - this.getLastEmail = this.getLastEmail.bind(this); - this.setLastEmail = this.setLastEmail.bind(this); - this.getLastUsername = this.getLastUsername.bind(this); - this.setLastUsername = this.setLastUsername.bind(this); - this.hasProfile = this.hasProfile.bind(this); - this.getProfile = this.getProfile.bind(this); - this.getProfileByUsername = this.getProfileByUsername.bind(this); - this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this); - this.getProfiles = this.getProfiles.bind(this); - this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this); - this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this); - this.saveProfile = this.saveProfile.bind(this); - this.setSessions = this.setSessions.bind(this); - this.getSessions = this.getSessions.bind(this); - this.setAudits = this.setAudits.bind(this); - this.getAudits = this.getAudits.bind(this); - this.setTeams = this.setTeams.bind(this); - this.getTeams = this.getTeams.bind(this); - this.getCurrentMentionKeys = this.getCurrentMentionKeys.bind(this); - this.setStatuses = this.setStatuses.bind(this); - this.pSetStatuses = this.pSetStatuses.bind(this); - this.setStatus = this.setStatus.bind(this); - this.getStatuses = this.getStatuses.bind(this); - this.getStatus = this.getStatus.bind(this); - this.profileCache = null; } @@ -277,7 +233,11 @@ class UserStoreClass extends EventEmitter { } getCurrentMentionKeys() { - var user = this.getCurrentUser(); + return this.getMentionKeys(this.getCurrentId()); + } + + getMentionKeys(id) { + var user = this.getProfile(id); var keys = []; @@ -330,7 +290,7 @@ class UserStoreClass extends EventEmitter { } var UserStore = new UserStoreClass(); -UserStore.setMaxListeners(0); +UserStore.setMaxListeners(15); UserStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx index 79e58147f..ed94f94b8 100644 --- a/web/react/utils/channel_intro_messages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -47,7 +47,7 @@ export function createDMIntroMessage(channel) { </div> <div className='channel-intro-profile'> <strong> - <UserProfile userId={teammate.id}/> + <UserProfile user={teammate}/> </strong> </div> <p className='channel-intro-text'> diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index a4d2515e2..38516fae2 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -19,7 +19,7 @@ import {FormattedTime} from 'mm-intl'; export function isEmail(email) { // writing a regex to match all valid email addresses is really, really hard (see http://stackoverflow.com/a/201378) // so we just do a simple check and rely on a verification email to tell if it's a real address - return email.indexOf('@') !== -1; + return (/^.+@.+$/).test(email); } export function cleanUpUrlable(input) { @@ -313,41 +313,17 @@ export function getTimestamp() { // extracts links not styled by Markdown export function extractLinks(text) { const links = []; - let replaceText = text; - - // pull out the Markdown code blocks - const codeBlocks = []; - const splitText = replaceText.split('`'); // also handles ``` - for (let i = 1; i < splitText.length; i += 2) { - if (splitText[i].trim() !== '') { - codeBlocks.push(splitText[i]); - } - } + let inText = text; + + // strip out code blocks + inText = inText.replace(/`[^`]*`/g, ''); + + // strip out inline markdown images + inText = inText.replace(/!\[[^\]]*\]\([^\)]*\)/g, ''); function replaceFn(autolinker, match) { let link = ''; const matchText = match.getMatchedText(); - const tempText = replaceText; - - const start = replaceText.indexOf(matchText); - const end = start + matchText.length; - - replaceText = replaceText.substring(0, start) + replaceText.substring(end); - - // if it's a Markdown link, just skip it - if (start > 1) { - if (tempText.charAt(start - 2) === ']' && tempText.charAt(start - 1) === '(' && tempText.charAt(end) === ')') { - return; - } - } - - // if it's in a Markdown code block, skip it - for (const i in codeBlocks) { - if (codeBlocks[i].indexOf(matchText) === 0) { - codeBlocks[i] = codeBlocks[i].replace(matchText, ''); - return; - } - } if (matchText.trim().indexOf('http') === 0) { link = matchText; @@ -358,16 +334,19 @@ export function extractLinks(text) { links.push(link); } - Autolinker.link(text, { - replaceFn, - urls: {schemeMatches: true, wwwMatches: true, tldMatches: false}, - emails: false, - twitter: false, - phone: false, - hashtag: false - }); + Autolinker.link( + inText, + { + replaceFn, + urls: {schemeMatches: true, wwwMatches: true, tldMatches: false}, + emails: false, + twitter: false, + phone: false, + hashtag: false + } + ); - return {links, text}; + return links; } export function escapeRegExp(string) { @@ -758,7 +737,7 @@ export function applyTheme(theme) { changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.command-name:hover, .mentions-name:hover, .suggestion--selected, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); - changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); + changeCss('code, .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); changeCss('@media(min-width: 960px){.post.current--user:hover .post__body ', 'background: none;', 1); changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); changeCss('.search-help-popover .search-autocomplete__item:hover, .settings-modal .settings-table .settings-content .appearance-section .theme-elements__body', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); @@ -1417,3 +1396,7 @@ export function languages() { export function isPostEphemeral(post) { return post.type === Constants.POST_TYPE_EPHEMERAL || post.state === Constants.POST_DELETED; } + +export function getRootId(post) { + return post.root_id === '' ? post.id : post.root_id; +} |