diff options
Diffstat (limited to 'web')
56 files changed, 614 insertions, 256 deletions
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx index 6b97287e4..5c22bf5cf 100644 --- a/web/react/components/admin_console/gitlab_settings.jsx +++ b/web/react/components/admin_console/gitlab_settings.jsx @@ -113,7 +113,15 @@ export default class GitLabSettings extends React.Component { /> {'false'} </label> - <p className='help-text'>{'When true, Mattermost allows team creation and account signup using GitLab OAuth. To configure, log in to your GitLab account and go to Applications -> Profile Settings. Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". Then use "Secret" and "Id" fields to complete the options below.'}</p> + <p className='help-text'> + {'When true, Mattermost allows team creation and account signup using GitLab OAuth.'} <br/> + </p> + <ol className='help-text'> + <li>{'Log in to your GitLab account and go to Applications -> Profile Settings.'}</li> + <li>{'Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". '}</li> + <li>{'Then use "Secret" and "Id" fields from GitLab to complete the options below.'}</li> + <li>{'Complete the Endpoint URLs below. '}</li> + </ol> </div> </div> @@ -179,7 +187,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Enable} /> - <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p> + <p className='help-text'>{'Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p> </div> </div> @@ -201,7 +209,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Enable} /> - <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p> + <p className='help-text'>{'Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p> </div> </div> @@ -223,7 +231,7 @@ export default class GitLabSettings extends React.Component { onChange={this.handleChange} disabled={!this.state.Enable} /> - <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URLs as appropriate.'}</p> + <p className='help-text'>{'Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'}</p> </div> </div> diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 665ccd719..395e22e6c 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -111,8 +111,10 @@ export default class UserItem extends React.Component { const user = this.props.user; let currentRoles = 'Member'; if (user.roles.length > 0) { - if (user.roles.indexOf('system_admin') > -1) { + if (Utils.isSystemAdmin(user.roles)) { currentRoles = 'System Admin'; + } else if (Utils.isAdmin(user.roles)) { + currentRoles = 'Team Admin'; } else { currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1); } @@ -158,7 +160,7 @@ export default class UserItem extends React.Component { href='#' onClick={this.handleMakeAdmin} > - {'Make Admin'} + {'Make Team Admin'} </a> </li> ); diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index d16069725..d0d6ab5e2 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -104,12 +104,6 @@ export default class ChannelLoader extends React.Component { } }); - /* Setup modal events */ - $('.modal').on('show.bs.modal', function onShow() { - $('.modal-body').css('overflow-y', 'auto'); - $('.modal-body').css('max-height', $(window).height() * 0.7); - }); - /* Prevent backspace from navigating back a page */ $(window).on('keydown.preventBackspace', (e) => { if (e.which === 8 && !$(e.target).is('input, textarea')) { diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index add4125d7..680d693f1 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -262,25 +262,27 @@ export default class CreateComment extends React.Component { id={this.props.rootId} className='post-create-body comment-create-body' > - <Textbox - onUserInput={this.handleUserInput} - onKeyPress={this.commentMsgKeyPress} - messageText={this.state.messageText} - createMessage='Add a comment...' - initialText='' - id='reply_textbox' - ref='textbox' - /> - <FileUpload - ref='fileUpload' - getFileCount={this.getFileCount} - onUploadStart={this.handleUploadStart} - onFileUpload={this.handleFileUploadComplete} - onUploadError={this.handleUploadError} - onTextDrop={this.handleTextDrop} - postType='comment' - channelId={this.props.channelId} - /> + <div className='post-body__cell'> + <Textbox + onUserInput={this.handleUserInput} + onKeyPress={this.commentMsgKeyPress} + messageText={this.state.messageText} + createMessage='Add a comment...' + initialText='' + id='reply_textbox' + ref='textbox' + /> + <FileUpload + ref='fileUpload' + getFileCount={this.getFileCount} + onUploadStart={this.handleUploadStart} + onFileUpload={this.handleFileUploadComplete} + onUploadError={this.handleUploadError} + onTextDrop={this.handleTextDrop} + postType='comment' + channelId={this.props.channelId} + /> + </div> </div> <MsgTyping channelId={this.props.channelId} diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index ed265ab02..f35079383 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -192,9 +192,12 @@ export default class CreatePost extends React.Component { PostStore.storeCurrentDraft(draft); } resizePostHolder() { - const height = $(window).height() - $(React.findDOMNode(this.refs.topDiv)).height() - $('#error_bar').outerHeight() - 50; + const height = $(window).height() - $(React.findDOMNode(this.refs.topDiv)).height() - 50; $('.post-list-holder-by-time').css('height', `${height}px`); $(window).trigger('resize'); + if ($(window).width() > 960) { + $('#post_textbox').focus(); + } } handleUploadStart(clientIds, channelId) { const draft = PostStore.getDraft(channelId); diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx index b100fd337..6311d9460 100644 --- a/web/react/components/error_bar.jsx +++ b/web/react/components/error_bar.jsx @@ -9,7 +9,6 @@ export default class ErrorBar extends React.Component { this.onErrorChange = this.onErrorChange.bind(this); this.handleClose = this.handleClose.bind(this); - this.resize = this.resize.bind(this); this.prevTimer = null; this.state = ErrorStore.getLastError(); @@ -46,34 +45,14 @@ export default class ErrorBar extends React.Component { return false; } - resize() { - if (this.isValidError(this.state)) { - var height = $(React.findDOMNode(this)).outerHeight(); - height = height < 30 ? 30 : height; - $('body').css('padding-top', height + 'px'); - } else { - $('body').css('padding-top', '0'); - } - } - componentDidMount() { ErrorStore.addChangeListener(this.onErrorChange); - - $(window).resize(() => { - this.resize(); - }); - - this.resize(); } componentWillUnmount() { ErrorStore.removeChangeListener(this.onErrorChange); } - componentDidUpdate() { - this.resize(); - } - onErrorChange() { var newState = ErrorStore.getLastError(); diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx index 59c4e08e3..9e3e3a683 100644 --- a/web/react/components/find_team.jsx +++ b/web/react/components/find_team.jsx @@ -70,6 +70,7 @@ export default class FindTeam extends React.Component { className='form-control' placeholder='you@domain.com' maxLength='128' + spellCheck='false' /> {emailError} </div> diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index 234013b93..eb6bfa9b6 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -96,7 +96,6 @@ export default class GetLinkModal extends React.Component { <p> Send teammates the link below for them to sign-up to this team site. <br /><br /> - Be careful not to share this link publicly, since anyone with the link can join your team. </p> <textarea className='form-control no-resize' diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 2ca39d1b1..c2f2c15ac 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -211,6 +211,7 @@ export default class InviteMemberModal extends React.Component { placeholder='First name' maxLength='64' disabled={!this.state.emailEnabled} + spellCheck='false' /> {firstNameError} </div> @@ -224,6 +225,7 @@ export default class InviteMemberModal extends React.Component { placeholder='Last name' maxLength='64' disabled={!this.state.emailEnabled} + spellCheck='false' /> {lastNameError} </div> @@ -242,6 +244,7 @@ export default class InviteMemberModal extends React.Component { placeholder='email@domain.com' maxLength='64' disabled={!this.state.emailEnabled} + spellCheck='false' /> {emailError} </div> diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 997abce68..f81822e1e 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -136,6 +136,7 @@ export default class Login extends React.Component { defaultValue={priorEmail} ref='email' placeholder='Email' + spellCheck='false' /> </div> <div className={'form-group' + errorClass}> @@ -146,6 +147,7 @@ export default class Login extends React.Component { name='password' ref='password' placeholder='Password' + spellCheck='false' /> </div> <div className='form-group'> diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 9a104aa32..3af1d3800 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -82,8 +82,10 @@ export default class MemberListTeamItem extends React.Component { const timestamp = UserStore.getCurrentUser().update_at; if (user.roles.length > 0) { - if (user.roles.indexOf('system_admin') > -1) { + if (Utils.isSystemAdmin(user.roles)) { currentRoles = 'System Admin'; + } else if (Utils.isAdmin(user.roles)) { + currentRoles = 'Team Admin'; } else { currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1); } @@ -112,7 +114,7 @@ export default class MemberListTeamItem extends React.Component { href='#' onClick={this.handleMakeAdmin} > - {'Make Admin'} + {'Make Team Admin'} </a> </li> ); diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 31ecb4c5d..bc610cd60 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -1,10 +1,11 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var ChannelStore = require('../stores/channel_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var Client = require('../utils/client.jsx'); +var Constants = require('../utils/constants.jsx'); var AsyncClient = require('../utils/async_client.jsx'); +var PreferenceStore = require('../stores/preference_store.jsx'); var utils = require('../utils/utils.jsx'); export default class MoreDirectChannels extends React.Component { @@ -15,27 +16,28 @@ export default class MoreDirectChannels extends React.Component { } componentDidMount() { - var self = this; - $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => { var button = e.relatedTarget; - self.setState({channels: $(button).data('channels')}); + this.setState({channels: $(button).data('channels')}); // eslint-disable-line react/no-did-mount-set-state }); } - render() { - var self = this; + handleJoinDirectChannel(channel) { + const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'true'); + AsyncClient.savePreferences([preference]); + } - var directMessageItems = this.state.channels.map(function mapActivityToChannel(channel, index) { + render() { + var directMessageItems = this.state.channels.map((channel, index) => { var badge = ''; var titleClass = ''; - var active = ''; var handleClick = null; if (channel.fake) { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = utils.getUserIdFromChannelName(channel); - if (self.state.loadingDMChannel === index) { + if (this.state.loadingDMChannel === index) { badge = ( <img className='channel-loading-gif pull-right' @@ -44,47 +46,42 @@ export default class MoreDirectChannels extends React.Component { ); } - if (self.state.loadingDMChannel === -1) { - handleClick = function clickHandler(e) { + if (this.state.loadingDMChannel === -1) { + handleClick = (e) => { e.preventDefault(); - self.setState({loadingDMChannel: index}); + this.setState({loadingDMChannel: index}); + this.handleJoinDirectChannel(channel); Client.createDirectChannel(channel, otherUserId, - function success(data) { - $(React.findDOMNode(self.refs.modal)).modal('hide'); - self.setState({loadingDMChannel: -1}); + (data) => { + $(React.findDOMNode(this.refs.modal)).modal('hide'); + this.setState({loadingDMChannel: -1}); AsyncClient.getChannel(data.id); utils.switchChannel(data); }, - function error() { - self.setState({loadingDMChannel: -1}); + () => { + this.setState({loadingDMChannel: -1}); window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; } ); }; } } else { - if (channel.id === ChannelStore.getCurrentId()) { - active = 'active'; - } - if (channel.unread) { badge = <span className='badge pull-right small'>{channel.unread}</span>; titleClass = 'unread-title'; } - handleClick = function clickHandler(e) { + handleClick = (e) => { e.preventDefault(); + this.handleJoinDirectChannel(channel); utils.switchChannel(channel); - $(React.findDOMNode(self.refs.modal)).modal('hide'); + $(React.findDOMNode(this.refs.modal)).modal('hide'); }; } return ( - <li - key={channel.name} - className={active} - > + <li key={channel.name}> <a className={'sidebar-channel ' + titleClass} href='#' @@ -111,10 +108,10 @@ export default class MoreDirectChannels extends React.Component { className='close' data-dismiss='modal' > - <span aria-hidden='true'>×</span> - <span className='sr-only'>Close</span> + <span aria-hidden='true'>{'×'}</span> + <span className='sr-only'>{'Close'}</span> </button> - <h4 className='modal-title'>More Direct Messages</h4> + <h4 className='modal-title'>{'More Direct Messages'}</h4> </div> <div className='modal-body'> <ul className='nav nav-pills nav-stacked'> @@ -126,7 +123,7 @@ export default class MoreDirectChannels extends React.Component { type='button' className='btn btn-default' data-dismiss='modal' - >Close</button> + >{'Close'}</button> </div> </div> </div> diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index b6defc393..49d517419 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -111,7 +111,7 @@ export default class NavbarDropdown extends React.Component { data-toggle='modal' data-target='#team_members' > - {'Manage Team'} + {'Manage Members'} </a> </li> ); diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx index 0ef187114..6112adbaf 100644 --- a/web/react/components/password_reset_form.jsx +++ b/web/react/components/password_reset_form.jsx @@ -69,6 +69,7 @@ export default class PasswordResetForm extends React.Component { name='password' ref='password' placeholder='Password' + spellCheck='false' /> </div> {error} diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx index 78fbcaa2f..f547499b0 100644 --- a/web/react/components/password_reset_send_link.jsx +++ b/web/react/components/password_reset_send_link.jsx @@ -73,6 +73,7 @@ export default class PasswordResetSendLink extends React.Component { name='email' ref='email' placeholder='Email' + spellCheck='false' /> </div> {error} diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 87962641f..a95095ff6 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -150,6 +150,8 @@ export default class PostInfo extends React.Component { <ul className='post-header post-info'> <li className='post-header-col'> <OverlayTrigger + delayShow='500' + container={this} placement='top' overlay={tooltip} > diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index d2cbc798e..4f0fe3ed0 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -32,11 +32,17 @@ export default class SettingItemMax extends React.Component { } var inputs = this.props.inputs; + var widthClass; + if (this.props.width === 'full') { + widthClass = 'col-sm-12'; + } else { + widthClass = 'col-sm-9 col-sm-offset-3'; + } return ( <ul className='section-max form-horizontal'> <li className='col-sm-12 section-title'>{this.props.title}</li> - <li className='col-sm-9 col-sm-offset-3'> + <li className={widthClass}> <ul className='setting-list'> <li className='setting-list-item'> {inputs} @@ -69,5 +75,6 @@ SettingItemMax.propTypes = { extraInfo: React.PropTypes.element, updateSection: React.PropTypes.func, submit: React.PropTypes.func, - title: React.PropTypes.string + title: React.PropTypes.string, + width: React.PropTypes.string }; diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index b5d2132d7..66568e1c8 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -10,7 +10,7 @@ export default class SettingsSidebar extends React.Component { handleClick(tab, e) { e.preventDefault(); this.props.updateTab(tab.name); - $('.settings-modal').addClass('display--content'); + $(e.target).closest('.settings-modal').addClass('display--content'); } render() { let tabList = this.props.tabs.map(function makeTab(tab) { diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 4ac1fd4a0..4911f17ef 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -1,19 +1,22 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var ChannelStore = require('../stores/channel_store.jsx'); -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var TeamStore = require('../stores/team_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx'); -var Utils = require('../utils/utils.jsx'); -var SidebarHeader = require('./sidebar_header.jsx'); -var SearchBox = require('./search_bar.jsx'); -var Constants = require('../utils/constants.jsx'); -var NewChannelFlow = require('./new_channel_flow.jsx'); -var UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const BrowserStore = require('../stores/browser_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const Client = require('../utils/client.jsx'); +const Constants = require('../utils/constants.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); +const NewChannelFlow = require('./new_channel_flow.jsx'); +const SearchBox = require('./search_bar.jsx'); +const SidebarHeader = require('./sidebar_header.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const TeamStore = require('../stores/team_store.jsx'); +const UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); +const Tooltip = ReactBootstrap.Tooltip; +const OverlayTrigger = ReactBootstrap.OverlayTrigger; export default class Sidebar extends React.Component { constructor(props) { @@ -23,12 +26,17 @@ export default class Sidebar extends React.Component { this.firstUnreadChannel = null; this.lastUnreadChannel = null; + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onChange = this.onChange.bind(this); this.onScroll = this.onScroll.bind(this); this.onResize = this.onResize.bind(this); this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); + this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this); this.createChannelElement = this.createChannelElement.bind(this); + this.isLeaving = new Map(); + const state = this.getStateFromStores(); state.modal = ''; state.loadingDMChannel = -1; @@ -36,7 +44,7 @@ export default class Sidebar extends React.Component { this.state = state; } getStateFromStores() { - var members = ChannelStore.getAllMembers(); + const members = ChannelStore.getAllMembers(); var teamMemberMap = UserStore.getActiveOnlyProfiles(); var currentId = ChannelStore.getCurrentId(); @@ -48,11 +56,13 @@ export default class Sidebar extends React.Component { teammates.push(teamMemberMap[id]); } + const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); + // Create lists of all read and unread direct channels - var showDirectChannels = []; - var readDirectChannels = []; + var visibleDirectChannels = []; + var hiddenDirectChannels = []; for (var i = 0; i < teammates.length; i++) { - var teammate = teammates[i]; + const teammate = teammates[i]; if (teammate.id === UserStore.getCurrentId()) { continue; @@ -65,90 +75,63 @@ export default class Sidebar extends React.Component { channelName = teammate.id + '__' + UserStore.getCurrentId(); } - var channel = ChannelStore.getByName(channelName); - - if (channel == null) { - var tempChannel = {}; - tempChannel.fake = true; - tempChannel.name = channelName; - tempChannel.display_name = teammate.username; - tempChannel.teammate_username = teammate.username; - tempChannel.status = UserStore.getStatus(teammate.id); - tempChannel.last_post_at = 0; - tempChannel.total_msg_count = 0; - tempChannel.type = 'D'; - readDirectChannels.push(tempChannel); - } else { - channel.display_name = teammate.username; - channel.teammate_username = teammate.username; + let forceShow = false; + let channel = ChannelStore.getByName(channelName); - channel.status = UserStore.getStatus(teammate.id); + if (channel) { + const member = members[channel.id]; + const msgCount = channel.total_msg_count - member.msg_count; - var channelMember = members[channel.id]; - var msgCount = channel.total_msg_count - channelMember.msg_count; - if (msgCount > 0) { - showDirectChannels.push(channel); - } else if (currentId === channel.id) { - showDirectChannels.push(channel); - } else { - readDirectChannels.push(channel); - } + // always show a channel if either it is the current one or if it is unread, but it is not currently being left + forceShow = (currentId === channel.id || msgCount > 0) && !this.isLeaving.get(channel.id); + } else { + channel = {}; + channel.fake = true; + channel.name = channelName; + channel.last_post_at = 0; + channel.total_msg_count = 0; + channel.type = 'D'; } - } - // If we don't have MAX_DMS unread channels, sort the read list by last_post_at - if (showDirectChannels.length < Constants.MAX_DMS) { - readDirectChannels.sort(function sortByLastPost(a, b) { - // sort by last_post_at first - if (a.last_post_at > b.last_post_at) { - return -1; - } - if (a.last_post_at < b.last_post_at) { - return 1; - } + channel.display_name = teammate.username; + channel.teammate_id = teammate.id; + channel.status = UserStore.getStatus(teammate.id); - // if last_post_at is equal, sort by name - if (a.display_name < b.display_name) { - return -1; - } - if (a.display_name > b.display_name) { - return 1; - } - return 0; - }); + if (preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false'))) { + visibleDirectChannels.push(channel); + } else if (forceShow) { + // make sure that unread direct channels are visible + const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true'); + AsyncClient.savePreferences([preference]); - var index = 0; - while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) { - showDirectChannels.push(readDirectChannels[index]); - index++; + visibleDirectChannels.push(channel); + } else { + hiddenDirectChannels.push(channel); } - readDirectChannels = readDirectChannels.slice(index); - - showDirectChannels.sort(function directSort(a, b) { - if (a.display_name < b.display_name) { - return -1; - } - if (a.display_name > b.display_name) { - return 1; - } - return 0; - }); } + visibleDirectChannels.sort(this.sortChannelsByDisplayName); + hiddenDirectChannels.sort(this.sortChannelsByDisplayName); + return { activeId: currentId, channels: ChannelStore.getAll(), - members: members, - showDirectChannels: showDirectChannels, - hideDirectChannels: readDirectChannels + members, + visibleDirectChannels, + hiddenDirectChannels }; } + componentDidMount() { ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange); SocketStore.addChangeListener(this.onSocketChange); + PreferenceStore.addChangeListener(this.onChange); + + AsyncClient.getDirectChannelPreferences(); + $('.nav-pills__container').perfectScrollbar(); this.updateTitle(); @@ -178,6 +161,7 @@ export default class Sidebar extends React.Component { UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); SocketStore.removeChangeListener(this.onSocketChange); + PreferenceStore.removeChangeListener(this.onChange); } onChange() { var newState = this.getStateFromStores(); @@ -322,7 +306,37 @@ export default class Sidebar extends React.Component { showBottomUnread }); } - createChannelElement(channel, index) { + + handleLeaveDirectChannel(channel) { + 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], + () => { + this.isLeaving.set(channel.id, false); + }, + () => { + this.isLeaving.set(channel.id, false); + } + ); + + this.setState(this.getStateFromStores()); + } + + if (channel.id === this.state.activeId) { + Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); + } + } + + sortChannelsByDisplayName(a, b) { + return a.display_name.localeCompare(b.display_name); + } + + createChannelElement(channel, index, arr, handleClose) { var members = this.state.members; var activeId = this.state.activeId; var channelMember = members[channel.id]; @@ -333,15 +347,16 @@ export default class Sidebar extends React.Component { linkClass = 'active'; } + let rowClass = 'sidebar-channel'; + var unread = false; if (channelMember) { msgCount = channel.total_msg_count - channelMember.msg_count; unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0; } - var titleClass = ''; if (unread) { - titleClass = 'unread-title'; + rowClass += ' unread-title'; if (channel.id !== activeId) { if (!this.firstUnreadChannel) { @@ -374,9 +389,8 @@ export default class Sidebar extends React.Component { ); } - var badgeClass; if (msgCount > 0) { - badgeClass = 'has-badge'; + rowClass += ' has-badge'; } // set up status icon for direct message channels @@ -405,8 +419,13 @@ export default class Sidebar extends React.Component { if (!channel.fake) { handleClick = function clickHandler(e) { + if (e.target.attributes.getNamedItem('data-close')) { + handleClose(channel); + } else { + Utils.switchChannel(channel); + } + e.preventDefault(); - Utils.switchChannel(channel); }; } else if (channel.fake && teamURL) { // It's a direct message channel that doesn't exist yet so let's create it now @@ -415,23 +434,49 @@ export default class Sidebar extends React.Component { if (this.state.loadingDMChannel === -1) { handleClick = function clickHandler(e) { e.preventDefault(); - this.setState({loadingDMChannel: index}); - - Client.createDirectChannel(channel, otherUserId, - function success(data) { - this.setState({loadingDMChannel: -1}); - AsyncClient.getChannel(data.id); - Utils.switchChannel(data); - }.bind(this), - function error() { - this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; - }.bind(this) - ); + + if (e.target.attributes.getNamedItem('data-close')) { + handleClose(channel); + } else { + this.setState({loadingDMChannel: index}); + + Client.createDirectChannel(channel, otherUserId, + (data) => { + this.setState({loadingDMChannel: -1}); + AsyncClient.getChannel(data.id); + Utils.switchChannel(data); + }, + () => { + this.setState({loadingDMChannel: -1}); + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + } + ); + } }.bind(this); } } + let closeButton = null; + const removeTooltip = <Tooltip>{'Remove from list'}</Tooltip>; + if (handleClose && !badge) { + closeButton = ( + <OverlayTrigger + delayShow='1000' + placement='top' + overlay={removeTooltip} + > + <span + className='btn-close' + data-close='true' + > + {'×'} + </span> + </OverlayTrigger> + ); + + rowClass += ' has-close'; + } + return ( <li key={channel.name} @@ -439,13 +484,14 @@ export default class Sidebar extends React.Component { className={linkClass} > <a - className={'sidebar-channel ' + titleClass + ' ' + badgeClass} + className={rowClass} href={href} onClick={handleClick} > {status} {channel.display_name} {badge} + {closeButton} </a> </li> ); @@ -464,7 +510,9 @@ export default class Sidebar extends React.Component { const privateChannels = this.state.channels.filter((channel) => channel.type === 'P'); const privateChannelItems = privateChannels.map(this.createChannelElement); - const directMessageItems = this.state.showDirectChannels.map(this.createChannelElement); + const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => { + return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); + }); // update the favicon to show if there are any notifications var link = document.createElement('link'); @@ -484,17 +532,18 @@ export default class Sidebar extends React.Component { head.appendChild(link); var directMessageMore = null; - if (this.state.hideDirectChannels.length > 0) { + if (this.state.hiddenDirectChannels.length > 0) { directMessageMore = ( - <li> + <li key='more'> <a + key={`more${this.state.hiddenDirectChannels.length}`} href='#' data-toggle='modal' className='nav-more' data-target='#more_direct_channels' - data-channels={JSON.stringify(this.state.hideDirectChannels)} + data-channels={JSON.stringify(this.state.hiddenDirectChannels)} > - {'More (' + this.state.hideDirectChannels.length + ')'} + {'More (' + this.state.hiddenDirectChannels.length + ')'} </a> </li> ); @@ -505,6 +554,9 @@ export default class Sidebar extends React.Component { showChannelModal = true; } + const createChannelTootlip = <Tooltip>{'Create new channel'}</Tooltip>; + const createGroupTootlip = <Tooltip>{'Create new group'}</Tooltip>; + return ( <div> <NewChannelFlow @@ -538,7 +590,12 @@ export default class Sidebar extends React.Component { <ul className='nav nav-pills nav-stacked'> <li> <h4> - Channels + {'Channels'} + <OverlayTrigger + delayShow='500' + placement='top' + overlay={createChannelTootlip} + > <a className='add-channel-btn' href='#' @@ -546,6 +603,7 @@ export default class Sidebar extends React.Component { > {'+'} </a> + </OverlayTrigger> </h4> </li> {publicChannelItems} @@ -557,7 +615,7 @@ export default class Sidebar extends React.Component { data-target='#more_channels' data-channeltype='O' > - More... + {'More...'} </a> </li> </ul> @@ -565,7 +623,12 @@ export default class Sidebar extends React.Component { <ul className='nav nav-pills nav-stacked'> <li> <h4> - Private Groups + {'Private Groups'} + <OverlayTrigger + delayShow='500' + placement='top' + overlay={createGroupTootlip} + > <a className='add-channel-btn' href='#' @@ -573,12 +636,13 @@ export default class Sidebar extends React.Component { > {'+'} </a> + </OverlayTrigger> </h4> </li> {privateChannelItems} </ul> <ul className='nav nav-pills nav-stacked'> - <li><h4>Direct Messages</h4></li> + <li><h4>{'Direct Messages'}</h4></li> {directMessageItems} {directMessageMore} </ul> diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index ea2bcf9a4..ac101d631 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -75,7 +75,7 @@ export default class SidebarRightMenu extends React.Component { data-toggle='modal' data-target='#team_members' > - <i className='glyphicon glyphicon-wrench'></i>Manage Team</a> + <i className='glyphicon glyphicon-wrench'></i>Manage Members</a> </li> ); } diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 75661f812..98f862e69 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -149,7 +149,7 @@ export default class SignupUserComplete extends React.Component { // set up the email entry and hide it if an email was provided var yourEmailIs = ''; if (this.state.user.email) { - yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {global.window.config.SiteName}.</span>; + yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.config.SiteName}.</span>; } var emailContainerStyle = 'margin--extra'; @@ -169,6 +169,7 @@ export default class SignupUserComplete extends React.Component { placeholder='' maxLength='128' autoFocus={true} + spellCheck='false' /> {emailError} </div> @@ -204,9 +205,10 @@ export default class SignupUserComplete extends React.Component { className='form-control' placeholder='' maxLength='128' + spellCheck='false' /> {nameError} - <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p> + <span className='help-block'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</span> </div> </div> <div className='margin--extra'> @@ -218,6 +220,7 @@ export default class SignupUserComplete extends React.Component { className='form-control' placeholder='' maxLength='128' + spellCheck='false' /> {passwordError} </div> diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx index 40f06c382..a80b1a472 100644 --- a/web/react/components/team_import_tab.jsx +++ b/web/react/components/team_import_tab.jsx @@ -34,14 +34,14 @@ export default class TeamImportTab extends React.Component { render() { var uploadHelpText = ( <div> - <p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p> - <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p> + <p>{'To import a team from Slack go to Slack > Team Settings > Import/Export Data > Export > Start Export. Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p> + <p>{'The Slack import to Mattermost is in "Beta". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p> </div> ); var uploadSection = ( <SettingUpload - title='Import from Slack' + title='Import from Slack (Beta)' submit={this.doImportSlack} helpText={uploadHelpText} fileTypesAccepted='.zip' diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx index 65da4bc96..c91ed0811 100644 --- a/web/react/components/team_signup_display_name_page.jsx +++ b/web/react/components/team_signup_display_name_page.jsx @@ -66,6 +66,7 @@ export default class TeamSignupDisplayNamePage extends React.Component { defaultValue={this.props.state.team.display_name} autoFocus={true} onFocus={this.handleFocus} + spellCheck='false' /> </div> </div> diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx index 219f14eef..7253e80e9 100644 --- a/web/react/components/team_signup_email_item.jsx +++ b/web/react/components/team_signup_email_item.jsx @@ -51,6 +51,7 @@ export default class TeamSignupEmailItem extends React.Component { placeholder='Email Address' defaultValue={this.props.email} maxLength='128' + spellCheck='false' /> {emailError} </div> diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index dce8105ca..cb9a9f05b 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -109,8 +109,9 @@ export default class TeamSignupPasswordPage extends React.Component { className='form-control' placeholder='' maxLength='128' + spellCheck='false' /> - <div className='color--light form__hint'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div> + <span className='color--light help-block'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</span> </div> </div> {passwordError} diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx index 398a52f7d..3fb0aaa27 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/team_signup_url_page.jsx @@ -111,6 +111,7 @@ export default class TeamSignupUrlPage extends React.Component { defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus} + spellCheck='false' /> </div> </div> diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx index 53a389c4c..82dabad3d 100644 --- a/web/react/components/team_signup_username_page.jsx +++ b/web/react/components/team_signup_username_page.jsx @@ -68,8 +68,9 @@ export default class TeamSignupUsernamePage extends React.Component { placeholder='' defaultValue={this.props.state.user.username} maxLength='128' + spellCheck='false' /> - <div className='color--light form__hint'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</div> + <span className='color--light help-block'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</span> </div> </div> {nameError} diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index 78a41eed5..2d7ef081f 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -145,6 +145,7 @@ export default class TeamSignupWelcomePage extends React.Component { className='form-control' placeholder='Email Address' maxLength='128' + spellCheck='false' /> </div> </div> diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx index 9376a4564..ba32a9f97 100644 --- a/web/react/components/team_signup_with_email.jsx +++ b/web/react/components/team_signup_with_email.jsx @@ -63,6 +63,7 @@ export default class EmailSignUpPage extends React.Component { className='form-control' placeholder='Email Address' maxLength='128' + spellCheck='false' /> </div> <div className='form-group'> diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx index 011bfebff..6ccf762c1 100644 --- a/web/react/components/team_signup_with_sso.jsx +++ b/web/react/components/team_signup_with_sso.jsx @@ -104,6 +104,7 @@ export default class SSOSignUpPage extends React.Component { placeholder='Enter name of new team' maxLength='128' onChange={this.nameChange} + spellCheck='false' /> {nameError} </div> diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index 0563c294a..741dbcd5d 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -246,9 +246,11 @@ export default class Textbox extends React.Component { if (e.scrollHeight - mod < 167) { $(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod); $(w).css({height: 'auto'}).height(e.scrollHeight + 2); + $(w).closest('.post-body__cell').removeClass('scroll'); } else { $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167); $(w).css({height: 'auto'}).height(167); + $(w).closest('.post-body__cell').addClass('scroll'); } if (prevHeight !== $(e).height() && this.props.onHeightChange) { diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 948f06444..cc6165c1b 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -32,7 +32,7 @@ export default class UserProfile extends React.Component { componentDidMount() { UserStore.addChangeListener(this.onChange); if (!this.props.disablePopover) { - $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}}); + $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'click hover', html: true, delay: {show: 200, hide: 100}}); $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); } } diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index 6d64e83b6..a16440d55 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -178,7 +178,7 @@ export default class UserSettingsAppearance extends React.Component { href='#' onClick={this.submitTheme} > - {'Submit'} + {'Save'} </a> <a className='btn btn-sm theme' diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index ac3a2e37b..0e872315d 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -368,8 +368,7 @@ export default class UserSettingsGeneralTab extends React.Component { const extraInfo = ( <span> - {'Use Nickname for a name you might be called that is different from your first name and user name.'} - {'This is most often used when two or more people have similar sounding names and usernames.'} + {'Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.'} </span> ); diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 5e20d41f1..3be062ad3 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -38,6 +38,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMax title='Incoming Webhooks' + width = 'full' inputs={inputs} updateSection={function clearSection(e) { this.updateSection(''); @@ -49,6 +50,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMin title='Incoming Webhooks' + width = 'full' describe='Manage your incoming webhooks (Developer feature)' updateSection={function updateNameSection() { this.updateSection('incoming-hooks'); diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 8ef68dd0a..c5f0abc12 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -195,6 +195,7 @@ export default class ViewImageModal extends React.Component { target='_blank' > <img + style={{maxHeight: this.state.imgHeight}} ref='image' src={this.getPreviewImagePath(filename)} /> @@ -210,6 +211,7 @@ export default class ViewImageModal extends React.Component { content = ( <video + style={{maxHeight: this.state.imgHeight}} ref='video' data-setup='{}' controls='controls' @@ -334,7 +336,6 @@ export default class ViewImageModal extends React.Component { > <div className={'image-wrapper ' + bgClass} - style={{maxHeight: this.state.imgHeight}} onMouseEnter={this.onMouseEnterImage} onMouseLeave={this.onMouseLeaveImage} onClick={(e) => e.stopPropagation()} diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx new file mode 100644 index 000000000..d71efa10f --- /dev/null +++ b/web/react/stores/preference_store.jsx @@ -0,0 +1,122 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const ActionTypes = require('../utils/constants.jsx').ActionTypes; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const BrowserStore = require('./browser_store.jsx'); +const EventEmitter = require('events').EventEmitter; +const UserStore = require('../stores/user_store.jsx'); + +const CHANGE_EVENT = 'change'; + +function getPreferenceKey(category, name) { + return `${category}-${name}`; +} + +function getPreferenceKeyForModel(preference) { + return `${preference.category}-${preference.name}`; +} + +class PreferenceStoreClass extends EventEmitter { + constructor() { + super(); + + this.getAllPreferences = this.getAllPreferences.bind(this); + this.getPreference = this.getPreference.bind(this); + this.getPreferences = this.getPreferences.bind(this); + this.getPreferencesWhere = this.getPreferencesWhere.bind(this); + this.setAllPreferences = this.setAllPreferences.bind(this); + this.setPreference = this.setPreference.bind(this); + + this.emitChange = this.emitChange.bind(this); + this.addChangeListener = this.addChangeListener.bind(this); + this.removeChangeListener = this.removeChangeListener.bind(this); + + this.handleEventPayload = this.handleEventPayload.bind(this); + this.dispatchToken = AppDispatcher.register(this.handleEventPayload); + } + + getAllPreferences() { + return new Map(BrowserStore.getItem('preferences', [])); + } + + getPreference(category, name, defaultValue = '') { + return this.getAllPreferences().get(getPreferenceKey(category, name)) || defaultValue; + } + + getPreferences(category) { + return this.getPreferencesWhere((preference) => (preference.category === category)); + } + + getPreferencesWhere(pred) { + const all = this.getAllPreferences(); + const preferences = []; + + for (const [, preference] of all) { + if (pred(preference)) { + preferences.push(preference); + } + } + + return preferences; + } + + setAllPreferences(preferences) { + // note that we store the preferences as an array of key-value pairs so that we can deserialize + // it as a proper Map instead of an object + BrowserStore.setItem('preferences', [...preferences]); + } + + setPreference(category, name, value) { + const preferences = this.getAllPreferences(); + + const key = getPreferenceKey(category, name); + let preference = preferences.get(key); + + if (!preference) { + preference = { + user_id: UserStore.getCurrentId(), + category, + name + }; + } + preference.value = value; + + preferences.set(key, preference); + + this.setAllPreferences(preferences); + + return preference; + } + + emitChange(preferences) { + this.emit(CHANGE_EVENT, preferences); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECIEVED_PREFERENCES: + const preferences = this.getAllPreferences(); + + for (const preference of action.preferences) { + preferences.set(getPreferenceKeyForModel(preference), preference); + } + + this.setAllPreferences(preferences); + this.emitChange(preferences); + } + } +} + +const PreferenceStore = new PreferenceStoreClass(); +export default PreferenceStore; diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index a903f055b..1bf8a6fee 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -637,3 +637,55 @@ export function getMyTeam() { } ); } + +export function getDirectChannelPreferences() { + if (isCallInProgress('getDirectChannelPreferences')) { + return; + } + + callTracker.getDirectChannelPreferences = utils.getTimestamp(); + client.getPreferenceCategory( + Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, + (data, textStatus, xhr) => { + callTracker.getDirectChannelPreferences = 0; + + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_PREFERENCES, + preferences: data + }); + }, + (err) => { + callTracker.getDirectChannelPreferences = 0; + dispatchError(err, 'getDirectChannelPreferences'); + } + ); +} + +export function savePreferences(preferences, success, error) { + client.savePreferences( + preferences, + (data, textStatus, xhr) => { + if (xhr.status !== 304) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_PREFERENCES, + preferences + }); + } + + if (success) { + success(data); + } + }, + (err) => { + dispatchError(err, 'savePreferences'); + + if (error) { + error(); + } + } + ); +} diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 6dccfcdeb..76a402855 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1141,3 +1141,31 @@ export function listIncomingHooks(success, error) { } }); } + +export function getPreferenceCategory(category, success, error) { + $.ajax({ + url: `/api/v1/preferences/${category}`, + dataType: 'json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getPreferenceCategory', xhr, status, err); + error(e); + } + }); +} + +export function savePreferences(preferences, success, error) { + $.ajax({ + url: '/api/v1/preferences/save', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(preferences), + success, + error: (xhr, status, err) => { + var e = handleError('savePreferences', xhr, status, err); + error(e); + } + }); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index affc49196..cee2ec114 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -28,6 +28,7 @@ module.exports = { RECIEVED_AUDITS: null, RECIEVED_TEAMS: null, RECIEVED_STATUSES: null, + RECIEVED_PREFERENCES: null, RECIEVED_MSG: null, @@ -201,7 +202,7 @@ module.exports = { centerChannelBg: '#1F1F1F', centerChannelColor: '#DDDDDD', newMessageSeparator: '#CC992D', - linkColor: '#0177e7', + linkColor: '#0D93FF', buttonBg: '#0177e7', buttonColor: '#FFFFFF', mentionHighlightBg: '#784098', @@ -285,5 +286,8 @@ module.exports = { id: 'mentionHighlightLink', uiName: 'Mention Highlight Link' } - ] + ], + Preferences: { + CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show' + } }; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index f9166063e..8f697a9c5 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -423,7 +423,10 @@ export function applyTheme(theme) { if (theme.sidebarTextActiveColor) { changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); - changeCss('.sidebar--left .nav-pills__container li.active a .status .online--icon', 'fill:' + theme.sidebarTextActiveColor, 2); + } + + if (theme.sidebarTextActiveBg === theme.onlineIndicator) { + changeCss('.sidebar--left .nav-pills__container li.active a .status .online--icon', 'fill:' + theme.sidebarTextActiveColor, 1); } if (theme.sidebarHeaderBg) { @@ -470,6 +473,8 @@ export function applyTheme(theme) { if (theme.centerChannelColor) { changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1); changeCss('#post-create', 'color:' + theme.centerChannelColor, 2); + changeCss('.channel-header__links a', 'fill:' + changeOpacity(theme.centerChannelColor, 0.7), 1); + changeCss('.channel-header__links a:hover, .channel-header__links a:active', 'fill:' + theme.centerChannelColor, 2); changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3); changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2); changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1); @@ -482,6 +487,10 @@ export function applyTheme(theme) { changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); + changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); + changeCss('.popover.left>.arrow', 'border-left-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); + changeCss('.popover.top>.arrow', 'border-top-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1); changeCss('.command-name, .popover .popover-title', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.dropdown-menu .divider', 'background:' + theme.centerChannelColor, 1); changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1); @@ -497,7 +506,7 @@ export function applyTheme(theme) { changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); - changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-channel-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); @@ -505,6 +514,7 @@ export function applyTheme(theme) { changeCss('.post:hover, .modal .more-channel-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .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, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); + changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1); changeCss('.post.current--user:hover .post-body ', 'background: none;', 1); changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); } diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index 09907da6d..4b4fc1664 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -117,6 +117,9 @@ .form-group { margin-bottom: 25px; } + ul, ol { + padding-left: 23px; + } .help-text { margin: 10px 0 0 15px; color: #777; diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 18462d92a..44a474213 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -34,15 +34,20 @@ body { } } +img { + max-width: 100%; + height: auto; +} + .input-group-addon { background: transparent; } .popover { color: #333; - &.bottom { + &.bottom, &.right, &.top, &.left { >.arrow:after { - border-bottom-color: rgba(white, 0.5); + border-color: transparent; } } .popover-title { @@ -78,6 +83,9 @@ a:focus, a:hover { .tooltip { .tooltip-inner { word-break: break-word; + font-size: 13px; + padding: 3px 10px 4px; + font-weight: 500; } } diff --git a/web/sass-files/sass/partials/_content.scss b/web/sass-files/sass/partials/_content.scss index c8c205047..49b3916a9 100644 --- a/web/sass-files/sass/partials/_content.scss +++ b/web/sass-files/sass/partials/_content.scss @@ -28,6 +28,7 @@ bottom: 0; left: 0; width: 100%; + z-index: 3; } .post-list { .new-messages-hr { diff --git a/web/sass-files/sass/partials/_error-bar.scss b/web/sass-files/sass/partials/_error-bar.scss index 2e3d3c87e..8b7432bbb 100644 --- a/web/sass-files/sass/partials/_error-bar.scss +++ b/web/sass-files/sass/partials/_error-bar.scss @@ -3,7 +3,7 @@ text-align:center; position: relative; color: #fff; - position: fixed; + position: absolute; top: 0; width: 100%; z-index: 9999; diff --git a/web/sass-files/sass/partials/_forms.scss b/web/sass-files/sass/partials/_forms.scss index 6c1f7cc6c..2d7b6cd26 100644 --- a/web/sass-files/sass/partials/_forms.scss +++ b/web/sass-files/sass/partials/_forms.scss @@ -37,3 +37,9 @@ } } } + +.help-block { + font-size: 0.95em; + margin: 10px 0 0; + color: #999; +} diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index 8e353aff9..8bf163f90 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -147,7 +147,8 @@ } .header__info { color: #fff; - padding-left: 4px; + @include clearfix; + padding-left: 2px; z-index: 1; position: relative; } @@ -155,7 +156,7 @@ display: block; font-weight: 600; font-size: 16px; - max-width: 80%; + max-width: 85%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -290,11 +291,6 @@ display: inline-block; width: 15px; margin: 9px 6px 3px 0; - &:hover { - svg { - fill: #777; - } - } a { height: 100%; display: block; @@ -302,6 +298,6 @@ svg { vertical-align: top; margin-top: 8px; - fill: #aaa; + fill: inherit; } } diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss index 122586354..1aa942ad0 100644 --- a/web/sass-files/sass/partials/_markdown.scss +++ b/web/sass-files/sass/partials/_markdown.scss @@ -53,15 +53,10 @@ blockquote { } pre { border: none; - background-color: #f7f7f7; margin: 5px 0; - .current--user & { - background: #fff; - } - code { - color: #c7254e; - } + color: inherit; } code { background: #fff; + color: inherit; } diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index 2722333a4..90ea8ce2c 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -1,3 +1,6 @@ +#channel_members_modal .modal-body { + min-height: 110px; +} .modal-body { padding: 20px 15px; } diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index ccd7fd425..0f3cc0ef6 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -147,12 +147,12 @@ body.ios { &::-webkit-scrollbar { width: 0px !important; } - &.inactive { - display: none; - } - &.active { - display: inline; - } + &.inactive { + display: none; + } + &.active { + display: inline; + } } .post-list__table { display: table; @@ -195,6 +195,14 @@ body.ios { .post-body__cell { vertical-align: top; position: relative; + &.scroll { + .btn-file { + right: 15px; + } + .custom-textarea { + padding-right: 43px; + } + } } .send-button { display: none; @@ -399,6 +407,8 @@ body.ios { display: none; } .post-body { + position: relative; + z-index: 1; max-width: 100%; width: 600px; float: left; @@ -423,10 +433,13 @@ body.ios { } } .post-header { + position: relative; list-style-type: none; margin: 0 0 1px; padding-left: 0px; &.post-header-post { + position: relative; + z-index: 1; width: 200px; text-align: right; float: left; @@ -511,9 +524,9 @@ body.ios { } .bot-indicator { - background-color: lightgrey; - border-radius:2px; - padding-left:2px; - padding-right:2px; - font-family:"Courier New" + background-color: lightgrey; + border-radius:2px; + padding-left:2px; + padding-right:2px; + font-family:"Courier New" } diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 82ec1811a..dbe6029ec 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -48,6 +48,7 @@ } .post-header { &.post-header-post { + z-index: inherit; width: auto; float: none; text-align: left; @@ -73,6 +74,10 @@ display: inline; .post-info { display: inline; + .tooltip { + margin-top: -25px; + margin-left: 40px; + } .post-profile-time { margin: 0; } @@ -134,6 +139,7 @@ } .post-header { &.post-header-post { + z-index: inherit; width: auto; float: none; text-align: left; @@ -153,6 +159,10 @@ display: inline; .post-info { display: inline; + .tooltip { + margin-top: -25px; + margin-left: 40px; + } .post-profile-time { width: auto; margin: 0; @@ -327,6 +337,7 @@ } .modal-title { float: none; + max-width: 90%; } .btn { &.btn-primary { @@ -421,6 +432,9 @@ .post-body__cell { display: table-cell; padding-left: 45px; + .sidebar--right & { + padding-left: 0; + } } .app__content & { .btn-file { @@ -612,6 +626,12 @@ &:hover, &:focus { background: transparent; } + &.has-close { + .btn-close { + display: block; + @include opacity(0.5); + } + } } } } @@ -667,7 +687,7 @@ .modal-image { .image-wrapper { font-size: 12px; - max-width: 280px; + min-width: 280px; .modal-close { @include opacity(1); } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 8debb0b4e..0c2f25eab 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -268,7 +268,6 @@ position:absolute; right:15px; top:13px; - color:#414142; } } diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 73d702fef..24aaef059 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -98,6 +98,27 @@ &.has-badge { padding-right: 30px; } + &.has-close { + padding-right: 30px; + &:hover { + .btn-close { + display: block; + @include opacity(0.8); + } + } + .btn-close { + position: absolute; + right: 15px; + top: -1px; + font-size: 20px; + font-weight: 600; + @include opacity(0); + display: none; + &:hover { + @include opacity(1); + } + } + } &.nav-more { text-decoration: underline; } diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss index b37dbf421..c954b03d8 100644 --- a/web/sass-files/sass/partials/_sidebar--right.scss +++ b/web/sass-files/sass/partials/_sidebar--right.scss @@ -26,6 +26,7 @@ .post-header { .post-header-col { &.post-header__reply { + min-width: 30px; text-align: right; float: right; } diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index fcf0d5d77..6d0256142 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -96,12 +96,6 @@ margin-bottom: 0.8em; } - .form__hint { - font-size: 0.95em; - color: #999; - margin: 10px 0 0; - } - .signup-team-confirm__container { padding: 100px 0px 100px 0px; } diff --git a/web/templates/channel.html b/web/templates/channel.html index 2af94e415..13fd80d75 100644 --- a/web/templates/channel.html +++ b/web/templates/channel.html @@ -54,7 +54,7 @@ <script> window.setup_channel_page({{ .Props }}); $('body').tooltip( {selector: '[data-toggle=tooltip]'} ); - $('.modal-body').css('max-height', $(window).height() * 0.7); + $('.modal-body').css('max-height', $(window).height() - 150); $('.modal-body').perfectScrollbar(); </script> </body> |