summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx16
-rw-r--r--web/react/components/email_verify.jsx2
-rw-r--r--web/react/components/find_team.jsx1
-rw-r--r--web/react/components/invite_member_modal.jsx3
-rw-r--r--web/react/components/login.jsx2
-rw-r--r--web/react/components/more_direct_channels.jsx59
-rw-r--r--web/react/components/navbar_dropdown.jsx2
-rw-r--r--web/react/components/password_reset_form.jsx1
-rw-r--r--web/react/components/password_reset_send_link.jsx1
-rw-r--r--web/react/components/post_info.jsx1
-rw-r--r--web/react/components/sidebar.jsx248
-rw-r--r--web/react/components/sidebar_right_menu.jsx2
-rw-r--r--web/react/components/signup_user_complete.jsx3
-rw-r--r--web/react/components/team_import_tab.jsx6
-rw-r--r--web/react/components/team_signup_display_name_page.jsx1
-rw-r--r--web/react/components/team_signup_email_item.jsx1
-rw-r--r--web/react/components/team_signup_password_page.jsx1
-rw-r--r--web/react/components/team_signup_url_page.jsx1
-rw-r--r--web/react/components/team_signup_username_page.jsx1
-rw-r--r--web/react/components/team_signup_welcome_page.jsx1
-rw-r--r--web/react/components/team_signup_with_email.jsx1
-rw-r--r--web/react/components/team_signup_with_sso.jsx1
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx2
-rw-r--r--web/react/stores/preference_store.jsx122
-rw-r--r--web/react/utils/async_client.jsx52
-rw-r--r--web/react/utils/client.jsx28
-rw-r--r--web/react/utils/constants.jsx6
-rw-r--r--web/react/utils/utils.jsx1
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss3
-rw-r--r--web/sass-files/sass/partials/_content.scss1
-rw-r--r--web/sass-files/sass/partials/_post.scss27
-rw-r--r--web/sass-files/sass/partials/_responsive.scss9
32 files changed, 447 insertions, 159 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/email_verify.jsx b/web/react/components/email_verify.jsx
index 2b413b848..9be7f97f8 100644
--- a/web/react/components/email_verify.jsx
+++ b/web/react/components/email_verify.jsx
@@ -22,7 +22,7 @@ export default class EmailVerify extends React.Component {
title = global.window.config.SiteName + ' Email Verified';
body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>;
} else {
- title = global.window.config.SiteName + ' Email Not Verified';
+ title = global.window.config.SiteName + ': You are almost done';
body = <p>Please verify your email address. Check your inbox for an email.</p>;
resend = (
<button
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/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/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'>&times;</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..7addd825f 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,6 +150,7 @@ export default class PostInfo extends React.Component {
<ul className='post-header post-info'>
<li className='post-header-col'>
<OverlayTrigger
+ container={this}
placement='top'
overlay={tooltip}
>
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 4ac1fd4a0..1caf4caa5 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -1,19 +1,20 @@
// 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');
export default class Sidebar extends React.Component {
constructor(props) {
@@ -23,12 +24,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 +42,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 +54,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 +73,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 +159,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 +304,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];
@@ -405,8 +417,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 +432,40 @@ 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;
+ if (handleClose && !badge) {
+ closeButton = (
+ <span
+ className='sidebar-channel__close pull-right'
+ data-close='true'
+ >
+ {'×'}
+ </span>
+ );
+ }
+
return (
<li
key={channel.name}
@@ -446,6 +480,7 @@ export default class Sidebar extends React.Component {
{status}
{channel.display_name}
{badge}
+ {closeButton}
</a>
</li>
);
@@ -464,7 +499,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 +521,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>
);
@@ -538,7 +576,7 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>
- Channels
+ {'Channels'}
<a
className='add-channel-btn'
href='#'
@@ -557,7 +595,7 @@ export default class Sidebar extends React.Component {
data-target='#more_channels'
data-channeltype='O'
>
- More...
+ {'More...'}
</a>
</li>
</ul>
@@ -565,7 +603,7 @@ export default class Sidebar extends React.Component {
<ul className='nav nav-pills nav-stacked'>
<li>
<h4>
- Private Groups
+ {'Private Groups'}
<a
className='add-channel-btn'
href='#'
@@ -578,7 +616,7 @@ export default class Sidebar extends React.Component {
{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 8a3af707f..4f48f3dee 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -169,6 +169,7 @@ export default class SignupUserComplete extends React.Component {
placeholder=''
maxLength='128'
autoFocus={true}
+ spellCheck='false'
/>
{emailError}
</div>
@@ -204,6 +205,7 @@ 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>
@@ -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..01f961332 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -109,6 +109,7 @@ 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>
</div>
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..85e611481 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -68,6 +68,7 @@ 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>
</div>
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/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/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 e3cbfccde..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,
@@ -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 6a5188974..91502352e 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -508,6 +508,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/_content.scss b/web/sass-files/sass/partials/_content.scss
index c8c205047..64d253283 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: 6;
}
.post-list {
.new-messages-hr {
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 85bb2eb28..b457d0dd2 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;
@@ -407,6 +407,8 @@ body.ios {
display: none;
}
.post-body {
+ position: relative;
+ z-index: 5;
max-width: 100%;
width: 600px;
float: left;
@@ -431,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: 5;
width: 200px;
text-align: right;
float: left;
@@ -519,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 4e532c16e..447f47d87 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -73,6 +73,10 @@
display: inline;
.post-info {
display: inline;
+ .tooltip {
+ margin-top: -25px;
+ margin-left: 40px;
+ }
.post-profile-time {
margin: 0;
}
@@ -153,6 +157,10 @@
display: inline;
.post-info {
display: inline;
+ .tooltip {
+ margin-top: -25px;
+ margin-left: 40px;
+ }
.post-profile-time {
width: auto;
margin: 0;
@@ -327,6 +335,7 @@
}
.modal-title {
float: none;
+ max-width: 90%;
}
.btn {
&.btn-primary {