diff options
Diffstat (limited to 'webapp/components')
31 files changed, 420 insertions, 325 deletions
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx index e2fefc44e..e73d842d0 100644 --- a/webapp/components/about_build_modal.jsx +++ b/webapp/components/about_build_modal.jsx @@ -24,7 +24,7 @@ export default class AboutBuildModal extends React.Component { let title = ( <FormattedMessage id='about.teamEditiont0' - defaultMessage='Team Edition T0' + defaultMessage='Team Edition' /> ); @@ -33,14 +33,14 @@ export default class AboutBuildModal extends React.Component { title = ( <FormattedMessage id='about.teamEditiont1' - defaultMessage='Team Edition T1' + defaultMessage='Enterprise Edition' /> ); if (license.IsLicensed === 'true') { title = ( <FormattedMessage id='about.enterpriseEditione1' - defaultMessage='Enterprise Edition E1' + defaultMessage='Enterprise Edition' /> ); licensee = ( diff --git a/webapp/components/access_history_modal.jsx b/webapp/components/access_history_modal.jsx index 94a10c97f..9c49c3879 100644 --- a/webapp/components/access_history_modal.jsx +++ b/webapp/components/access_history_modal.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; import {Modal} from 'react-bootstrap'; import LoadingScreen from './loading_screen.jsx'; import AuditTable from './audit_table.jsx'; @@ -36,11 +35,8 @@ class AccessHistoryModal extends React.Component { } onShow() { AsyncClient.getAudits(); - - if ($(window).width() > 768) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } else { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150); + if (!Utils.isMobile()) { + $('.modal-body').perfectScrollbar(); } } onHide() { diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx index 9a4ff3ef2..f1dd4a26a 100644 --- a/webapp/components/activity_log_modal.jsx +++ b/webapp/components/activity_log_modal.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; import UserStore from 'stores/user_store.jsx'; import * as Client from 'utils/client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; @@ -56,11 +55,8 @@ export default class ActivityLogModal extends React.Component { } onShow() { AsyncClient.getSessions(); - - if ($(window).width() > 768) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } else { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150); + if (!Utils.isMobile()) { + $('.modal-body').perfectScrollbar(); } } onHide() { diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx index 5aa0dba7e..ad310d8e0 100644 --- a/webapp/components/admin_console/license_settings.jsx +++ b/webapp/components/admin_console/license_settings.jsx @@ -105,36 +105,27 @@ class LicenseSettings extends React.Component { let licenseType; let licenseKey; + const issued = Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true); + const startsAt = Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)); + const expiresAt = Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)); + if (global.window.mm_license.IsLicensed === 'true') { - edition = ( - <FormattedMessage - id='admin.license.enterpriseEdition' - defaultMessage='Mattermost Enterprise Edition. Designed for enterprise-scale communication.' - /> - ); + // Note: DO NOT LOCALISE THESE STRINGS. Legally we can not since the license is in English. + edition = 'Mattermost Enterprise Edition. Enterprise features on this server have been unlocked with a license key and a valid subscription.'; licenseType = ( - <FormattedHTMLMessage - id='admin.license.enterpriseType' - values={{ - terms: global.window.mm_config.TermsOfServiceLink, - name: global.window.mm_license.Name, - company: global.window.mm_license.Company, - users: global.window.mm_license.Users, - issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true), - start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)), - expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)), - ldap: global.window.mm_license.LDAP - }} - defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p> - <p>Your subscription details are as follows:</p> - Name: {name}<br /> - Company or organization name: {company}<br/> - Number of users: {users}<br/> - License issued: {issued}<br/> - Start date of license: {start}<br/> - Expiry date of license: {expires}<br/> - LDAP: {ldap}<br/></div>' - /> + <div> + <p> + {'This software is offered under a commercial license.\n\nSee ENTERPRISE-EDITION-LICENSE.txt in your root install directory for details. See NOTICE.txt for information about open source software used in this system.\n\nYour subscription details are as follows:'} + </p> + {`Name: ${global.window.mm_license.Name}`}<br/> + {`Company or organization name: ${global.window.mm_license.Company}`}<br/> + {`Number of users: ${global.window.mm_license.Users}`}<br/> + {`License issued: ${issued}`}<br/> + {`Start date of license: ${startsAt}`}<br/> + {`Expiry date of license: ${expiresAt}`}<br/> + <br/> + {'See also '}<a href='https://about.mattermost.com/enterprise-edition-terms/'>{'Enterprise Edition Terms of Service'}</a>{' and '}<a href='https://about.mattermost.com/privacy/'>{'Privacy Policy.'}</a> + </div> ); licenseKey = ( @@ -162,20 +153,15 @@ class LicenseSettings extends React.Component { </div> ); } else { + // Note: DO NOT LOCALISE THESE STRINGS. Legally we can not since the license is in English. edition = ( - <FormattedMessage - id='admin.license.teamEdition' - defaultMessage='Mattermost Team Edition. Designed for teams from 5 to 50 users.' - /> + <p> + {'Mattermost Enterprise Edition. Unlock enterprise features in this software through the purchase of a subscription from '} + <a href='https://mattermost.com/'>{'https://mattermost.com/'}</a> + </p> ); - licenseType = ( - <FormattedHTMLMessage - id='admin.license.teamType' - defaultMessage='<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p> - <p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>' - /> - ); + licenseType = 'This software is offered under a commercial license.\n\nSee ENTERPRISE-EDITION-LICENSE.txt in your root install directory for details. See NOTICE.txt for information about open source software used in this system.'; let fileName; if (this.state.fileName) { diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 7cd713942..369fa2dbb 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -80,6 +80,7 @@ export default class ChannelHeader extends React.Component { SearchStore.addSearchChangeListener(this.onListenerChange); PreferenceStore.addChangeListener(this.onListenerChange); UserStore.addChangeListener(this.onListenerChange); + $('.sidebar--left .dropdown-menu').perfectScrollbar(); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx new file mode 100644 index 000000000..e4af9f9ce --- /dev/null +++ b/webapp/components/channel_invite_button.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Client from 'utils/client.jsx'; + +import {FormattedMessage} from 'react-intl'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class ChannelInviteButton extends React.Component { + static get propTypes() { + return { + user: React.PropTypes.object.isRequired, + channel: React.PropTypes.object.isRequired, + onInviteError: React.PropTypes.func.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + + this.state = { + addingUser: false + }; + } + + handleClick() { + if (this.state.addingUser) { + return; + } + + this.setState({ + addingUser: true + }); + + const data = { + user_id: this.props.user.id + }; + + Client.addChannelMember( + this.props.channel.id, + data, + () => { + this.setState({ + addingUser: false + }); + + this.props.onInviteError(null); + AsyncClient.getChannelExtraInfo(); + }, + (err) => { + this.setState({ + addingUser: false + }); + + this.props.onInviteError(err); + } + ); + } + + render() { + return ( + <SpinnerButton + onClick={this.handleClick} + spinning={this.state.addingUser} + > + <i className='glyphicon glyphicon-envelope'/> + <FormattedMessage + id='channel_invite.add' + defaultMessage=' Add' + /> + </SpinnerButton> + ); + } +} diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx index dfb0d4187..c7c1906a5 100644 --- a/webapp/components/channel_invite_modal.jsx +++ b/webapp/components/channel_invite_modal.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import $ from 'jquery'; +import ChannelInviteButton from './channel_invite_button.jsx'; import FilteredUserList from './filtered_user_list.jsx'; import LoadingScreen from './loading_screen.jsx'; @@ -9,7 +10,6 @@ import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; -import * as Client from 'utils/client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import {FormattedMessage} from 'react-intl'; @@ -23,9 +23,8 @@ export default class ChannelInviteModal extends React.Component { super(props); this.onListenerChange = this.onListenerChange.bind(this); - this.handleInvite = this.handleInvite.bind(this); this.getStateFromStores = this.getStateFromStores.bind(this); - this.createInviteButton = this.createInviteButton.bind(this); + this.handleInviteError = this.handleInviteError.bind(this); this.state = this.getStateFromStores(); } @@ -120,36 +119,16 @@ export default class ChannelInviteModal extends React.Component { this.setState(newState); } } - handleInvite(user) { - const data = { - user_id: user.id - }; - - Client.addChannelMember( - this.props.channel.id, - data, - () => { - this.setState({inviteError: null}); - AsyncClient.getChannelExtraInfo(); - }, - (err) => { - this.setState({inviteError: err.message}); - } - ); - } - createInviteButton({user}) { - return ( - <a - onClick={this.handleInvite.bind(this, user)} - className='btn btn-sm btn-primary' - > - <i className='glyphicon glyphicon-envelope'/> - <FormattedMessage - id='channel_invite.add' - defaultMessage=' Add' - /> - </a> - ); + handleInviteError(err) { + if (err) { + this.setState({ + inviteError: err.message + }); + } else { + this.setState({ + inviteError: null + }); + } } render() { var inviteError = null; @@ -169,7 +148,11 @@ export default class ChannelInviteModal extends React.Component { <FilteredUserList style={{maxHeight}} users={this.state.nonmembers} - actions={[this.createInviteButton]} + actions={[ChannelInviteButton]} + actionProps={{ + channel: this.props.channel, + onInviteError: this.handleInviteError + }} /> ); } diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx index 0aeb70c57..177f282d3 100644 --- a/webapp/components/create_comment.jsx +++ b/webapp/components/create_comment.jsx @@ -6,7 +6,6 @@ import ReactDOM from 'react-dom'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Client from 'utils/client.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import SocketStore from 'stores/socket_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PostDeletedModal from './post_deleted_modal.jsx'; @@ -17,6 +16,7 @@ import MsgTyping from './msg_typing.jsx'; import FileUpload from './file_upload.jsx'; import FilePreview from './file_preview.jsx'; import * as Utils from 'utils/utils.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; import Constants from 'utils/constants.jsx'; @@ -196,11 +196,7 @@ class CreateComment extends React.Component { } } - const t = Date.now(); - if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { - SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}}); - this.lastTime = t; - } + GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId); } handleUserInput(messageText) { let draft = PostStore.getCommentDraft(this.props.rootId); diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index 36bfbf22d..e5e99debd 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -19,7 +19,6 @@ import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; -import SocketStore from 'stores/socket_store.jsx'; import Constants from 'utils/constants.jsx'; @@ -213,11 +212,7 @@ class CreatePost extends React.Component { } } - const t = Date.now(); - if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { - SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}}); - this.lastTime = t; - } + GlobalActions.emitLocalUserTypingEvent(this.state.channelId, ''); } handleUserInput(messageText) { this.setState({messageText}); diff --git a/webapp/components/filtered_user_list.jsx b/webapp/components/filtered_user_list.jsx index 1e4afd2be..bd6c49714 100644 --- a/webapp/components/filtered_user_list.jsx +++ b/webapp/components/filtered_user_list.jsx @@ -114,6 +114,7 @@ class FilteredUserList extends React.Component { <UserList users={users} actions={this.props.actions} + actionProps={this.props.actionProps} /> </div> </div> @@ -123,13 +124,15 @@ class FilteredUserList extends React.Component { FilteredUserList.defaultProps = { users: [], - actions: [] + actions: [], + actionProps: {} }; FilteredUserList.propTypes = { intl: intlShape.isRequired, users: React.PropTypes.arrayOf(React.PropTypes.object), actions: React.PropTypes.arrayOf(React.PropTypes.func), + actionProps: React.PropTypes.object, style: React.PropTypes.object }; diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx index d567183ac..1f8fd6133 100644 --- a/webapp/components/invite_member_modal.jsx +++ b/webapp/components/invite_member_modal.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -176,12 +175,6 @@ class InviteMemberModal extends React.Component { }); } - componentDidUpdate(prevProps, prevState) { - if (!prevState.show && this.state.show) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } - } - addInviteFields() { var count = this.state.idCount + 1; var inviteIds = this.state.inviteIds; diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index 6d35ff8c2..c6f7b50b1 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -5,12 +5,13 @@ import $ from 'jquery'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'action_creators/global_actions.jsx'; import UserStore from 'stores/user_store.jsx'; -import SocketStore from 'stores/socket_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import BrowserStore from 'stores/browser_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import ErrorBar from 'components/error_bar.jsx'; +import * as Websockets from 'action_creators/websocket_actions.jsx'; import {browserHistory} from 'react-router'; @@ -66,11 +67,6 @@ export default class LoggedIn extends React.Component { } } } - onSocketChange(msg) { - if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { - UserStore.setStatus(msg.user_id, 'online'); - } - } componentWillMount() { // Emit view action GlobalActions.viewLoggedIn(); @@ -78,8 +74,8 @@ export default class LoggedIn extends React.Component { // Listen for user UserStore.addChangeListener(this.onUserChanged); - // Add listner for socker store - SocketStore.addChangeListener(this.onSocketChange); + // Initalize websockets + Websockets.initialize(); // Get all statuses regularally. (Soon to be switched to websocket) this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL); @@ -89,7 +85,7 @@ export default class LoggedIn extends React.Component { // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) - if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { + if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { return; } @@ -99,7 +95,7 @@ export default class LoggedIn extends React.Component { if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) - if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { + if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { return; } @@ -178,7 +174,7 @@ export default class LoggedIn extends React.Component { $(window).off('focus'); $(window).off('blur'); - SocketStore.removeChangeListener(this.onSocketChange); + Websockets.close(); UserStore.removeChangeListener(this.onUserChanged); $('body').off('click.userpopover'); diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index 57cac7229..d1446059d 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -5,9 +5,9 @@ import {Modal} from 'react-bootstrap'; import FilteredUserList from './filtered_user_list.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; -import loadingGif from 'images/load.gif'; import {FormattedMessage} from 'react-intl'; +import SpinnerButton from 'components/spinner_button.jsx'; import React from 'react'; @@ -83,26 +83,16 @@ export default class MoreDirectChannels extends React.Component { } createJoinDirectChannelButton({user}) { - if (this.state.loadingDMChannel === user.id) { - return ( - <img - className='channel-loading-gif' - src={loadingGif} - /> - ); - } - return ( - <button - type='button' - className='btn btn-primary btn-message' + <SpinnerButton + spinning={this.state.loadingDMChannel === user.id} onClick={this.handleShowDirectChannel.bind(this, user)} > <FormattedMessage id='more_direct_channels.message' defaultMessage='Message' /> - </button> + </SpinnerButton> ); } diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx index b1781623c..b2d414287 100644 --- a/webapp/components/msg_typing.jsx +++ b/webapp/components/msg_typing.jsx @@ -1,21 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import SocketStore from 'stores/socket_store.jsx'; -import UserStore from 'stores/user_store.jsx'; +import UserTypingStore from 'stores/user_typing_store.jsx'; -import Constants from 'utils/constants.jsx'; - -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; - -const SocketEvents = Constants.SocketEvents; - -const holders = defineMessages({ - someone: { - id: 'msg_typing.someone', - defaultMessage: 'Someone' - } -}); +import {FormattedMessage} from 'react-intl'; import React from 'react'; @@ -23,69 +11,40 @@ class MsgTyping extends React.Component { constructor(props) { super(props); - this.onChange = this.onChange.bind(this); + this.onTypingChange = this.onTypingChange.bind(this); this.updateTypingText = this.updateTypingText.bind(this); this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this); - this.typingUsers = {}; this.state = { text: '' }; } - componentDidMount() { - SocketStore.addChangeListener(this.onChange); + componentWillMount() { + UserTypingStore.addChangeListener(this.onTypingChange); + this.onTypingChange(); + } + + componentWillUnmount() { + UserTypingStore.removeChangeListener(this.onTypingChange); } componentWillReceiveProps(nextProps) { if (this.props.channelId !== nextProps.channelId) { - for (const u in this.typingUsers) { - if (!this.typingUsers.hasOwnProperty(u)) { - continue; - } - - clearTimeout(this.typingUsers[u]); - } - this.typingUsers = {}; - this.setState({text: ''}); + this.updateTypingText(UserTypingStore.getUsersTyping(nextProps.channelId, nextProps.parentId)); } } - componentWillUnmount() { - SocketStore.removeChangeListener(this.onChange); + onTypingChange() { + this.updateTypingText(UserTypingStore.getUsersTyping(this.props.channelId, this.props.parentId)); } - onChange(msg) { - let username = this.props.intl.formatMessage(holders.someone); - if (msg.action === SocketEvents.TYPING && - this.props.channelId === msg.channel_id && - this.props.parentId === msg.props.parent_id) { - if (UserStore.hasProfile(msg.user_id)) { - username = UserStore.getProfile(msg.user_id).username; - } - - if (this.typingUsers[username]) { - clearTimeout(this.typingUsers[username]); - } - - this.typingUsers[username] = setTimeout(function myTimer(user) { - delete this.typingUsers[user]; - this.updateTypingText(); - }.bind(this, username), Constants.UPDATE_TYPING_MS); - - this.updateTypingText(); - } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) { - if (UserStore.hasProfile(msg.user_id)) { - username = UserStore.getProfile(msg.user_id).username; - } - clearTimeout(this.typingUsers[username]); - delete this.typingUsers[username]; - this.updateTypingText(); + updateTypingText(typingUsers) { + if (!typingUsers) { + return; } - } - updateTypingText() { - const users = Object.keys(this.typingUsers); + const users = Object.keys(typingUsers); let text = ''; switch (users.length) { case 0: @@ -129,9 +88,8 @@ class MsgTyping extends React.Component { } MsgTyping.propTypes = { - intl: intlShape.isRequired, channelId: React.PropTypes.string, parentId: React.PropTypes.string }; -export default injectIntl(MsgTyping);
\ No newline at end of file +export default MsgTyping; diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index fb3b25957..520f05ed0 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -8,6 +8,7 @@ import MessageWrapper from './message_wrapper.jsx'; import NotifyCounts from './notify_counts.jsx'; import ChannelInfoModal from './channel_info_modal.jsx'; 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'; @@ -433,6 +434,7 @@ export default class Navbar extends React.Component { var editChannelHeaderModal = null; var editChannelPurposeModal = null; let renameChannelModal = null; + let channelMembersModal = null; if (channel) { popoverContent = ( @@ -523,6 +525,14 @@ export default class Navbar extends React.Component { channel={channel} /> ); + + channelMembersModal = ( + <ChannelMembersModal + show={this.state.showMembersModal} + onModalDismissed={() => this.setState({showMembersModal: false})} + channel={channel} + /> + ); } var collapseButtons = this.createCollapseButtons(currentId); @@ -556,6 +566,7 @@ export default class Navbar extends React.Component { {editChannelHeaderModal} {editChannelPurposeModal} {renameChannelModal} + {channelMembersModal} </div> ); } diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index 7d048019c..819c7f590 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -1,6 +1,8 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; + import UserStore from 'stores/user_store.jsx'; import {Popover, Overlay} from 'react-bootstrap'; import * as Utils from 'utils/utils.jsx'; @@ -20,6 +22,10 @@ export default class PopoverListMembers extends React.Component { this.closePopover = this.closePopover.bind(this); } + componentDidUpdate() { + $('.member-list__popover .popover-content').perfectScrollbar(); + } + componentWillMount() { this.setState({showPopover: false}); } diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 29986d415..de99eb37d 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -5,7 +5,6 @@ import ReactDOM from 'react-dom'; import PostStore from 'stores/post_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserProfile from './user_profile.jsx'; -import UserStore from 'stores/user_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -97,8 +96,8 @@ class RhsComment extends React.Component { return ''; } - var isOwner = UserStore.getCurrentId() === post.user_id; - var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); + const isOwner = this.props.currentUser.id === post.user_id; + const isAdmin = Utils.isAdmin(this.props.currentUser.roles); var dropdownContents = []; @@ -193,11 +192,11 @@ class RhsComment extends React.Component { var post = this.props.post; var currentUserCss = ''; - if (UserStore.getCurrentId() === post.user_id) { + if (this.props.currentUser === post.user_id) { currentUserCss = 'current--user'; } - var timestamp = UserStore.getCurrentUser().update_at; + var timestamp = this.props.currentUser.update_at; let loading; let postClass = ''; @@ -305,7 +304,8 @@ RhsComment.defaultProps = { RhsComment.propTypes = { intl: intlShape.isRequired, post: React.PropTypes.object, - user: React.PropTypes.object + user: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired }; export default injectIntl(RhsComment); diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index a2c7ee7f8..1aa4a555f 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -55,8 +55,8 @@ export default class RhsRootPost extends React.Component { render() { const post = this.props.post; const user = this.props.user; - var isOwner = user.id === post.user_id; - var isAdmin = Utils.isAdmin(user.roles); + var isOwner = this.props.currentUser.id === post.user_id; + var isAdmin = Utils.isAdmin(this.props.currentUser.roles); var timestamp = UserStore.getProfile(post.user_id).update_at; var channel = ChannelStore.get(post.channel_id); @@ -286,5 +286,6 @@ RhsRootPost.defaultProps = { RhsRootPost.propTypes = { post: React.PropTypes.object.isRequired, user: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired, commentCount: React.PropTypes.number }; diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx index cc900f8e7..f0324d7ce 100644 --- a/webapp/components/rhs_thread.jsx +++ b/webapp/components/rhs_thread.jsx @@ -46,6 +46,9 @@ export default class RhsThread extends React.Component { window.addEventListener('resize', this.handleResize); this.mounted = true; + if (!Utils.isMobile()) { + $('.sidebar--right .post-right__scroll').perfectScrollbar(); + } } componentDidUpdate() { if ($('.post-right__scroll')[0]) { @@ -130,7 +133,7 @@ export default class RhsThread extends React.Component { } // sort failed posts to bottom, followed by pending, and then regular posts - postsArray.sort(function postSort(a, b) { + postsArray.sort((a, b) => { if ((a.state === Constants.POST_LOADING || a.state === Constants.POST_FAILED) && (b.state !== Constants.POST_LOADING && b.state !== Constants.POST_FAILED)) { return 1; } @@ -182,24 +185,26 @@ export default class RhsThread extends React.Component { post={selected} commentCount={postsArray.length} user={profile} + currentUser={this.props.currentUser} /> <div className='post-right-comments-container'> - {postsArray.map(function mapPosts(comPost) { - let p; - if (UserStore.getCurrentId() === comPost.user_id) { - p = UserStore.getCurrentUser(); - } else { - p = profiles[comPost.user_id]; - } - return ( - <Comment - ref={comPost.id} - key={comPost.id + 'commentKey'} - post={comPost} - user={p} - /> - ); - })} + {postsArray.map((comPost) => { + let p; + if (UserStore.getCurrentId() === comPost.user_id) { + p = UserStore.getCurrentUser(); + } else { + p = profiles[comPost.user_id]; + } + return ( + <Comment + ref={comPost.id} + key={comPost.id + 'commentKey'} + post={comPost} + user={p} + currentUser={this.props.currentUser} + /> + ); + })} </div> <div className='post-create__container'> <CreateComment @@ -221,5 +226,6 @@ RhsThread.defaultProps = { RhsThread.propTypes = { fromSearch: React.PropTypes.string, - isMentionSearch: React.PropTypes.bool + isMentionSearch: React.PropTypes.bool, + currentUser: React.PropTypes.object.isRequired }; diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx index 7619e41cd..c5baf50ef 100644 --- a/webapp/components/search_results.jsx +++ b/webapp/components/search_results.jsx @@ -61,6 +61,9 @@ export default class SearchResults extends React.Component { UserStore.addChangeListener(this.onUserChange); this.resize(); window.addEventListener('resize', this.handleResize); + if (!Utils.isMobile()) { + $('.sidebar--right .search-items-container').perfectScrollbar(); + } } shouldComponentUpdate(nextProps, nextState) { diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 49ae1bec6..0e1b7dd0e 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -93,12 +93,12 @@ export default class Sidebar extends React.Component { const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); const directChannels = []; - for (const preference of preferences) { - if (preference.value !== 'true') { + for (const [name, value] of preferences) { + if (value !== 'true') { continue; } - const teammateId = preference.name; + const teammateId = name; let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId)); @@ -163,6 +163,9 @@ export default class Sidebar extends React.Component { componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); + if (!Utils.isMobile()) { + $('.sidebar--left .nav-pills__container').perfectScrollbar(); + } } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); @@ -239,11 +242,10 @@ export default class Sidebar extends React.Component { if (!this.isLeaving.get(channel.id)) { this.isLeaving.set(channel.id, true); - const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false'); - - // bypass AsyncClient since we've already saved the updated preferences - Client.savePreferences( - [preference], + AsyncClient.savePreference( + Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + channel.teammate_id, + 'false', () => { this.isLeaving.set(channel.id, false); }, diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx index 1b3286963..a2e3914f3 100644 --- a/webapp/components/sidebar_right.jsx +++ b/webapp/components/sidebar_right.jsx @@ -8,6 +8,7 @@ import SearchResults from './search_results.jsx'; import RhsThread from './rhs_thread.jsx'; import SearchStore from 'stores/search_store.jsx'; import PostStore from 'stores/post_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; const SIDEBAR_SCROLL_DELAY = 500; @@ -22,33 +23,38 @@ export default class SidebarRight extends React.Component { this.onSelectedChange = this.onSelectedChange.bind(this); this.onSearchChange = this.onSearchChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.onShowSearch = this.onShowSearch.bind(this); this.doStrangeThings = this.doStrangeThings.bind(this); - this.state = this.getStateFromStores(); - } - getStateFromStores() { - return { - search_visible: SearchStore.getSearchResults() != null, - post_right_visible: PostStore.getSelectedPost() != null, - is_mention_search: SearchStore.getIsMentionSearch() + this.state = { + searchVisible: !!SearchStore.getSearchResults(), + isMentionSearch: SearchStore.getIsMentionSearch(), + postRightVisible: !!PostStore.getSelectedPost(), + fromSearch: false, + currentUser: UserStore.getCurrentUser() }; } componentDidMount() { SearchStore.addSearchChangeListener(this.onSearchChange); PostStore.addSelectedPostChangeListener(this.onSelectedChange); SearchStore.addShowSearchListener(this.onShowSearch); + UserStore.addChangeListener(this.onUserChange); this.doStrangeThings(); } componentWillUnmount() { SearchStore.removeSearchChangeListener(this.onSearchChange); PostStore.removeSelectedPostChangeListener(this.onSelectedChange); SearchStore.removeShowSearchListener(this.onShowSearch); + UserStore.removeChangeListener(this.onUserChange); + } + shouldComponentUpdate(nextProps, nextState) { + return !Utils.areObjectsEqual(nextState, this.state); } componentWillUpdate(nextProps, nextState) { - const isOpen = this.state.search_visible || this.state.post_right_visible; - const willOpen = nextState.search_visible || nextState.post_right_visible; + const isOpen = this.state.searchVisible || this.state.postRightVisible; + const willOpen = nextState.searchVisible || nextState.postRightVisible; if (!isOpen && willOpen) { setTimeout(() => PostStore.jumpPostsViewSidebarOpen(), SIDEBAR_SCROLL_DELAY); @@ -66,7 +72,7 @@ export default class SidebarRight extends React.Component { $('.sidebar--right').addClass('move--left'); //$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>'); - if (this.state.search_visible || this.state.post_right_visible) { + if (this.state.searchVisible || this.state.postRightVisible) { if (windowWidth > 960) { velocity($('.inner-wrap'), {marginRight: sidebarRightWidth}, {duration: 500, easing: 'easeOutSine'}); velocity($('.sidebar--right'), {translateX: 0}, {duration: 500, easing: 'easeOutSine'}); @@ -98,35 +104,40 @@ export default class SidebarRight extends React.Component { this.doStrangeThings(); } onSelectedChange(fromSearch) { - var newState = this.getStateFromStores(fromSearch); - newState.from_search = fromSearch; - if (!Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } + this.setState({ + postRightVisible: !!PostStore.getSelectedPost(), + fromSearch + }); } onSearchChange() { - var newState = this.getStateFromStores(); - if (!Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } + this.setState({ + searchVisible: !!SearchStore.getSearchResults(), + isMentionSearch: SearchStore.getIsMentionSearch() + }); + } + onUserChange() { + this.setState({ + currentUser: UserStore.getCurrentUser() + }); } onShowSearch() { - if (!this.state.search_visible) { + if (!this.state.searchVisible) { this.setState({ - search_visible: true + searchVisible: true }); } } render() { - var content = ''; + let content = null; - if (this.state.search_visible) { - content = <SearchResults isMentionSearch={this.state.is_mention_search}/>; - } else if (this.state.post_right_visible) { + if (this.state.searchVisible) { + content = <SearchResults isMentionSearch={this.state.isMentionSearch}/>; + } else if (this.state.postRightVisible) { content = ( <RhsThread - fromSearch={this.state.from_search} - isMentionSearch={this.state.is_mention_search} + fromSearch={this.state.fromSearch} + isMentionSearch={this.state.isMentionSearch} + currentUser={this.state.currentUser} /> ); } diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx new file mode 100644 index 000000000..fcc9af8cd --- /dev/null +++ b/webapp/components/spinner_button.jsx @@ -0,0 +1,48 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import loadingGif from 'images/load.gif'; + +export default class SpinnerButton extends React.Component { + static get propTypes() { + return { + children: React.PropTypes.node, + spinning: React.PropTypes.bool.isRequired, + onClick: React.PropTypes.func + }; + } + + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + } + + handleClick(e) { + if (this.props.onClick) { + this.props.onClick(e); + } + } + + render() { + if (this.props.spinning) { + return ( + <img + className='spinner-button__gif' + src={loadingGif} + /> + ); + } + + return ( + <button + onClick={this.handleClick} + className='btn btn-sm btn-primary' + > + {this.props.children} + </button> + ); + } +} diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx index 7dbbd680a..c19787993 100644 --- a/webapp/components/team_settings_modal.jsx +++ b/webapp/components/team_settings_modal.jsx @@ -5,6 +5,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import SettingsSidebar from './settings_sidebar.jsx'; import TeamSettings from './team_settings.jsx'; +import * as Utils from 'utils/utils.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; @@ -49,9 +50,16 @@ class TeamSettingsModal extends React.Component { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); }); + + if (!Utils.isMobile()) { + $('.settings-modal .settings-content').perfectScrollbar(); + } } updateTab(tab) { this.setState({activeTab: tab, activeSection: ''}); + if (!Utils.isMobile()) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); + } } updateSection(section) { this.setState({activeSection: section}); diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx index 5db45523e..734842cad 100644 --- a/webapp/components/tutorial/tutorial_intro_screens.jsx +++ b/webapp/components/tutorial/tutorial_intro_screens.jsx @@ -36,17 +36,22 @@ export default class TutorialIntroScreens extends React.Component { Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); - let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0); - const newValue = (parseInt(preference.value, 10) + 1).toString(); - - preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue); - AsyncClient.savePreferences([preference]); + AsyncClient.savePreference( + Preferences.TUTORIAL_STEP, + UserStore.getCurrentId(), + step + 1 + ); } skipTutorial(e) { e.preventDefault(); - const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999'); - AsyncClient.savePreferences([preference]); + + AsyncClient.savePreference( + Preferences.TUTORIAL_STEP, + UserStore.getCurrentId(), + 999 + ); } createScreen() { switch (this.state.currentScreen) { diff --git a/webapp/components/tutorial/tutorial_tip.jsx b/webapp/components/tutorial/tutorial_tip.jsx index ab49d4b04..d93fff1b1 100644 --- a/webapp/components/tutorial/tutorial_tip.jsx +++ b/webapp/components/tutorial/tutorial_tip.jsx @@ -29,12 +29,13 @@ export default class TutorialTip extends React.Component { this.setState({show}); if (!show && this.state.currentScreen >= this.props.screens.length - 1) { - let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0); - const newValue = (parseInt(preference.value, 10) + 1).toString(); - - preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue); - AsyncClient.savePreferences([preference]); + AsyncClient.savePreference( + Preferences.TUTORIAL_STEP, + UserStore.getCurrentId(), + step + 1 + ); } } handleNext() { diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx index 2140158e6..3652723be 100644 --- a/webapp/components/user_list.jsx +++ b/webapp/components/user_list.jsx @@ -18,16 +18,22 @@ export default class UserList extends React.Component { key={user.id} user={user} actions={this.props.actions} + actionProps={this.props.actionProps} /> ); }); } else { content = ( - <div key='no-users-found'> - <FormattedMessage - id='user_list.notFound' - defaultMessage='No users found :(' - /> + <div + key='no-users-found' + className='no-channel-message' + > + <p className='primary-message'> + <FormattedMessage + id='user_list.notFound' + defaultMessage='No users found :(' + /> + </p> </div> ); } @@ -42,10 +48,12 @@ export default class UserList extends React.Component { UserList.defaultProps = { users: [], - actions: [] + actions: [], + actionProps: {} }; UserList.propTypes = { users: React.PropTypes.arrayOf(React.PropTypes.object), - actions: React.PropTypes.arrayOf(React.PropTypes.func) + actions: React.PropTypes.arrayOf(React.PropTypes.func), + actionProps: React.PropTypes.object }; diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx index ed3a29a61..f6fd91688 100644 --- a/webapp/components/user_list_row.jsx +++ b/webapp/components/user_list_row.jsx @@ -6,7 +6,7 @@ import PreferenceStore from 'stores/preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; import React from 'react'; -export default function UserListRow({user, actions}) { +export default function UserListRow({user, actions, actionProps}) { const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', ''); let name = user.username; @@ -21,6 +21,7 @@ export default function UserListRow({user, actions}) { <Action key={index.toString()} user={user} + {...actionProps} /> ); }); @@ -56,10 +57,12 @@ export default function UserListRow({user, actions}) { } UserListRow.defaultProps = { - actions: [] + actions: [], + actionProps: {} }; UserListRow.propTypes = { user: React.PropTypes.object.isRequired, - actions: React.PropTypes.arrayOf(React.PropTypes.func) + actions: React.PropTypes.arrayOf(React.PropTypes.func), + actionProps: React.PropTypes.object }; diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx index 7c496f57b..4fcdc9a41 100644 --- a/webapp/components/user_settings/user_settings_advanced.jsx +++ b/webapp/components/user_settings/user_settings_advanced.jsx @@ -1,11 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import Constants from 'utils/constants.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; @@ -68,25 +69,27 @@ class AdvancedSettingsDisplay extends React.Component { const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures); const advancedSettings = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS); const settings = { - send_on_ctrl_enter: PreferenceStore.getPreference( + send_on_ctrl_enter: PreferenceStore.get( Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', - {value: 'false'} - ).value + 'false' + ) }; let enabledFeatures = 0; - advancedSettings.forEach((setting) => { - preReleaseFeaturesKeys.forEach((key) => { + for (const [name, value] of advancedSettings) { + for (const key of preReleaseFeaturesKeys) { const feature = PreReleaseFeatures[key]; - if (setting.name === Constants.FeatureTogglePrefix + feature.label) { - settings[setting.name] = setting.value; - if (setting.value === 'true') { - enabledFeatures++; + + if (name === Constants.FeatureTogglePrefix + feature.label) { + settings[name] = value; + + if (value === 'true') { + enabledFeatures += 1; } } - }); - }); + } + } this.state = {preReleaseFeatures: PreReleaseFeatures, settings, preReleaseFeaturesKeys, enabledFeatures}; } @@ -124,20 +127,21 @@ class AdvancedSettingsDisplay extends React.Component { handleSubmit(settings) { const preferences = []; + const userId = UserStore.getCurrentId(); + // this should be refactored so we can actually be certain about what type everything is (Array.isArray(settings) ? settings : [settings]).forEach((setting) => { - preferences.push( - PreferenceStore.setPreference( - Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, - setting, - String(this.state.settings[setting]) - ) - ); + preferences.push({ + user_id: userId, + category: Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, + name: setting, + value: this.state.settings[setting] + }); }); - Client.savePreferences(preferences, + AsyncClient.savePreferences( + preferences, () => { - PreferenceStore.emitChange(); this.updateSection(''); }, (err) => { diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx index 3299588f7..e56156049 100644 --- a/webapp/components/user_settings/user_settings_display.jsx +++ b/webapp/components/user_settings/user_settings_display.jsx @@ -6,24 +6,22 @@ import SettingItemMax from '../setting_item_max.jsx'; import ManageLanguages from './manage_languages.jsx'; import ThemeSetting from './user_settings_theme.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; import * as I18n from 'i18n/i18n.jsx'; import Constants from 'utils/constants.jsx'; +const Preferences = Constants.Preferences; -import {savePreferences} from 'utils/client.jsx'; import {FormattedMessage} from 'react-intl'; function getDisplayStateFromStores() { - const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'}); - const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'}); - const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT}); - return { - militaryTime: militaryTime.value, - nameFormat: nameFormat.value, - selectedFont: selectedFont.value + militaryTime: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', 'false'), + nameFormat: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'username'), + selectedFont: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT) }; } @@ -44,13 +42,29 @@ export default class UserSettingsDisplay extends React.Component { this.state = getDisplayStateFromStores(); } handleSubmit() { - const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime); - const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat); - const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont); + const userId = UserStore.getCurrentId(); + + const timePreference = { + user_id: userId, + category: Preferences.CATEGORY_DISPLAY_SETTINGS, + name: 'use_military_time', + value: this.state.militaryTime + }; + const namePreference = { + user_id: userId, + category: Preferences.CATEGORY_DISPLAY_SETTINGS, + name: 'name_format', + value: this.state.nameFormat + }; + const fontPreference = { + user_id: userId, + category: Preferences.CATEGORY_DISPLAY_SETTINGS, + name: 'selected_font', + value: this.state.selectedFont + }; - savePreferences([timePreference, namePreference, fontPreference], + AsyncClient.savePreferences([timePreference, namePreference, fontPreference], () => { - PreferenceStore.emitChange(); this.updateSection(''); }, (err) => { diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx index bd1df6ea5..d1c1f0fe2 100644 --- a/webapp/components/user_settings/user_settings_modal.jsx +++ b/webapp/components/user_settings/user_settings_modal.jsx @@ -64,7 +64,6 @@ class UserSettingsModal extends React.Component { constructor(props) { super(props); - this.handleShow = this.handleShow.bind(this); this.handleHide = this.handleHide.bind(this); this.handleHidden = this.handleHidden.bind(this); this.handleCollapse = this.handleCollapse.bind(this); @@ -95,24 +94,13 @@ class UserSettingsModal extends React.Component { } componentDidMount() { - if (this.props.show) { - this.handleShow(); - } UserStore.addChangeListener(this.onUserChanged); } - componentDidUpdate(prevProps) { - if (this.props.show && !prevProps.show) { - this.handleShow(); - } + componentDidUpdate() { UserStore.removeChangeListener(this.onUserChanged); - } - - handleShow() { - if ($(window).width() > 768) { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - } else { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50); + if (!Utils.isMobile()) { + $('.settings-modal .modal-body').perfectScrollbar(); } } @@ -222,6 +210,10 @@ class UserSettingsModal extends React.Component { active_section: '' }); } + + if (!Utils.isMobile()) { + $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update'); + } } updateSection(section, skipConfirm) { |