summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/access_history_modal.jsx5
-rw-r--r--web/react/components/admin_console/admin_controller.jsx9
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx15
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx1
-rw-r--r--web/react/components/admin_console/line_chart.jsx50
-rw-r--r--web/react/components/admin_console/team_analytics.jsx393
-rw-r--r--web/react/components/admin_console/team_settings.jsx34
-rw-r--r--web/react/components/admin_console/team_users.jsx10
-rw-r--r--web/react/components/center_panel.jsx84
-rw-r--r--web/react/components/channel_header.jsx189
-rw-r--r--web/react/components/channel_view.jsx43
-rw-r--r--web/react/components/create_comment.jsx35
-rw-r--r--web/react/components/create_post.jsx207
-rw-r--r--web/react/components/edit_channel_modal.jsx38
-rw-r--r--web/react/components/edit_channel_purpose_modal.jsx124
-rw-r--r--web/react/components/edit_post_modal.jsx25
-rw-r--r--web/react/components/error_bar.jsx12
-rw-r--r--web/react/components/file_attachment.jsx2
-rw-r--r--web/react/components/find_team.jsx2
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/invite_member_modal.jsx3
-rw-r--r--web/react/components/login.jsx59
-rw-r--r--web/react/components/mention_list.jsx13
-rw-r--r--web/react/components/more_channels.jsx2
-rw-r--r--web/react/components/more_direct_channels.jsx62
-rw-r--r--web/react/components/msg_typing.jsx55
-rw-r--r--web/react/components/navbar.jsx66
-rw-r--r--web/react/components/navbar_dropdown.jsx5
-rw-r--r--web/react/components/new_channel_flow.jsx10
-rw-r--r--web/react/components/new_channel_modal.jsx14
-rw-r--r--web/react/components/popover_list_members.jsx143
-rw-r--r--web/react/components/post.jsx4
-rw-r--r--web/react/components/post_body.jsx6
-rw-r--r--web/react/components/post_info.jsx22
-rw-r--r--web/react/components/post_list.jsx720
-rw-r--r--web/react/components/post_list_container.jsx63
-rw-r--r--web/react/components/posts_view.jsx303
-rw-r--r--web/react/components/posts_view_container.jsx267
-rw-r--r--web/react/components/register_app_modal.jsx135
-rw-r--r--web/react/components/rhs_comment.jsx1
-rw-r--r--web/react/components/rhs_root_post.jsx1
-rw-r--r--web/react/components/rhs_thread.jsx10
-rw-r--r--web/react/components/search_autocomplete.jsx28
-rw-r--r--web/react/components/search_bar.jsx25
-rw-r--r--web/react/components/search_results.jsx10
-rw-r--r--web/react/components/search_results_item.jsx4
-rw-r--r--web/react/components/setting_item_max.jsx4
-rw-r--r--web/react/components/setting_picture.jsx4
-rw-r--r--web/react/components/sidebar.jsx156
-rw-r--r--web/react/components/sidebar_header.jsx77
-rw-r--r--web/react/components/sidebar_right.jsx68
-rw-r--r--web/react/components/sidebar_right_menu.jsx3
-rw-r--r--web/react/components/signup_team.jsx80
-rw-r--r--web/react/components/team_general_tab.jsx371
-rw-r--r--web/react/components/team_settings.jsx8
-rw-r--r--web/react/components/team_settings_modal.jsx2
-rw-r--r--web/react/components/team_signup_display_name_page.jsx3
-rw-r--r--web/react/components/team_signup_url_page.jsx4
-rw-r--r--web/react/components/team_signup_with_email.jsx2
-rw-r--r--web/react/components/textbox.jsx6
-rw-r--r--web/react/components/time_since.jsx50
-rw-r--r--web/react/components/tutorial/tutorial_intro_screens.jsx152
-rw-r--r--web/react/components/tutorial/tutorial_tip.jsx131
-rw-r--r--web/react/components/user_settings/code_theme_chooser.jsx55
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx5
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx39
-rw-r--r--web/react/components/user_settings/user_settings.jsx12
-rw-r--r--web/react/components/user_settings/user_settings_advanced.jsx169
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx25
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx100
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx3
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx6
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx1
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx12
74 files changed, 3459 insertions, 1400 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index c8af2553d..f0a31ce90 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -90,8 +90,9 @@ export default class AccessHistoryModal extends React.Component {
case '/channels/update':
currentAuditDesc = 'Updated the ' + channelName + ' channel/group name';
break;
- case '/channels/update_desc':
- currentAuditDesc = 'Updated the ' + channelName + ' channel/group description';
+ case '/channels/update_desc': // support the old path
+ case '/channels/update_header':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group header';
break;
default:
let userIdField = [];
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index f770d166c..8e0ab0555 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -6,6 +6,7 @@ var AdminStore = require('../../stores/admin_store.jsx');
var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
+var Utils = require('../../utils/utils.jsx');
var EmailSettingsTab = require('./email_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx');
@@ -18,6 +19,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
var TeamUsersTab = require('./team_users.jsx');
+var TeamAnalyticsTab = require('./team_analytics.jsx');
export default class AdminController extends React.Component {
constructor(props) {
@@ -45,7 +47,8 @@ export default class AdminController extends React.Component {
};
if (!props.tab) {
- history.replaceState(null, null, `/admin_console/${this.state.selected}`);
+ var tokenIndex = Utils.getUrlParameter('session_token_index');
+ history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`);
}
}
@@ -149,6 +152,10 @@ export default class AdminController extends React.Component {
if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
+ } else if (this.state.selected === 'team_analytics') {
+ if (this.state.teams) {
+ tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
+ }
}
}
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index b0e01ff17..0d52ae347 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -3,6 +3,7 @@
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
var SelectTeamModal = require('./select_team_modal.jsx');
+var Utils = require('../../utils/utils.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -24,12 +25,13 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
- history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
+ var tokenIndex = Utils.getUrlParameter('session_token_index');
+ history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
}
isSelected(name, teamId) {
if (this.props.selected === name) {
- if (name === 'team_users') {
+ if (name === 'team_users' || name === 'team_analytics') {
if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
return 'active';
}
@@ -121,6 +123,15 @@ export default class AdminSidebar extends React.Component {
{'- Users'}
</a>
</li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_analytics', team.id)}
+ onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
+ >
+ {'- Statistics'}
+ </a>
+ </li>
</ul>
</li>
</ul>
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
index 8b0f00083..f8fb6d115 100644
--- a/web/react/components/admin_console/gitlab_settings.jsx
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -259,7 +259,6 @@ export default class GitLabSettings extends React.Component {
}
}
-
//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim();
// <div className='form-group'>
// <label
diff --git a/web/react/components/admin_console/line_chart.jsx b/web/react/components/admin_console/line_chart.jsx
new file mode 100644
index 000000000..7e2f95c84
--- /dev/null
+++ b/web/react/components/admin_console/line_chart.jsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class LineChart extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.initChart = this.initChart.bind(this);
+ this.chart = null;
+ }
+
+ componentDidMount() {
+ this.initChart(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.chart) {
+ this.chart.destroy();
+ this.initChart(nextProps);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ }
+
+ initChart(props) {
+ var el = ReactDOM.findDOMNode(this);
+ var ctx = el.getContext('2d');
+ this.chart = new Chart(ctx).Line(props.data, props.options || {}); //eslint-disable-line new-cap
+ }
+
+ render() {
+ return (
+ <canvas
+ width={this.props.width}
+ height={this.props.height}
+ />
+ );
+ }
+}
+
+LineChart.propTypes = {
+ width: React.PropTypes.string,
+ height: React.PropTypes.string,
+ data: React.PropTypes.object,
+ options: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx
new file mode 100644
index 000000000..0c9d1f61b
--- /dev/null
+++ b/web/react/components/admin_console/team_analytics.jsx
@@ -0,0 +1,393 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+var LineChart = require('./line_chart.jsx');
+
+export default class TeamAnalytics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getData = this.getData.bind(this);
+
+ this.state = {
+ users: null,
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ recent_active_users: null,
+ newly_created_users: null
+ };
+ }
+
+ componentDidMount() {
+ this.getData(this.props.team.id);
+ }
+
+ getData(teamId) {
+ Client.getAnalytics(
+ teamId,
+ 'standard',
+ (data) => {
+ for (var index in data) {
+ if (data[index].name === 'channel_open_count') {
+ this.setState({channel_open_count: data[index].value});
+ }
+
+ if (data[index].name === 'channel_private_count') {
+ this.setState({channel_private_count: data[index].value});
+ }
+
+ if (data[index].name === 'post_count') {
+ this.setState({post_count: data[index].value});
+ }
+ }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getAnalytics(
+ teamId,
+ 'post_counts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Total Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({post_counts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getAnalytics(
+ teamId,
+ 'user_counts_with_posts_day',
+ (data) => {
+ data.reverse();
+
+ var chartData = {
+ labels: [],
+ datasets: [{
+ label: 'Active Users With Posts',
+ fillColor: 'rgba(151,187,205,0.2)',
+ strokeColor: 'rgba(151,187,205,1)',
+ pointColor: 'rgba(151,187,205,1)',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: 'rgba(151,187,205,1)',
+ data: []
+ }]
+ };
+
+ for (var index in data) {
+ if (data[index]) {
+ var row = data[index];
+ chartData.labels.push(row.name);
+ chartData.datasets[0].data.push(row.value);
+ }
+ }
+
+ this.setState({user_counts_with_posts_day: chartData});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ this.setState({users});
+
+ var usersList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ usersList.push(users[id]);
+ }
+ }
+
+ usersList.sort((a, b) => {
+ if (a.last_activity_at < b.last_activity_at) {
+ return 1;
+ }
+
+ if (a.last_activity_at > b.last_activity_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ var recentActive = [];
+ for (let i = 0; i < usersList.length; i++) {
+ recentActive.push(usersList[i]);
+ if (i > 19) {
+ break;
+ }
+ }
+
+ this.setState({recent_active_users: recentActive});
+
+ usersList.sort((a, b) => {
+ if (a.create_at < b.create_at) {
+ return 1;
+ }
+
+ if (a.create_at > b.create_at) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ var newlyCreated = [];
+ for (let i = 0; i < usersList.length; i++) {
+ newlyCreated.push(usersList[i]);
+ if (i > 19) {
+ break;
+ }
+ }
+
+ this.setState({newly_created_users: newlyCreated});
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.setState({
+ users: null,
+ serverError: null,
+ channel_open_count: null,
+ channel_private_count: null,
+ post_count: null,
+ post_counts_day: null,
+ user_counts_with_posts_day: null,
+ recent_active_users: null,
+ newly_created_users: null
+ });
+
+ this.getData(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ var totalCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Users'}<i className='fa fa-users'/></div>
+ <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
+ </div>
+ </div>
+ );
+
+ var openChannelCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Public Groups'}<i className='fa fa-unlock-alt'/></div>
+ <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
+ </div>
+ </div>
+ );
+
+ var openPrivateCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div>
+ <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
+ </div>
+ </div>
+ );
+
+ var postCount = (
+ <div className='col-sm-3'>
+ <div className='total-count'>
+ <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div>
+ <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
+ </div>
+ </div>
+ );
+
+ var postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.state.post_counts_day != null) {
+ postCountsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div className='content'>
+ <LineChart
+ data={this.state.post_counts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Total Posts'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ </div>
+ );
+
+ if (this.state.user_counts_with_posts_day != null) {
+ usersWithPostsByDay = (
+ <div className='col-sm-12'>
+ <div className='total-count by-day'>
+ <div className='title'>{'Active Users With Posts'}</div>
+ <div className='content'>
+ <LineChart
+ data={this.state.user_counts_with_posts_day}
+ width='740'
+ height='225'
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var recentActiveUser = (
+ <div className='recent-active-users'>
+ <div>{'Recent Active Users'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.recent_active_users != null) {
+ recentActiveUser = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Recent Active Users'}</div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.state.recent_active_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td>{user.email}</td>
+ <td>{Utils.displayDateTime(user.last_activity_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ var newUsers = (
+ <div className='recent-active-users'>
+ <div>{'Newly Created Users'}</div>
+ <div>{'Loading...'}</div>
+ </div>
+ );
+
+ if (this.state.newly_created_users != null) {
+ newUsers = (
+ <div className='col-sm-6'>
+ <div className='total-count recent-active-users'>
+ <div className='title'>{'Newly Created Users'}</div>
+ <div className='content'>
+ <table>
+ <tbody>
+ {
+ this.state.newly_created_users.map((user) => {
+ return (
+ <tr key={user.id}>
+ <td>{user.email}</td>
+ <td>{Utils.displayDateTime(user.create_at)}</td>
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed team_statistics'>
+ <h3>{'Statistics for ' + this.props.team.name}</h3>
+ {serverError}
+ <div className='row'>
+ {totalCount}
+ {postCount}
+ {openChannelCount}
+ {openPrivateCount}
+ </div>
+ <div className='row'>
+ {postCountsByDay}
+ </div>
+ <div className='row'>
+ {usersWithPostsByDay}
+ </div>
+ <div className='row'>
+ {recentActiveUser}
+ {newUsers}
+ </div>
+ </div>
+ );
+ }
+}
+
+TeamAnalytics.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index 9ecd14a1e..6587184ea 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -32,6 +32,7 @@ export default class TeamSettings extends React.Component {
config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
+ config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked;
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
@@ -243,6 +244,39 @@ export default class TeamSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTeamListing'
+ >
+ {'Enable Team Directory: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamListing'
+ value='true'
+ ref='EnableTeamListing'
+ defaultChecked={this.props.config.TeamSettings.EnableTeamListing}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamListing'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableTeamListing}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
index ffb412159..b44aba56e 100644
--- a/web/react/components/admin_console/team_users.jsx
+++ b/web/react/components/admin_console/team_users.jsx
@@ -33,14 +33,6 @@ export default class UserList extends React.Component {
this.getTeamProfiles(this.props.team.id);
}
- // this.setState({
- // teamId: this.state.teamId,
- // users: this.state.users,
- // serverError: this.state.serverError,
- // showPasswordModal: this.state.showPasswordModal,
- // user: this.state.user
- // });
-
getTeamProfiles(teamId) {
Client.getProfilesForTeam(
teamId,
@@ -95,8 +87,6 @@ export default class UserList extends React.Component {
}
doPasswordResetDismiss() {
- this.state.showPasswordModal = false;
- this.state.user = null;
this.setState({
teamId: this.state.teamId,
users: this.state.users,
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
new file mode 100644
index 000000000..242c2c637
--- /dev/null
+++ b/web/react/components/center_panel.jsx
@@ -0,0 +1,84 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const TutorialIntroScreens = require('./tutorial/tutorial_intro_screens.jsx');
+const CreatePost = require('./create_post.jsx');
+const PostsViewContainer = require('./posts_view_container.jsx');
+const ChannelHeader = require('./channel_header.jsx');
+const Navbar = require('./navbar.jsx');
+const FileUploadOverlay = require('./file_upload_overlay.jsx');
+
+const PreferenceStore = require('../stores/preference_store.jsx');
+const UserStore = require('../stores/user_store.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const TutorialSteps = Constants.TutorialSteps;
+const Preferences = Constants.Preferences;
+
+export default class CenterPanel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
+
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS};
+ }
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ }
+ onPreferenceChange() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS});
+ }
+ render() {
+ let postsContainer;
+ if (this.state.showTutorialScreens) {
+ postsContainer = <TutorialIntroScreens />;
+ } else {
+ postsContainer = <PostsViewContainer />;
+ }
+
+ return (
+ <div className='inner__wrap channel__wrap'>
+ <div className='row header'>
+ <div id='navbar'>
+ <Navbar/>
+ </div>
+ </div>
+ <div className='row main'>
+ <FileUploadOverlay
+ id='file_upload_overlay'
+ overlayType='center'
+ />
+ <div
+ id='app-content'
+ className='app__content'
+ >
+ <div id='channel-header'>
+ <ChannelHeader />
+ </div>
+ <div id='post-list'>
+ {postsContainer}
+ </div>
+ <div
+ className='post-create__container'
+ id='post-create'
+ >
+ <CreatePost />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+CenterPanel.defaultProps = {
+};
+
+CenterPanel.propTypes = {
+};
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 1b709336f..20f106f30 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -3,7 +3,8 @@
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
-const PostStore = require('../stores/post_store.jsx');
+const SearchStore = require('../stores/search_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const NavbarSearchBox = require('./search_bar.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
@@ -11,6 +12,7 @@ const TextFormatting = require('../utils/text_formatting.jsx');
const Utils = require('../utils/utils.jsx');
const MessageWrapper = require('./message_wrapper.jsx');
const PopoverListMembers = require('./popover_list_members.jsx');
+const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Constants = require('../utils/constants.jsx');
@@ -27,7 +29,9 @@ export default class ChannelHeader extends React.Component {
this.handleLeave = this.handleLeave.bind(this);
this.searchMentions = this.searchMentions.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.showEditChannelPurposeModal = false;
+ this.state = state;
}
getStateFromStores() {
return {
@@ -35,20 +39,22 @@ export default class ChannelHeader extends React.Component {
memberChannel: ChannelStore.getCurrentMember(),
memberTeam: UserStore.getCurrentUser(),
users: ChannelStore.getCurrentExtraInfo().members,
- searchVisible: PostStore.getSearchResults() !== null
+ searchVisible: SearchStore.getSearchResults() !== null
};
}
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
- PostStore.addSearchChangeListener(this.onListenerChange);
+ SearchStore.addSearchChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
+ PreferenceStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
- PostStore.removeSearchChangeListener(this.onListenerChange);
- UserStore.addChangeListener(this.onListenerChange);
+ SearchStore.removeSearchChangeListener(this.onListenerChange);
+ UserStore.removeChangeListener(this.onListenerChange);
+ PreferenceStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -110,11 +116,11 @@ export default class ChannelHeader extends React.Component {
bSize='large'
placement='bottom'
className='description'
- onMouseOver={() => this.refs.descriptionOverlay.show()}
- onMouseOut={() => this.refs.descriptionOverlay.hide()}
+ onMouseOver={() => this.refs.headerOverlay.show()}
+ onMouseOut={() => this.refs.headerOverlay.hide()}
>
<MessageWrapper
- message={channel.description}
+ message={channel.header}
/>
</Popover>
);
@@ -131,7 +137,7 @@ export default class ChannelHeader extends React.Component {
} else {
contact = this.state.users[0];
}
- channelTitle = contact.nickname || contact.username;
+ channelTitle = Utils.displayUsername(contact.id);
}
}
@@ -144,7 +150,7 @@ export default class ChannelHeader extends React.Component {
if (isDirect) {
dropdownContents.push(
<li
- key='edit_description_direct'
+ key='edit_header_direct'
role='presentation'
>
<a
@@ -152,11 +158,11 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set Channel Description...
+ Set Channel Header...
</a>
</li>
);
@@ -216,7 +222,7 @@ export default class ChannelHeader extends React.Component {
dropdownContents.push(
<li
- key='set_channel_description'
+ key='set_channel_header'
role='presentation'
>
<a
@@ -224,11 +230,25 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set {channelTerm} Description...
+ Set {channelTerm} Header...
+ </a>
+ </li>
+ );
+ dropdownContents.push(
+ <li
+ key='set_channel_purpose'
+ role='presentation'
+ >
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showEditChannelPurposeModal: true})}
+ >
+ Set {channelTerm} Purpose...
</a>
</li>
);
@@ -307,84 +327,91 @@ export default class ChannelHeader extends React.Component {
}
return (
- <table className='channel-header alt'>
- <tbody>
- <tr>
- <th>
- <div className='channel-header__info'>
- <div className='dropdown'>
+ <div>
+ <table className='channel-header alt'>
+ <tbody>
+ <tr>
+ <th>
+ <div className='channel-header__info'>
+ <div className='dropdown'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <strong className='heading'>{channelTitle} </strong>
+ <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ </a>
+ <ul
+ className='dropdown-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {dropdownContents}
+ </ul>
+ </div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ placement='bottom'
+ overlay={popoverContent}
+ ref='headerOverlay'
+ >
+ <div
+ onClick={TextFormatting.handleClick}
+ className='description'
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
+ />
+ </OverlayTrigger>
+ </div>
+ </th>
+ <th>
+ <PopoverListMembers
+ members={this.state.users}
+ channelId={channel.id}
+ />
+ </th>
+ <th className='search-bar__container'><NavbarSearchBox /></th>
+ <th>
+ <div className='dropdown channel-header__links'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
- id='channel_header_dropdown'
+ id='channel_header_right_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
- <strong className='heading'>{channelTitle} </strong>
- <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
+ <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
</a>
<ul
- className='dropdown-menu'
+ className='dropdown-menu dropdown-menu-right'
role='menu'
- aria-labelledby='channel_header_dropdown'
+ aria-labelledby='channel_header_right_dropdown'
>
- {dropdownContents}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.searchMentions}
+ >
+ Recent Mentions
+ </a>
+ </li>
</ul>
</div>
- <OverlayTrigger
- trigger={['hover', 'focus']}
- placement='bottom'
- overlay={popoverContent}
- ref='descriptionOverlay'
- >
- <div
- onClick={TextFormatting.handleClick}
- className='description'
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}}
- />
- </OverlayTrigger>
- </div>
- </th>
- <th>
- <PopoverListMembers
- members={this.state.users}
- channelId={channel.id}
- />
- </th>
- <th className='search-bar__container'><NavbarSearchBox /></th>
- <th>
- <div className='dropdown channel-header__links'>
- <a
- href='#'
- className='dropdown-toggle theme'
- type='button'
- id='channel_header_right_dropdown'
- data-toggle='dropdown'
- aria-expanded='true'
- >
- <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
- </a>
- <ul
- className='dropdown-menu dropdown-menu-right'
- role='menu'
- aria-labelledby='channel_header_right_dropdown'
- >
- <li role='presentation'>
- <a
- role='menuitem'
- href='#'
- onClick={this.searchMentions}
- >
- Recent Mentions
- </a>
- </li>
- </ul>
- </div>
- </th>
- </tr>
- </tbody>
- </table>
+ </th>
+ </tr>
+ </tbody>
+ </table>
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ </div>
);
}
}
diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx
new file mode 100644
index 000000000..3f53a94c2
--- /dev/null
+++ b/web/react/components/channel_view.jsx
@@ -0,0 +1,43 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const CenterPanel = require('../components/center_panel.jsx');
+const Sidebar = require('../components/sidebar.jsx');
+const SidebarRight = require('../components/sidebar_right.jsx');
+const SidebarRightMenu = require('../components/sidebar_right_menu.jsx');
+
+export default class ChannelView extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ return (
+ <div className='container-fluid'>
+ <div
+ className='sidebar--right'
+ id='sidebar-right'
+ >
+ <SidebarRight/>
+ </div>
+ <div
+ className='sidebar--menu'
+ id='sidebar-menu'
+ >
+ <SidebarRightMenu/>
+ </div>
+ <div
+ className='sidebar--left'
+ id='sidebar-left'
+ >
+ <Sidebar/>
+ </div>
+ <CenterPanel />
+ </div>
+ );
+ }
+}
+ChannelView.defaultProps = {
+};
+
+ChannelView.propTypes = {
+};
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 435c7d542..058594165 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const PostStore = require('../stores/post_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const Textbox = require('./textbox.jsx');
const MsgTyping = require('./msg_typing.jsx');
const FileUpload = require('./file_upload.jsx');
@@ -27,7 +28,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -36,6 +37,7 @@ export default class CreateComment extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
PostStore.clearCommentDraftUploads();
@@ -45,15 +47,23 @@ export default class CreateComment extends React.Component {
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
- windowWidth: Utils.windowWidth()
+ windowWidth: Utils.windowWidth(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
};
}
componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
window.removeEventListener('resize', this.handleResize);
}
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
handleResize() {
this.setState({windowWidth: Utils.windowWidth()});
}
@@ -140,14 +150,16 @@ export default class CreateComment extends React.Component {
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
}
commentMsgKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
- if ((t - this.lastTime) > 5000) {
+ if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}});
this.lastTime = t;
}
@@ -161,7 +173,12 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.commentMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -313,7 +330,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
createMessage='Add a comment...'
initialText=''
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index 8b5fc4162..1545cdfaa 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -1,20 +1,26 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+const MsgTyping = require('./msg_typing.jsx');
+const Textbox = require('./textbox.jsx');
+const FileUpload = require('./file_upload.jsx');
+const FilePreview = require('./file_preview.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
+const Utils = require('../utils/utils.jsx');
+
const ChannelStore = require('../stores/channel_store.jsx');
const PostStore = require('../stores/post_store.jsx');
const UserStore = require('../stores/user_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
const SocketStore = require('../stores/socket_store.jsx');
-const MsgTyping = require('./msg_typing.jsx');
-const Textbox = require('./textbox.jsx');
-const FileUpload = require('./file_upload.jsx');
-const FilePreview = require('./file_preview.jsx');
-const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
@@ -35,13 +41,16 @@ export default class CreatePost extends React.Component {
this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
- this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleResize = this.handleResize.bind(this);
+ this.sendMessage = this.sendMessage.bind(this);
PostStore.clearDraftUploads();
const draft = this.getCurrentDraft();
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
this.state = {
channelId: ChannelStore.getCurrentId(),
@@ -51,8 +60,12 @@ export default class CreatePost extends React.Component {
submitting: false,
initialText: draft.messageText,
windowWidth: Utils.windowWidth(),
- windowHeight: Utils.windowHeight()
+ windowHeight: Utils.windowHeight(),
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value,
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER
};
+
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
}
handleResize() {
this.setState({
@@ -122,6 +135,11 @@ export default class CreatePost extends React.Component {
post.message,
false,
(data) => {
+ if (data.response === 'not implemented') {
+ this.sendMessage(post);
+ return;
+ }
+
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
@@ -130,72 +148,82 @@ export default class CreatePost extends React.Component {
}
},
(err) => {
- const state = {};
- state.serverError = err.message;
- state.submitting = false;
- this.setState(state);
- }
- );
- } else {
- post.channel_id = this.state.channelId;
- post.filenames = this.state.previews;
-
- const time = Utils.getTimestamp();
- const userId = UserStore.getCurrentId();
- post.pending_post_id = `${userId}:${time}`;
- post.user_id = userId;
- post.create_at = time;
- post.root_id = this.state.rootId;
- post.parent_id = this.state.parentId;
-
- const channel = ChannelStore.get(this.state.channelId);
-
- PostStore.storePendingPost(post);
- PostStore.storeDraft(channel.id, null);
- this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
-
- Client.createPost(post, channel,
- (data) => {
- AsyncClient.getPosts();
-
- const member = ChannelStore.getMember(channel.id);
- member.msg_count = channel.total_msg_count;
- member.last_viewed_at = Date.now();
- ChannelStore.setChannelMember(member);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POST,
- post: data
- });
- },
- (err) => {
- const state = {};
-
- if (err.message === 'Invalid RootId parameter') {
- if ($('#post_deleted').length > 0) {
- $('#post_deleted').modal('show');
- }
- PostStore.removePendingPost(post.pending_post_id);
+ if (err.sendMessage) {
+ this.sendMessage(post);
} else {
- post.state = Constants.POST_FAILED;
- PostStore.updatePendingPost(post);
+ const state = {};
+ state.serverError = err.message;
+ state.submitting = false;
+ this.setState(state);
}
-
- state.submitting = false;
- this.setState(state);
}
);
+ } else {
+ this.sendMessage(post);
}
}
+ sendMessage(post) {
+ post.channel_id = this.state.channelId;
+ post.filenames = this.state.previews;
+
+ const time = Utils.getTimestamp();
+ const userId = UserStore.getCurrentId();
+ post.pending_post_id = `${userId}:${time}`;
+ post.user_id = userId;
+ post.create_at = time;
+ post.root_id = this.state.rootId;
+ post.parent_id = this.state.parentId;
+
+ const channel = ChannelStore.get(this.state.channelId);
+
+ PostStore.storePendingPost(post);
+ PostStore.storeDraft(channel.id, null);
+ PostStore.jumpPostsViewToBottom();
+ this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
+
+ Client.createPost(post, channel,
+ (data) => {
+ AsyncClient.getPosts();
+
+ const member = ChannelStore.getMember(channel.id);
+ member.msg_count = channel.total_msg_count;
+ member.last_viewed_at = Date.now();
+ ChannelStore.setChannelMember(member);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POST,
+ post: data
+ });
+ },
+ (err) => {
+ const state = {};
+
+ if (err.message === 'Invalid RootId parameter') {
+ if ($('#post_deleted').length > 0) {
+ $('#post_deleted').modal('show');
+ }
+ PostStore.removePendingPost(post.pending_post_id);
+ } else {
+ post.state = Constants.POST_FAILED;
+ PostStore.updatePendingPost(post);
+ }
+
+ state.submitting = false;
+ this.setState(state);
+ }
+ );
+ }
postMsgKeyPress(e) {
- if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
- e.preventDefault();
- ReactDOM.findDOMNode(this.refs.textbox).blur();
- this.handleSubmit(e);
+ if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') {
+ if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
+ e.preventDefault();
+ ReactDOM.findDOMNode(this.refs.textbox).blur();
+ this.handleSubmit(e);
+ }
}
const t = Date.now();
- if ((t - this.lastTime) > 5000) {
+ if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}});
this.lastTime = t;
}
@@ -240,8 +268,14 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
handleUploadError(err, clientId) {
+ let message = err;
+ if (message && typeof message !== 'string') {
+ // err is an AppError from the server
+ message = err.message;
+ }
+
if (clientId === -1) {
- this.setState({serverError: err});
+ this.setState({serverError: message});
} else {
const draft = PostStore.getDraft(this.state.channelId);
@@ -252,7 +286,7 @@ export default class CreatePost extends React.Component {
PostStore.storeDraft(this.state.channelId, draft);
- this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
+ this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
}
}
handleTextDrop(text) {
@@ -286,11 +320,13 @@ export default class CreatePost extends React.Component {
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
this.resizePostHolder();
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
window.removeEventListener('resize', this.handleResize);
}
onChange() {
@@ -301,6 +337,13 @@ export default class CreatePost extends React.Component {
this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
+ onPreferenceChange() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ this.setState({
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER,
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
getFileCount(channelId) {
if (channelId === this.state.channelId) {
return this.state.previews.length + this.state.uploadsInProgress.length;
@@ -309,7 +352,12 @@ export default class CreatePost extends React.Component {
const draft = PostStore.getDraft(channelId);
return draft.previews.length + draft.uploadsInProgress.length;
}
- handleArrowUp(e) {
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.postMsgKeyPress(e);
+ return;
+ }
+
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
@@ -330,6 +378,25 @@ export default class CreatePost extends React.Component {
});
}
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Sending Messages'}</h4>
+ <p>{'Type here to write a message.'}</p>
+ <p>{'Click the attachment button to upload an image or a file.'}</p>
+ </div>
+ );
+
+ return (
+ <TutorialTip
+ placement='top'
+ screens={screens}
+ overlayClass='tip-overlay--chat'
+ />
+ );
+ }
render() {
let serverError = null;
if (this.state.serverError) {
@@ -361,6 +428,11 @@ export default class CreatePost extends React.Component {
postFooterClassName += ' has-error';
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<form
id='create_post'
@@ -374,7 +446,7 @@ export default class CreatePost extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
- onKeyDown={this.handleArrowUp}
+ onKeyDown={this.handleKeyDown}
onHeightChange={this.resizePostHolder}
messageText={this.state.messageText}
createMessage='Write a message...'
@@ -399,6 +471,7 @@ export default class CreatePost extends React.Component {
>
<i className='fa fa-paper-plane' />
</a>
+ {tutorialTip}
</div>
<div className={postFooterClassName}>
{postError}
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
index d63a1db30..5b3c74e82 100644
--- a/web/react/components/edit_channel_modal.jsx
+++ b/web/react/components/edit_channel_modal.jsx
@@ -14,7 +14,7 @@ export default class EditChannelModal extends React.Component {
this.onShow = this.onShow.bind(this);
this.state = {
- description: '',
+ header: '',
title: '',
channelId: '',
serverError: ''
@@ -28,32 +28,32 @@ export default class EditChannelModal extends React.Component {
return;
}
- data.channel_description = this.state.description.trim();
+ data.channel_header = this.state.header.trim();
- Client.updateChannelDesc(data,
- function handleUpdateSuccess() {
+ Client.updateChannelHeader(data,
+ () => {
this.setState({serverError: ''});
AsyncClient.getChannel(this.state.channelId);
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- }.bind(this),
- function handleUpdateError(err) {
- if (err.message === 'Invalid channel_description parameter') {
- this.setState({serverError: 'This description is too long, please enter a shorter one'});
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_header parameter') {
+ this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
} else {
this.setState({serverError: err.message});
}
- }.bind(this)
+ }
);
}
handleUserInput(e) {
- this.setState({description: e.target.value});
+ this.setState({header: e.target.value});
}
handleClose() {
- this.setState({description: '', serverError: ''});
+ this.setState({header: '', serverError: ''});
}
onShow(e) {
const button = e.relatedTarget;
- this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
+ this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
}
componentDidMount() {
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
@@ -73,7 +73,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
- Edit Description
+ {'Edit Header'}
</h4>
);
if (this.state.title) {
@@ -82,7 +82,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
- Edit Description for <span className='name'>{this.state.title}</span>
+ {'Edit Header for '}<span className='name'>{this.state.title}</span>
</h4>
);
}
@@ -105,17 +105,17 @@ export default class EditChannelModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
{editTitle}
</div>
<div className='modal-body'>
+ <p>{'Edit the text appearing next to the channel name in the channel header.'}</p>
<textarea
className='form-control no-resize'
rows='6'
- ref='channelDesc'
maxLength='1024'
- value={this.state.description}
+ value={this.state.header}
onChange={this.handleUserInput}
/>
{serverError}
@@ -126,14 +126,14 @@ export default class EditChannelModal extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
- Cancel
+ {'Cancel'}
</button>
<button
type='button'
className='btn btn-primary'
onClick={this.handleEdit}
>
- Save
+ {'Save'}
</button>
</div>
</div>
diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx
new file mode 100644
index 000000000..4d162cfe7
--- /dev/null
+++ b/web/react/components/edit_channel_purpose_modal.jsx
@@ -0,0 +1,124 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const AsyncClient = require('../utils/async_client.jsx');
+const Client = require('../utils/client.jsx');
+const Modal = ReactBootstrap.Modal;
+
+export default class EditChannelPurposeModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleHide = this.handleHide.bind(this);
+ this.handleSave = this.handleSave.bind(this);
+
+ this.state = {serverError: ''};
+ }
+
+ handleHide() {
+ this.setState({serverError: ''});
+
+ if (this.props.onModalDismissed) {
+ this.props.onModalDismissed();
+ }
+ }
+
+ handleSave() {
+ if (!this.props.channel) {
+ return;
+ }
+
+ const data = {
+ channel_id: this.props.channel.id,
+ channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim()
+ };
+
+ Client.updateChannelPurpose(data,
+ () => {
+ AsyncClient.getChannel(this.props.channel.id);
+
+ this.handleHide();
+ },
+ (err) => {
+ if (err.message === 'Invalid channel_purpose parameter') {
+ this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'});
+ } else {
+ this.setState({serverError: err.message});
+ }
+ }
+ );
+ }
+
+ render() {
+ if (!this.props.show) {
+ return null;
+ }
+
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='form-group has-error'>
+ <br/>
+ <label className='control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ let title = <span>{'Edit Purpose'}</span>;
+ if (this.props.channel.display_name) {
+ title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
+ }
+
+ let channelTerm = 'Channel';
+ if (this.props.channel.channelType === 'P') {
+ channelTerm = 'Group';
+ }
+
+ return (
+ <Modal
+ className='modal-edit-channel-purpose'
+ show={this.props.show}
+ onHide={this.handleHide}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>
+ {title}
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <p>{`Describe how this ${channelTerm} should be used.`}</p>
+ <textarea
+ ref='purpose'
+ className='form-control no-resize'
+ rows='6'
+ maxLength='128'
+ defaultValue={this.props.channel.purpose}
+ />
+ {serverError}
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.handleHide}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ type='button'
+ className='btn btn-primary'
+ onClick={this.handleSave}
+ >
+ {'Save'}
+ </button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+EditChannelPurposeModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ channel: React.PropTypes.object,
+ onModalDismissed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index b259b3c18..ef32baa7d 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -6,6 +6,10 @@ var AsyncClient = require('../utils/async_client.jsx');
var Textbox = require('./textbox.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
var PostStore = require('../stores/post_store.jsx');
+var PreferenceStore = require('../stores/preference_store.jsx');
+
+var Constants = require('../utils/constants.jsx');
+var KeyCodes = Constants.KeyCodes;
export default class EditPostModal extends React.Component {
constructor() {
@@ -16,6 +20,8 @@ export default class EditPostModal extends React.Component {
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
}
@@ -51,7 +57,7 @@ export default class EditPostModal extends React.Component {
this.setState({editText: editMessage});
}
handleEditKeyPress(e) {
- if (e.which === 13 && !e.shiftKey && !e.altKey) {
+ if (this.state.ctrlSend === 'false' && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.editbox).blur();
this.handleEdit(e);
@@ -72,6 +78,16 @@ export default class EditPostModal extends React.Component {
$(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
}
+ handleKeyDown(e) {
+ if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
+ this.handleEdit(e);
+ }
+ }
+ onPreferenceChange() {
+ this.setState({
+ ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value
+ });
+ }
componentDidMount() {
var self = this;
@@ -84,7 +100,7 @@ export default class EditPostModal extends React.Component {
if (!button) {
return;
}
- self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid')});
+ self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refocusid')});
});
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
@@ -101,9 +117,11 @@ export default class EditPostModal extends React.Component {
});
PostStore.addEditPostListener(this.handleEditPostEvent);
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
}
componentWillUnmount() {
- PostStore.removeEditPostListener(this.handleEditPostEvent);
+ PostStore.removeEditPostListner(this.handleEditPostEvent);
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
}
render() {
var error = (<div className='form-group'><br /></div>);
@@ -138,6 +156,7 @@ export default class EditPostModal extends React.Component {
<Textbox
onUserInput={this.handleEditInput}
onKeyPress={this.handleEditKeyPress}
+ onKeyDown={this.handleKeyDown}
messageText={this.state.editText}
createMessage='Edit the post...'
id='edit_textbox'
diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx
index 6311d9460..f098384aa 100644
--- a/web/react/components/error_bar.jsx
+++ b/web/react/components/error_bar.jsx
@@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
- this.prevTimer = null;
this.state = ErrorStore.getLastError();
- if (this.isValidError(this.state)) {
- this.prevTimer = setTimeout(this.handleClose, 10000);
- }
}
isValidError(s) {
@@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component {
onErrorChange() {
var newState = ErrorStore.getLastError();
- if (this.prevTimer != null) {
- clearInterval(this.prevTimer);
- this.prevTimer = null;
- }
-
if (newState) {
this.setState(newState);
- if (!this.isConnectionError(newState)) {
- this.prevTimer = setTimeout(this.handleClose, 10000);
- }
} else {
this.setState({message: null});
}
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index 4d4e8390c..e707e32f5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -270,7 +270,7 @@ export default class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
data-toggle='tooltip'
- title={filenameString}
+ title={'Download ' + filenameString}
className='post-image__name'
>
{trimmedFilename}
diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx
index e324f3666..bd3c11973 100644
--- a/web/react/components/find_team.jsx
+++ b/web/react/components/find_team.jsx
@@ -50,7 +50,7 @@ export default class FindTeam extends React.Component {
if (this.state.sent) {
return (
<div>
- <h4>{'Find Your team'}</h4>
+ <h4>{'Find Your teams'}</h4>
<p>{'An email was sent with links to any teams to which you are a member.'}</p>
</div>
);
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 325e86f3d..8839bc3c7 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -98,7 +98,7 @@ export default class GetLinkModal extends React.Component {
<br /><br />
</p>
<textarea
- className='form-control no-resize'
+ className='form-control no-resize min-height'
readOnly='true'
ref='textarea'
value={this.state.value}
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 86a4b04cf..bea700725 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -4,6 +4,7 @@
var utils = require('../utils/utils.jsx');
var Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var ConfirmModal = require('./confirm_modal.jsx');
export default class InviteMemberModal extends React.Component {
@@ -292,7 +293,7 @@ export default class InviteMemberModal extends React.Component {
} else {
var teamInviteLink = null;
if (currentUser && this.props.teamType === 'O') {
- var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id;
+ var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id;
var link =
(
<a
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index 108735caf..2b9ce67ca 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -101,7 +101,7 @@ export default class Login extends React.Component {
href={'/' + teamName + '/login/gitlab'}
>
<span className='icon' />
- <span>with GitLab</span>
+ <span>{'with GitLab'}</span>
</a>
);
}
@@ -154,7 +154,7 @@ export default class Login extends React.Component {
type='submit'
className='btn btn-primary'
>
- Sign in
+ {'Sign in'}
</button>
</div>
</div>
@@ -166,7 +166,7 @@ export default class Login extends React.Component {
<div>
{loginMessage}
<div className='or__container'>
- <span>or</span>
+ <span>{'or'}</span>
</div>
</div>
);
@@ -176,16 +176,48 @@ export default class Login extends React.Component {
if (emailSignup) {
forgotPassword = (
<div className='form-group'>
- <a href={'/' + teamName + '/reset_password'}>I forgot my password</a>
+ <a href={'/' + teamName + '/reset_password'}>{'I forgot my password'}</a>
+ </div>
+ );
+ }
+
+ let userSignUp = null;
+ if (this.props.inviteId) {
+ userSignUp = (
+ <div>
+ <span>{`Don't have an account? `}
+ <a
+ href={'/signup_user_complete/?id=' + this.props.inviteId}
+ className='signup-team-login'
+ >
+ {'Create one now'}
+ </a>
+ </span>
+ </div>
+ );
+ }
+
+ let teamSignUp = null;
+ if (global.window.mm_config.EnableTeamCreation === 'true') {
+ teamSignUp = (
+ <div className='margin--extra'>
+ <span>{'Want to create your own team? '}
+ <a
+ href='/'
+ className='signup-team-login'
+ >
+ {'Sign up now'}
+ </a>
+ </span>
</div>
);
}
return (
<div className='signup-team__container'>
- <h5 className='margin--less'>Sign in to:</h5>
+ <h5 className='margin--less'>{'Sign in to:'}</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2>
+ <h2 className='signup-team__subdomain'>{'on '}{global.window.mm_config.SiteName}</h2>
<form onSubmit={this.handleSubmit}>
{verifiedBox}
<div className={'form-group' + errorClass}>
@@ -193,20 +225,12 @@ export default class Login extends React.Component {
</div>
{loginMessage}
{emailSignup}
+ {userSignUp}
<div className='form-group margin--extra form-group--small'>
<span><a href='/find_team'>{'Find other teams'}</a></span>
</div>
{forgotPassword}
- <div className='margin--extra'>
- <span>{'Want to create your own team? '}
- <a
- href='/'
- className='signup-team-login'
- >
- Sign up now
- </a>
- </span>
- </div>
+ {teamSignUp}
</form>
</div>
);
@@ -219,5 +243,6 @@ Login.defaultProps = {
};
Login.propTypes = {
teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string,
+ inviteId: React.PropTypes.string
};
diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx
index 8c1da942d..61a24c09c 100644
--- a/web/react/components/mention_list.jsx
+++ b/web/react/components/mention_list.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var Mention = require('./mention.jsx');
@@ -66,7 +66,7 @@ export default class MentionList extends React.Component {
}
}
componentDidMount() {
- PostStore.addMentionDataChangeListener(this.onListenerChange);
+ SearchStore.addMentionDataChangeListener(this.onListenerChange);
$('.post-right__scroll').scroll(this.onScroll);
@@ -74,7 +74,7 @@ export default class MentionList extends React.Component {
$(document).click(this.onClick);
}
componentWillUnmount() {
- PostStore.removeMentionDataChangeListener(this.onListenerChange);
+ SearchStore.removeMentionDataChangeListener(this.onListenerChange);
$('body').off('keydown.mentionlist', '#' + this.props.id);
}
@@ -217,12 +217,17 @@ export default class MentionList extends React.Component {
if (this.state.selectedMention === index) {
isFocused = 'mentions-focus';
}
+
+ if (!users[i].secondary_text) {
+ users[i].secondary_text = Utils.getFullName(users[i]);
+ }
+
mentions[index] = (
<Mention
key={'mention_key_' + index}
ref={'mention' + index}
username={users[i].username}
- secondary_text={Utils.getFullName(users[i])}
+ secondary_text={users[i].secondary_text}
id={users[i].id}
listId={index}
isFocused={isFocused}
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index a0084ad30..c4f831c2e 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -109,7 +109,7 @@ export default class MoreChannels extends React.Component {
<tr key={channel.id}>
<td>
<p className='more-name'>{channel.display_name}</p>
- <p className='more-description'>{channel.description}</p>
+ <p className='more-purpose'>{channel.purpose}</p>
</td>
<td className='td--action'>
{joinButton}
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index 41746d1d7..b0232fc08 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -1,13 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AsyncClient = require('../utils/async_client.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const Constants = require('../utils/constants.jsx');
-const Client = require('../utils/client.jsx');
const Modal = ReactBootstrap.Modal;
-const PreferenceStore = require('../stores/preference_store.jsx');
-const TeamStore = require('../stores/team_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
@@ -70,52 +64,24 @@ export default class MoreDirectChannels extends React.Component {
}
handleShowDirectChannel(teammate, e) {
+ e.preventDefault();
+
if (this.state.loadingDMChannel !== -1) {
return;
}
- e.preventDefault();
-
- const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id);
- let channel = ChannelStore.getByName(channelName);
-
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
- AsyncClient.savePreferences([preference]);
-
- if (channel) {
- Utils.switchChannel(channel);
-
- this.handleHide();
- } else {
- this.setState({loadingDMChannel: teammate.id});
-
- channel = {
- name: channelName,
- last_post_at: 0,
- total_msg_count: 0,
- type: 'D',
- display_name: teammate.username,
- teammate_id: teammate.id,
- status: UserStore.getStatus(teammate.id)
- };
-
- Client.createDirectChannel(
- channel,
- teammate.id,
- (data) => {
- this.setState({loadingDMChannel: -1});
-
- AsyncClient.getChannel(data.id);
- Utils.switchChannel(data);
-
- this.handleHide();
- },
- () => {
- this.setState({loadingDMChannel: -1});
- window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
- }
- );
- }
+ this.setState({loadingDMChannel: teammate.id});
+ Utils.openDirectChannelToUser(
+ teammate,
+ (channel) => {
+ Utils.switchChannel(channel);
+ this.setState({loadingDMChannel: -1});
+ this.handleHide();
+ },
+ () => {
+ this.setState({loadingDMChannel: -1});
+ }
+ );
}
handleUserChange() {
diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx
index 1bd23c55c..ccf8a2445 100644
--- a/web/react/components/msg_typing.jsx
+++ b/web/react/components/msg_typing.jsx
@@ -11,11 +11,11 @@ export default class MsgTyping extends React.Component {
constructor(props) {
super(props);
- this.timer = null;
- this.lastTime = 0;
-
this.onChange = this.onChange.bind(this);
+ this.updateTypingText = this.updateTypingText.bind(this);
+ this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
+ this.typingUsers = {};
this.state = {
text: ''
};
@@ -27,7 +27,7 @@ export default class MsgTyping extends React.Component {
componentWillReceiveProps(newProps) {
if (this.props.channelId !== newProps.channelId) {
- this.setState({text: ''});
+ this.updateTypingText();
}
}
@@ -36,28 +36,51 @@ export default class MsgTyping extends React.Component {
}
onChange(msg) {
+ let username = 'Someone';
if (msg.action === SocketEvents.TYPING &&
this.props.channelId === msg.channel_id &&
this.props.parentId === msg.props.parent_id) {
- this.lastTime = new Date().getTime();
-
- var username = 'Someone';
if (UserStore.hasProfile(msg.user_id)) {
username = UserStore.getProfile(msg.user_id).username;
}
- this.setState({text: username + ' is typing...'});
-
- if (!this.timer) {
- this.timer = setInterval(function myTimer() {
- if ((new Date().getTime() - this.lastTime) > 8000) {
- this.setState({text: ''});
- }
- }.bind(this), 3000);
+ if (this.typingUsers[username]) {
+ clearTimeout(this.typingUsers[username]);
}
+
+ this.typingUsers[username] = setTimeout(function myTimer(user) {
+ delete this.typingUsers[user];
+ this.updateTypingText();
+ }.bind(this, username), Constants.UPDATE_TYPING_MS);
+
+ this.updateTypingText();
} else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
- this.setState({text: ''});
+ if (UserStore.hasProfile(msg.user_id)) {
+ username = UserStore.getProfile(msg.user_id).username;
+ }
+ clearTimeout(this.typingUsers[username]);
+ delete this.typingUsers[username];
+ this.updateTypingText();
+ }
+ }
+
+ updateTypingText() {
+ const users = Object.keys(this.typingUsers);
+ let text = '';
+ switch (users.length) {
+ case 0:
+ text = '';
+ break;
+ case 1:
+ text = users[0] + ' is typing...';
+ break;
+ default:
+ const last = users.pop();
+ text = users.join(', ') + ' and ' + last + ' are typing...';
+ break;
}
+
+ this.setState({text});
}
render() {
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index f9cd525fd..f7778f25f 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var MessageWrapper = require('./message_wrapper.jsx');
var NotifyCounts = require('./notify_counts.jsx');
+const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
@@ -26,7 +27,9 @@ export default class Navbar extends React.Component {
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.showEditChannelPurposeModal = false;
+ this.state = state;
}
getStateFromStores() {
return {
@@ -106,22 +109,35 @@ export default class Navbar extends React.Component {
</li>
);
- var setChannelDescriptionOption = (
+ var setChannelHeaderOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#edit_channel'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
- Set Channel Description...
+ Set Channel Header...
</a>
</li>
);
+ var setChannelPurposeOption = null;
+ if (!isDirect) {
+ setChannelPurposeOption = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={() => this.setState({showEditChannelPurposeModal: true})}
+ />
+ </li>
+ );
+ }
+
var addMembersOption;
var leaveChannelOption;
if (!isDirect && !ChannelStore.isDefault(channel)) {
@@ -249,7 +265,8 @@ export default class Navbar extends React.Component {
{viewInfoOption}
{addMembersOption}
{manageMembersOption}
- {setChannelDescriptionOption}
+ {setChannelHeaderOption}
+ {setChannelPurposeOption}
{notificationPreferenceOption}
{renameChannelOption}
{deleteChannelOption}
@@ -335,10 +352,10 @@ export default class Navbar extends React.Component {
<Popover
bsStyle='info'
placement='bottom'
- id='description-popover'
+ id='header-popover'
>
<MessageWrapper
- message={channel.description}
+ message={channel.header}
options={{singleline: true, mentionHighlight: false}}
/>
</Popover>
@@ -360,20 +377,20 @@ export default class Navbar extends React.Component {
}
}
- if (channel.description.length === 0) {
+ if (channel.header.length === 0) {
popoverContent = (
<Popover
bsStyle='info'
placement='bottom'
- id='description-popover'
+ id='header-popover'
>
<div>
- {'No channel description yet.'}
+ {'No channel header yet.'}
<br/>
<a
href='#'
data-toggle='modal'
- data-desc={channel.description}
+ data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
data-target='#edit_channel'
@@ -392,17 +409,24 @@ export default class Navbar extends React.Component {
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
return (
- <nav
- className='navbar navbar-default navbar-fixed-top'
- role='navigation'
- >
- <div className='container-fluid theme'>
- <div className='navbar-header'>
- {collapseButtons}
- {channelMenuDropdown}
+ <div>
+ <nav
+ className='navbar navbar-default navbar-fixed-top'
+ role='navigation'
+ >
+ <div className='container-fluid theme'>
+ <div className='navbar-header'>
+ {collapseButtons}
+ {channelMenuDropdown}
+ </div>
</div>
- </div>
- </nav>
+ </nav>
+ <EditChannelPurposeModal
+ show={this.state.showEditChannelPurposeModal}
+ onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
+ channel={channel}
+ />
+ </div>
);
}
}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 2b68645e5..f43bdffdf 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -58,6 +58,7 @@ export default class NavbarDropdown extends React.Component {
TeamStore.addChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
+ $('.sidebar--left .dropdown-menu').scrollTop(0);
this.blockToggle = true;
setTimeout(() => {
this.blockToggle = false;
@@ -103,7 +104,7 @@ export default class NavbarDropdown extends React.Component {
</li>
);
- if (this.props.teamType === 'O') {
+ if (this.props.teamType === Constants.OPEN_TEAM) {
teamLink = (
<li>
<a
@@ -111,7 +112,7 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#get_link'
data-title='Team Invite'
- data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
>
{'Get Team Invite Link'}
</a>
diff --git a/web/react/components/new_channel_flow.jsx b/web/react/components/new_channel_flow.jsx
index 186cfc2b0..d6280d118 100644
--- a/web/react/components/new_channel_flow.jsx
+++ b/web/react/components/new_channel_flow.jsx
@@ -30,7 +30,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
- channelDescription: '',
+ channelPurpose: '',
nameModified: false
};
}
@@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
- channelDescription: '',
+ channelPurpose: '',
nameModified: false
});
}
@@ -65,7 +65,7 @@ export default class NewChannelFlow extends React.Component {
const cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
- channel.description = this.state.channelDescription;
+ channel.purpose = this.state.channelPurpose;
channel.type = this.state.channelType;
Client.createChannel(channel,
@@ -109,7 +109,7 @@ export default class NewChannelFlow extends React.Component {
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
- channelDescription: data.description
+ channelPurpose: data.purpose
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
@@ -119,7 +119,7 @@ export default class NewChannelFlow extends React.Component {
const channelData = {
name: this.state.channelName,
displayName: this.state.channelDisplayName,
- description: this.state.channelDescription
+ purpose: this.state.channelPurpose
};
let showChannelModal = false;
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index 4e6280c99..c0cea496f 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component {
handleChange() {
const newData = {
displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
- description: ReactDOM.findDOMNode(this.refs.channel_desc).value
+ purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value
};
this.props.onDataChanged(newData);
}
@@ -136,22 +136,22 @@ export default class NewChannelModal extends React.Component {
</div>
<div className='form-group less'>
<div className='col-sm-3'>
- <label className='form__label control-label'>{'Description'}</label>
+ <label className='form__label control-label'>{'Purpose'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
<div className='col-sm-9'>
<textarea
className='form-control no-resize'
- ref='channel_desc'
+ ref='channel_purpose'
rows='4'
- placeholder='Description'
- maxLength='1024'
- value={this.props.channelData.description}
+ placeholder='Purpose'
+ maxLength='128'
+ value={this.props.channelData.purpose}
onChange={this.handleChange}
tabIndex='2'
/>
<p className='input__help'>
- {'Description helps others decide whether to join this channel.'}
+ {`Describe how this ${channelTerm} should be used.`}
</p>
{serverError}
</div>
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index 155e88600..f3c0fa0b4 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -3,9 +3,23 @@
var UserStore = require('../stores/user_store.jsx');
var Popover = ReactBootstrap.Popover;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+var Overlay = ReactBootstrap.Overlay;
+const Utils = require('../utils/utils.jsx');
+
+const ChannelStore = require('../stores/channel_store.jsx');
export default class PopoverListMembers extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
+ this.closePopover = this.closePopover.bind(this);
+ }
+
+ componentWillMount() {
+ this.setState({showPopover: false});
+ }
+
componentDidMount() {
const originalLeave = $.fn.popover.Constructor.prototype.leave;
$.fn.popover.Constructor.prototype.leave = function onLeave(obj) {
@@ -27,12 +41,40 @@ export default class PopoverListMembers extends React.Component {
}
};
}
+
+ componentDidUpdate() {
+ $(ReactDOM.findDOMNode(this.refs.memebersPopover)).find('.popover-content').perfectScrollbar();
+ }
+
+ handleShowDirectChannel(teammate, e) {
+ e.preventDefault();
+
+ Utils.openDirectChannelToUser(
+ teammate,
+ (channel, channelAlreadyExisted) => {
+ Utils.switchChannel(channel);
+ if (channelAlreadyExisted) {
+ this.closePopover();
+ }
+ },
+ () => {
+ this.closePopover();
+ }
+ );
+ }
+
+ closePopover() {
+ this.setState({showPopover: false});
+ }
+
render() {
let popoverHtml = [];
let count = 0;
let countText = '-';
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
+ const currentUserId = UserStore.getCurrentId();
+ const ch = ChannelStore.getCurrent();
if (members && teamMembers) {
members.sort((a, b) => {
@@ -40,13 +82,69 @@ export default class PopoverListMembers extends React.Component {
});
members.forEach((m, i) => {
+ const details = [];
+
+ const fullName = Utils.getFullName(m);
+ if (fullName) {
+ details.push(
+ <span
+ key={`${m.id}__full-name`}
+ className='full-name'
+ >
+ {fullName}
+ </span>
+ );
+ }
+
+ if (m.nickname) {
+ const separator = fullName ? ' - ' : '';
+ details.push(
+ <span
+ key={`${m.nickname}__nickname`}
+ >
+ {separator + m.nickname}
+ </span>
+ );
+ }
+
+ let button = '';
+ if (currentUserId !== m.id && ch.type !== 'D') {
+ button = (
+ <a
+ href='#'
+ className='btn-message'
+ onClick={(e) => this.handleShowDirectChannel(m, e)}
+ >
+ {'Message'}
+ </a>
+ );
+ }
+
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
popoverHtml.push(
<div
- className='text--nowrap'
+ className='text-nowrap'
key={'popover-member-' + i}
>
- {m.username}
+
+ <img
+ className='profile-img rounded pull-left'
+ width='26px'
+ height='26px'
+ src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
+ />
+ <div className='pull-left'>
+ <div
+ className='more-name'
+ >
+ {m.username}
+ </div>
+ </div>
+ <div
+ className='pull-right'
+ >
+ {button}
+ </div>
</div>
);
count++;
@@ -61,29 +159,36 @@ export default class PopoverListMembers extends React.Component {
}
return (
- <OverlayTrigger
- trigger='click'
- placement='bottom'
- rootClose={true}
- overlay={
+ <div>
+ <div
+ id='member_popover'
+ ref='member_popover_target'
+ onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
+ >
+ <div>
+ {countText}
+ <span
+ className='fa fa-user'
+ aria-hidden='true'
+ />
+ </div>
+ </div>
+ <Overlay
+ rootClose={true}
+ onHide={this.closePopover}
+ show={this.state.showPopover}
+ target={() => this.state.popoverTarget}
+ placement='bottom'
+ >
<Popover
+ ref='memebersPopover'
title='Members'
id='member-list-popover'
>
{popoverHtml}
</Popover>
- }
- >
- <div id='member_popover'>
- <div>
- {countText}
- <span
- className='fa fa-user'
- aria-hidden='true'
- />
- </div>
+ </Overlay>
</div>
- </OverlayTrigger>
);
}
}
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index dedac8951..c3c5b3e0b 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -204,7 +204,6 @@ export default class Post extends React.Component {
posts={posts}
handleCommentClick={this.handleCommentClick}
retryPost={this.retryPost}
- resize={this.props.resize}
/>
<PostInfo
ref='info'
@@ -228,6 +227,5 @@ Post.propTypes = {
sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
- isLastComment: React.PropTypes.bool,
- resize: React.PropTypes.func
+ isLastComment: React.PropTypes.bool
};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 45eae8c6a..e1f495d54 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -50,7 +50,6 @@ export default class PostBody extends React.Component {
componentDidUpdate() {
this.parseEmojis();
- this.props.resize();
}
componentWillReceiveProps(nextProps) {
@@ -297,7 +296,7 @@ export default class PostBody extends React.Component {
}
let embed;
- if (filenames.length === 0 && this.state.links) {
+ if (filenames.length === 0 && this.state.links && this.state.links.length > 0) {
embed = this.createEmbed(this.state.links[0]);
}
@@ -338,6 +337,5 @@ PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
- handleCommentClick: React.PropTypes.func.isRequired,
- resize: React.PropTypes.func.isRequired
+ handleCommentClick: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index 36260d77c..a01d842e5 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -3,10 +3,9 @@
var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
+var TimeSince = require('./time_since.jsx');
var Constants = require('../utils/constants.jsx');
-var Tooltip = ReactBootstrap.Tooltip;
-var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class PostInfo extends React.Component {
constructor(props) {
@@ -44,7 +43,7 @@ export default class PostInfo extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
- data-refoucsid='#post_textbox'
+ data-refocusid='#post_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
@@ -126,7 +125,7 @@ export default class PostInfo extends React.Component {
lastCommentClass = ' comment-icon__container__show';
}
- if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
+ if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) {
comments = (
<a
href='#'
@@ -144,21 +143,12 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
- let tooltip = <Tooltip id={post.id + 'tooltip'}>{`${utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}`}</Tooltip>;
-
return (
<ul className='post-header post-info'>
<li className='post-header-col'>
- <OverlayTrigger
- delayShow={500}
- container={this}
- placement='top'
- overlay={tooltip}
- >
- <time className='post-profile-time'>
- {utils.displayDateTime(post.create_at)}
- </time>
- </OverlayTrigger>
+ <TimeSince
+ eventTime={post.create_at}
+ />
</li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
deleted file mode 100644
index 3ceef478c..000000000
--- a/web/react/components/post_list.jsx
+++ /dev/null
@@ -1,720 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const Post = require('./post.jsx');
-const UserProfile = require('./user_profile.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
-const LoadingScreen = require('./loading_screen.jsx');
-
-const PostStore = require('../stores/post_store.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-const UserStore = require('../stores/user_store.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
-const PreferenceStore = require('../stores/preference_store.jsx');
-
-const Utils = require('../utils/utils.jsx');
-const Client = require('../utils/client.jsx');
-const Constants = require('../utils/constants.jsx');
-const ActionTypes = Constants.ActionTypes;
-const SocketEvents = Constants.SocketEvents;
-
-const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-
-export default class PostList extends React.Component {
- constructor(props) {
- super(props);
-
- this.gotMorePosts = false;
- this.scrolled = false;
- this.prevScrollTop = 0;
- this.seenNewMessages = false;
- this.isUserScroll = true;
- this.userHasSeenNew = false;
- this.loadInProgress = false;
-
- this.onChange = this.onChange.bind(this);
- this.onTimeChange = this.onTimeChange.bind(this);
- this.onSocketChange = this.onSocketChange.bind(this);
- this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this);
- this.loadMorePosts = this.loadMorePosts.bind(this);
- this.loadFirstPosts = this.loadFirstPosts.bind(this);
- this.activate = this.activate.bind(this);
- this.deactivate = this.deactivate.bind(this);
- this.handleResize = this.handleResize.bind(this);
- this.resizePostList = this.resizePostList.bind(this);
- this.updateScroll = this.updateScroll.bind(this);
-
- const state = this.getStateFromStores(props.channelId);
- state.numToDisplay = Constants.POST_CHUNK_SIZE;
- state.isFirstLoadComplete = false;
- state.windowHeight = Utils.windowHeight();
-
- this.state = state;
- }
- getStateFromStores(id) {
- var postList = PostStore.getPosts(id);
-
- if (postList != null) {
- var deletedPosts = PostStore.getUnseenDeletedPosts(id);
-
- if (deletedPosts && Object.keys(deletedPosts).length > 0) {
- for (var pid in deletedPosts) {
- if (deletedPosts.hasOwnProperty(pid)) {
- postList.posts[pid] = deletedPosts[pid];
- postList.order.unshift(pid);
- }
- }
-
- postList.order.sort((a, b) => {
- if (postList.posts[a].create_at > postList.posts[b].create_at) {
- return -1;
- }
- if (postList.posts[a].create_at < postList.posts[b].create_at) {
- return 1;
- }
- return 0;
- });
- }
-
- var pendingPostList = PostStore.getPendingPosts(id);
-
- if (pendingPostList) {
- postList.order = pendingPostList.order.concat(postList.order);
- for (var ppid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(ppid)) {
- postList.posts[ppid] = pendingPostList.posts[ppid];
- }
- }
- }
- }
-
- return {
- postList
- };
- }
- componentDidMount() {
- window.onload = () => this.scrollToBottom();
- if (this.props.isActive) {
- this.activate();
- this.loadFirstPosts(this.props.channelId);
- }
- }
- componentWillUnmount() {
- this.deactivate();
- }
- activate() {
- this.gotMorePosts = false;
- this.scrolled = false;
- this.prevScrollTop = 0;
- this.seenNewMessages = false;
- this.isUserScroll = true;
- this.userHasSeenNew = false;
-
- PostStore.clearUnseenDeletedPosts(this.props.channelId);
- PostStore.addChangeListener(this.onChange);
- UserStore.addStatusesChangeListener(this.onTimeChange);
- PreferenceStore.addChangeListener(this.onTimeChange);
- SocketStore.addChangeListener(this.onSocketChange);
-
- const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
-
- window.addEventListener('resize', this.handleResize);
-
- postHolder.on('scroll', () => {
- const position = postHolder.scrollTop() + postHolder.height() + 14;
- const bottom = postHolder[0].scrollHeight;
-
- if (position >= bottom) {
- this.scrolled = false;
- } else {
- this.scrolled = true;
- }
-
- if (this.isUserScroll) {
- this.userHasSeenNew = true;
- }
- this.isUserScroll = true;
-
- $('.top-visible-post').removeClass('top-visible-post');
-
- $(ReactDOM.findDOMNode(this.refs.postlistcontent)).children().each(function select() {
- if ($(this).position().top + $(this).height() / 2 > 0) {
- $(this).addClass('top-visible-post');
- return false;
- }
- });
- });
-
- $('.post-list__content div .post').removeClass('post--last');
- $('.post-list__content div:last-child .post').addClass('post--last');
-
- if (!this.state.isFirstLoadComplete) {
- this.loadFirstPosts(this.props.channelId);
- }
-
- this.resizePostList();
- this.onChange();
- this.scrollToBottom();
- }
- deactivate() {
- PostStore.removeChangeListener(this.onChange);
- UserStore.removeStatusesChangeListener(this.onTimeChange);
- SocketStore.removeChangeListener(this.onSocketChange);
- PreferenceStore.removeChangeListener(this.onTimeChange);
- $('body').off('click.userpopover');
-
- window.removeEventListener('resize', this.handleResize);
-
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- postHolder.off('scroll');
- }
- componentDidUpdate(prevProps, prevState) {
- if (!this.props.isActive) {
- return;
- }
-
- if (prevState.windowHeight !== this.state.windowHeight) {
- this.resizePostList();
- if (!this.scrolled) {
- this.scrollToBottom();
- }
- }
-
- $('.post-list__content div .post').removeClass('post--last');
- $('.post-list__content div:last-child .post').addClass('post--last');
-
- if (this.state.postList == null || prevState.postList == null) {
- this.scrollToBottom();
- return;
- }
-
- var order = this.state.postList.order || [];
- var posts = this.state.postList.posts || {};
- var oldOrder = prevState.postList.order || [];
- var oldPosts = prevState.postList.posts || {};
- var userId = UserStore.getCurrentId();
- var firstPost = posts[order[0]] || {};
- var isNewPost = oldOrder.indexOf(order[0]) === -1;
-
- if (this.props.isActive && !prevProps.isActive) {
- this.scrollToBottom();
- } else if (oldOrder.length === 0) {
- this.scrollToBottom();
-
- // the user is scrolled to the bottom
- } else if (!this.scrolled) {
- this.scrollToBottom();
-
- // there's a new post and
- // it's by the user (and not from their webhook) and not a comment
- } else if (isNewPost &&
- userId === firstPost.user_id &&
- !firstPost.props.from_webhook &&
- !Utils.isComment(firstPost)) {
- this.scrollToBottom(true);
-
- // the user clicked 'load more messages'
- } else if (this.gotMorePosts && oldOrder.length > 0) {
- let index;
- if (prevState.numToDisplay >= oldOrder.length) {
- index = oldOrder.length - 1;
- } else {
- index = prevState.numToDisplay;
- }
- const lastPost = oldPosts[oldOrder[index]];
- $('#post_' + lastPost.id)[0].scrollIntoView();
- this.gotMorePosts = false;
- } else {
- this.scrollTo(this.prevScrollTop);
- }
- }
- componentWillUpdate() {
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- this.prevScrollTop = postHolder.scrollTop();
- }
- componentWillReceiveProps(nextProps) {
- if (nextProps.isActive === true && this.props.isActive === false) {
- this.activate();
- } else if (nextProps.isActive === false && this.props.isActive === true) {
- this.deactivate();
- }
- }
- updateScroll() {
- if (!this.scrolled) {
- this.scrollToBottom();
- }
- }
- handleResize() {
- this.setState({
- windowHeight: Utils.windowHeight()
- });
- }
- resizePostList() {
- const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- if ($('#create_post').length > 0) {
- const height = this.state.windowHeight - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
- postHolder.css('height', height + 'px');
- }
- }
- scrollTo(val) {
- this.isUserScroll = false;
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- postHolder[0].scrollTop = val;
- }
- scrollToBottom(force) {
- this.isUserScroll = false;
- var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- if ($('#new_message_' + this.props.channelId)[0] && !this.userHasSeenNew && !force) {
- $('#new_message_' + this.props.channelId)[0].scrollIntoView();
- } else {
- postHolder.addClass('hide-scroll');
- postHolder[0].scrollTop = postHolder[0].scrollHeight;
- postHolder.removeClass('hide-scroll');
- }
- }
- loadFirstPosts(id) {
- if (this.loadInProgress) {
- return;
- }
-
- if (this.props.channelId == null) {
- return;
- }
-
- this.loadInProgress = true;
- Client.getPosts(
- id,
- PostStore.getLatestUpdate(id),
- () => {
- this.loadInProgress = false;
- this.setState({isFirstLoadComplete: true});
- },
- () => {
- this.loadInProgress = false;
- this.setState({isFirstLoadComplete: true});
- }
- );
- }
- onChange() {
- var newState = this.getStateFromStores(this.props.channelId);
-
- if (!Utils.areStatesEqual(newState.postList, this.state.postList)) {
- this.setState(newState);
- }
- }
- onSocketChange(msg) {
- if (msg.action === SocketEvents.POST_DELETED) {
- var activeRoot = $(document.activeElement).closest('.comment-create-body')[0];
- var activeRootPostId = '';
- if (activeRoot && activeRoot.id.length > 0) {
- activeRootPostId = activeRoot.id;
- }
-
- if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) {
- $('#post_deleted').modal('show');
- }
- }
- }
- onTimeChange() {
- if (!this.state.postList) {
- return;
- }
-
- for (var id in this.state.postList.posts) {
- if (!this.refs[id]) {
- continue;
- }
- this.refs[id].forceUpdateInfo();
- }
- }
- createDMIntroMessage(channel) {
- var teammate = Utils.getDirectTeammate(channel.id);
-
- if (teammate) {
- var teammateName = teammate.username;
- if (teammate.nickname.length > 0) {
- teammateName = teammate.nickname;
- }
-
- return (
- <div className='channel-intro'>
- <div className='post-profile-img__container channel-intro-img'>
- <img
- className='post-profile-img'
- src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
- height='50'
- width='50'
- />
- </div>
- <div className='channel-intro-profile'>
- <strong><UserProfile userId={teammate.id} /></strong>
- </div>
- <p className='channel-intro-text'>
- {'This is the start of your direct message history with ' + teammateName + '.'}<br/>
- {'Direct messages and files shared here are not shown to people outside this area.'}
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a description'}
- </a>
- </div>
- );
- }
-
- return (
- <div className='channel-intro'>
- <p className='channel-intro-text'>{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}</p>
- </div>
- );
- }
- createChannelIntroMessage(channel) {
- if (channel.type === 'D') {
- return this.createDMIntroMessage(channel);
- } else if (ChannelStore.isDefault(channel)) {
- return this.createDefaultIntroMessage(channel);
- } else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
- return this.createOffTopicIntroMessage(channel);
- } else if (channel.type === 'O' || channel.type === 'P') {
- return this.createStandardIntroMessage(channel);
- }
- }
- createDefaultIntroMessage(channel) {
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
- <p className='channel-intro__content'>
- {'Welcome to ' + channel.display_name + '!'}
- <br/><br/>
- {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
- <br/><br/>
- {'To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”.'}
- <br/>
- </p>
- </div>
- );
- }
- createOffTopicIntroMessage(channel) {
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
- <p className='channel-intro__content'>
- {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
- <br/>
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a description'}
- </a>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#channel_invite'
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this channel'}
- </a>
- </div>
- );
- }
- getChannelCreator(channel) {
- if (channel.creator_id.length > 0) {
- var creator = UserStore.getProfile(channel.creator_id);
- if (creator) {
- return creator.username;
- }
- }
-
- var members = ChannelStore.getExtraInfo(channel.id).members;
- for (var i = 0; i < members.length; i++) {
- if (Utils.isAdmin(members[i].roles)) {
- return members[i].username;
- }
- }
- }
- createStandardIntroMessage(channel) {
- var uiName = channel.display_name;
- var creatorName = '';
-
- var uiType;
- var memberMessage;
- if (channel.type === 'P') {
- uiType = 'private group';
- memberMessage = ' Only invited members can see this private group.';
- } else {
- uiType = 'channel';
- memberMessage = ' Any member can join and read this channel.';
- }
-
- var createMessage;
- if (creatorName === '') {
- createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + Utils.displayDate(channel.create_at) + '.';
- } else {
- createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{Utils.displayDate(channel.create_at)}</strong></span>);
- }
-
- return (
- <div className='channel-intro'>
- <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4>
- <p className='channel-intro__content'>
- {createMessage}
- {memberMessage}
- <br/>
- </p>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#edit_channel'
- data-desc={channel.description}
- data-title={channel.display_name}
- data-channelid={channel.id}
- >
- <i className='fa fa-pencil'></i>{'Set a description'}
- </a>
- <a
- className='intro-links'
- href='#'
- data-toggle='modal'
- data-target='#channel_invite'
- >
- <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
- </a>
- </div>
- );
- }
- createPosts(posts, order) {
- var postCtls = [];
- var previousPostDay = new Date(0);
- var userId = UserStore.getCurrentId();
-
- var renderedLastViewed = false;
- var lastViewed = Number.MAX_VALUE;
-
- if (ChannelStore.getMember(this.props.channelId) != null) {
- lastViewed = ChannelStore.getMember(this.props.channelId).last_viewed_at;
- }
-
- var numToDisplay = this.state.numToDisplay;
- if (order.length - 1 < numToDisplay) {
- numToDisplay = order.length - 1;
- }
-
- for (var i = numToDisplay; i >= 0; i--) {
- var post = posts[order[i]];
- var parentPost = posts[post.parent_id];
-
- var sameUser = false;
- var sameRoot = false;
- var hideProfilePic = false;
- var prevPost = posts[order[i + 1]];
-
- if (prevPost) {
- sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
-
- sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
-
- // hide the profile pic if:
- // the previous post was made by the same user as the current post,
- // the previous post is not a comment,
- // the current post is not a comment,
- // the current post is not from a webhook
- // and the previous post is not from a webhook
- if ((prevPost.user_id === post.user_id) &&
- !Utils.isComment(prevPost) &&
- !Utils.isComment(post) &&
- (!post.props || !post.props.from_webhook) &&
- (!prevPost.props || !prevPost.props.from_webhook)) {
- hideProfilePic = true;
- }
- }
-
- // check if it's the last comment in a consecutive string of comments on the same post
- // it is the last comment if it is last post in the channel or the next post has a different root post
- var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
-
- var postCtl = (
- <Post
- key={post.id + 'postKey'}
- ref={post.id}
- sameUser={sameUser}
- sameRoot={sameRoot}
- post={post}
- parentPost={parentPost}
- posts={posts}
- hideProfilePic={hideProfilePic}
- isLastComment={isLastComment}
- resize={this.updateScroll}
- />
- );
-
- const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
- if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
- postCtls.push(
- <div
- key={currentPostDay.toDateString()}
- className='date-separator'
- >
- <hr className='separator__hr' />
- <div className='separator__text'>{currentPostDay.toDateString()}</div>
- </div>
- );
- }
-
- if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) {
- renderedLastViewed = true;
-
- // Temporary fix to solve ie11 rendering issue
- let newSeparatorId = '';
- if (!Utils.isBrowserIE()) {
- newSeparatorId = 'new_message_' + this.props.channelId;
- }
- postCtls.push(
- <div
- id={newSeparatorId}
- key='unviewed'
- className='new-separator'
- >
- <hr
- className='separator__hr'
- />
- <div className='separator__text'>{'New Messages'}</div>
- </div>
- );
- }
- postCtls.push(postCtl);
- previousPostDay = currentPostDay;
- }
-
- return postCtls;
- }
- loadMorePosts() {
- if (this.state.postList == null) {
- return;
- }
-
- var posts = this.state.postList.posts;
- var order = this.state.postList.order;
- var channelId = this.props.channelId;
-
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...');
-
- Client.getPostsPage(
- channelId,
- order.length,
- Constants.POST_CHUNK_SIZE,
- function success(data) {
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
- this.gotMorePosts = true;
- this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE});
-
- if (!data) {
- return;
- }
-
- if (data.order.length === 0) {
- return;
- }
-
- var postList = {};
- postList.posts = $.extend(posts, data.posts);
- postList.order = order.concat(data.order);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_POSTS,
- id: channelId,
- post_list: postList
- });
-
- Client.getProfiles();
- }.bind(this),
- function fail(err) {
- $(ReactDOM.findDOMNode(this.refs.loadmore)).text('Load more messages');
- AsyncClient.dispatchError(err, 'getPosts');
- }.bind(this)
- );
- }
- render() {
- var order = [];
- var posts;
- var channel = ChannelStore.get(this.props.channelId);
-
- if (this.state.postList != null) {
- posts = this.state.postList.posts;
- order = this.state.postList.order;
- }
-
- var moreMessages = <p className='beginning-messages-text'>{'Beginning of Channel'}</p>;
- if (channel != null) {
- if (order.length >= this.state.numToDisplay) {
- moreMessages = (
- <a
- ref='loadmore'
- className='more-messages-text theme'
- href='#'
- onClick={this.loadMorePosts}
- >
- {'Load more messages'}
- </a>
- );
- } else {
- moreMessages = this.createChannelIntroMessage(channel);
- }
- }
-
- var postCtls = [];
- if (posts && this.state.isFirstLoadComplete) {
- postCtls = this.createPosts(posts, order);
- } else {
- postCtls.push(
- <LoadingScreen
- position='absolute'
- key='loading'
- />);
- }
-
- var activeClass = '';
- if (!this.props.isActive) {
- activeClass = 'inactive';
- }
-
- return (
- <div
- ref='postlist'
- className={'post-list-holder-by-time ' + activeClass}
- >
- <div className='post-list__table'>
- <div
- ref='postlistcontent'
- className='post-list__content'
- >
- {moreMessages}
- {postCtls}
- </div>
- </div>
- </div>
- );
- }
-}
-
-PostList.defaultProps = {
- isActive: false,
- channelId: null
-};
-PostList.propTypes = {
- isActive: React.PropTypes.bool,
- channelId: React.PropTypes.string
-};
diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx
deleted file mode 100644
index 09cee6218..000000000
--- a/web/react/components/post_list_container.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-const PostList = require('./post_list.jsx');
-const ChannelStore = require('../stores/channel_store.jsx');
-
-export default class PostListContainer extends React.Component {
- constructor() {
- super();
-
- this.onChange = this.onChange.bind(this);
- this.onLeave = this.onLeave.bind(this);
-
- let currentChannelId = ChannelStore.getCurrentId();
- if (currentChannelId) {
- this.state = {currentChannelId: currentChannelId, postLists: [currentChannelId]};
- } else {
- this.state = {currentChannelId: null, postLists: []};
- }
- }
- componentDidMount() {
- ChannelStore.addChangeListener(this.onChange);
- ChannelStore.addLeaveListener(this.onLeave);
- }
- onChange() {
- let channelId = ChannelStore.getCurrentId();
- if (channelId === this.state.currentChannelId) {
- return;
- }
-
- let postLists = this.state.postLists;
- if (postLists.indexOf(channelId) === -1) {
- postLists.push(channelId);
- }
- this.setState({currentChannelId: channelId, postLists: postLists});
- }
- onLeave(id) {
- let postLists = this.state.postLists;
- var index = postLists.indexOf(id);
- if (index !== -1) {
- postLists.splice(index, 1);
- }
- }
- render() {
- let postLists = this.state.postLists;
- let channelId = this.state.currentChannelId;
-
- let postListCtls = [];
- for (let i = 0; i <= this.state.postLists.length - 1; i++) {
- postListCtls.push(
- <PostList
- key={'postlistkey' + i}
- channelId={postLists[i]}
- isActive={postLists[i] === channelId}
- />
- );
- }
-
- return (
- <div>{postListCtls}</div>
- );
- }
-}
diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx
new file mode 100644
index 000000000..2b81d1d79
--- /dev/null
+++ b/web/react/components/posts_view.jsx
@@ -0,0 +1,303 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
+const Post = require('./post.jsx');
+const Constants = require('../utils/constants.jsx');
+
+export default class PostsView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleScroll = this.handleScroll.bind(this);
+ this.isAtBottom = this.isAtBottom.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.createPosts = this.createPosts.bind(this);
+ this.updateScrolling = this.updateScrolling.bind(this);
+ this.handleResize = this.handleResize.bind(this);
+
+ this.jumpToPostNode = null;
+ this.wasAtBottom = true;
+ this.scrollHeight = 0;
+ }
+ static get SCROLL_TYPE_FREE() {
+ return 1;
+ }
+ static get SCROLL_TYPE_BOTTOM() {
+ return 2;
+ }
+ static get SIDEBAR_OPEN() {
+ return 3;
+ }
+ isAtBottom() {
+ return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight);
+ }
+ handleScroll() {
+ // HACK FOR RHS -- REMOVE WHEN RHS DIES
+ const childNodes = this.refs.postlistcontent.childNodes;
+ for (let i = 0; i < childNodes.length; i++) {
+ // If the node is 1/3 down the page
+ if (childNodes[i].offsetTop > (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / 3))) {
+ this.jumpToPostNode = childNodes[i];
+ break;
+ }
+ }
+ this.wasAtBottom = this.isAtBottom();
+
+ // --- --------
+
+ this.props.postViewScrolled(this.isAtBottom());
+ this.prevScrollHeight = this.refs.postlist.scrollHeight;
+ }
+ loadMorePostsTop() {
+ this.props.loadMorePostsTopClicked();
+ }
+ createPosts(posts, order) {
+ const postCtls = [];
+ let previousPostDay = new Date(0);
+ const userId = UserStore.getCurrentId();
+
+ let renderedLastViewed = false;
+
+ let numToDisplay = this.props.numPostsToDisplay;
+ if (order.length - 1 < numToDisplay) {
+ numToDisplay = order.length - 1;
+ }
+
+ for (let i = numToDisplay; i >= 0; i--) {
+ const post = posts[order[i]];
+ const parentPost = posts[post.parent_id];
+ const prevPost = posts[order[i + 1]];
+
+ // If the post is a comment whose parent has been deleted, don't add it to the list.
+ if (parentPost && parentPost.state === Constants.POST_DELETED) {
+ continue;
+ }
+
+ let sameUser = false;
+ let sameRoot = false;
+ let hideProfilePic = false;
+
+ if (prevPost) {
+ sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5;
+
+ sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id);
+
+ // hide the profile pic if:
+ // the previous post was made by the same user as the current post,
+ // the previous post is not a comment,
+ // the current post is not a comment,
+ // the current post is not from a webhook
+ // and the previous post is not from a webhook
+ if ((prevPost.user_id === post.user_id) &&
+ !Utils.isComment(prevPost) &&
+ !Utils.isComment(post) &&
+ (!post.props || !post.props.from_webhook) &&
+ (!prevPost.props || !prevPost.props.from_webhook)) {
+ hideProfilePic = true;
+ }
+ }
+
+ // check if it's the last comment in a consecutive string of comments on the same post
+ // it is the last comment if it is last post in the channel or the next post has a different root post
+ var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
+
+ var postCtl = (
+ <Post
+ key={post.id + 'postKey'}
+ ref={post.id}
+ sameUser={sameUser}
+ sameRoot={sameRoot}
+ post={post}
+ parentPost={parentPost}
+ posts={posts}
+ hideProfilePic={hideProfilePic}
+ isLastComment={isLastComment}
+ />
+ );
+
+ const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
+ if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
+ postCtls.push(
+ <div
+ key={currentPostDay.toDateString()}
+ className='date-separator'
+ >
+ <hr className='separator__hr' />
+ <div className='separator__text'>{currentPostDay.toDateString()}</div>
+ </div>
+ );
+ }
+
+ if (post.user_id !== userId &&
+ this.props.messageSeparatorTime !== 0 &&
+ post.create_at > this.props.messageSeparatorTime &&
+ !renderedLastViewed) {
+ renderedLastViewed = true;
+
+ // Temporary fix to solve ie11 rendering issue
+ let newSeparatorId = '';
+ if (!Utils.isBrowserIE()) {
+ newSeparatorId = 'new_message_' + post.id;
+ }
+ postCtls.push(
+ <div
+ id={newSeparatorId}
+ key='unviewed'
+ className='new-separator'
+ >
+ <hr
+ className='separator__hr'
+ />
+ <div className='separator__text'>{'New Messages'}</div>
+ </div>
+ );
+ }
+ postCtls.push(postCtl);
+ previousPostDay = currentPostDay;
+ }
+
+ return postCtls;
+ }
+ updateScrolling() {
+ if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) {
+ window.requestAnimationFrame(() => {
+ this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
+ });
+ } else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPost) {
+ window.requestAnimationFrame(() => {
+ const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPost]);
+ postNode.scrollIntoView();
+ if (this.refs.postlist.scrollTop === postNode.offsetTop) {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
+ } else {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
+ }
+ });
+ } else if (this.props.scrollType === PostsView.SIDEBAR_OPEN) {
+ // If we are at the bottom then stay there
+ if (this.wasAtBottom) {
+ this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
+ } else {
+ window.requestAnimationFrame(() => {
+ this.jumpToPostNode.scrollIntoView();
+ if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
+ } else {
+ this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop);
+ }
+ });
+ }
+ } else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
+ window.requestAnimationFrame(() => {
+ this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
+ });
+ }
+ }
+ handleResize() {
+ this.updateScrolling();
+ }
+ componentDidMount() {
+ this.updateScrolling();
+ window.addEventListener('resize', this.handleResize);
+ }
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize);
+ }
+ componentDidUpdate() {
+ this.updateScrolling();
+ }
+ shouldComponentUpdate(nextProps) {
+ if (this.props.isActive !== nextProps.isActive) {
+ return true;
+ }
+ if (this.props.postList !== nextProps.postList) {
+ return true;
+ }
+ if (this.props.scrollPost !== nextProps.scrollPost) {
+ return true;
+ }
+ if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
+ return true;
+ }
+ if (this.props.numPostsToDisplay !== nextProps.numPostsToDisplay) {
+ return true;
+ }
+ if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
+ return true;
+ }
+ if (!Utils.areStatesEqual(this.props.postList, nextProps.postList)) {
+ return true;
+ }
+
+ return false;
+ }
+ render() {
+ let posts = [];
+ let order = [];
+ let moreMessages;
+ let postElements;
+ let activeClass = 'inactive';
+ if (this.props.postList != null) {
+ posts = this.props.postList.posts;
+ order = this.props.postList.order;
+
+ // Create intro message or top loadmore link
+ if (order.length >= this.props.numPostsToDisplay) {
+ moreMessages = (
+ <a
+ ref='loadmore'
+ className='more-messages-text theme'
+ href='#'
+ onClick={this.loadMorePostsTop}
+ >
+ {'Load more messages'}
+ </a>
+ );
+ } else {
+ moreMessages = this.props.introText;
+ }
+
+ // Create post elements
+ postElements = this.createPosts(posts, order);
+
+ // Show ourselves if we are marked active
+ if (this.props.isActive) {
+ activeClass = '';
+ }
+ }
+
+ return (
+ <div
+ ref='postlist'
+ className={'post-list-holder-by-time ' + activeClass}
+ onScroll={this.handleScroll}
+ >
+ <div className='post-list__table'>
+ <div
+ ref='postlistcontent'
+ className='post-list__content'
+ >
+ {moreMessages}
+ {postElements}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+PostsView.defaultProps = {
+};
+
+PostsView.propTypes = {
+ isActive: React.PropTypes.bool,
+ postList: React.PropTypes.object,
+ scrollPost: React.PropTypes.string,
+ scrollType: React.PropTypes.number,
+ postViewScrolled: React.PropTypes.func.isRequired,
+ loadMorePostsTopClicked: React.PropTypes.func.isRequired,
+ numPostsToDisplay: React.PropTypes.number,
+ introText: React.PropTypes.element,
+ messageSeparatorTime: React.PropTypes.number
+};
diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx
new file mode 100644
index 000000000..7671ca01d
--- /dev/null
+++ b/web/react/components/posts_view_container.jsx
@@ -0,0 +1,267 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const PostsView = require('./posts_view.jsx');
+const LoadingScreen = require('./loading_screen.jsx');
+
+const ChannelStore = require('../stores/channel_store.jsx');
+const PostStore = require('../stores/post_store.jsx');
+
+const Utils = require('../utils/utils.jsx');
+const Client = require('../utils/client.jsx');
+const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+
+import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx';
+
+export default class PostsViewContainer extends React.Component {
+ constructor() {
+ super();
+
+ this.onChannelChange = this.onChannelChange.bind(this);
+ this.onChannelLeave = this.onChannelLeave.bind(this);
+ this.onPostsChange = this.onPostsChange.bind(this);
+ this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
+ this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
+ this.postsLoaded = this.postsLoaded.bind(this);
+ this.postsLoadedFailure = this.postsLoadedFailure.bind(this);
+ this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
+
+ const currentChannelId = ChannelStore.getCurrentId();
+ const state = {
+ scrollType: PostsView.SCROLL_TYPE_BOTTOM,
+ scrollPost: null,
+ numPostsToDisplay: Constants.POST_CHUNK_SIZE
+ };
+ if (currentChannelId) {
+ Object.assign(state, {
+ currentChannelIndex: 0,
+ channels: [currentChannelId],
+ postLists: [this.getChannelPosts(currentChannelId)]
+ });
+ } else {
+ Object.assign(state, {
+ currentChannelIndex: null,
+ channels: [],
+ postLists: []
+ });
+ }
+
+ this.state = state;
+ }
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.onChannelChange);
+ ChannelStore.addLeaveListener(this.onChannelLeave);
+ PostStore.addChangeListener(this.onPostsChange);
+ PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest);
+ }
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.onChannelChange);
+ ChannelStore.removeLeaveListener(this.onChannelLeave);
+ PostStore.removeChangeListener(this.onPostsChange);
+ PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest);
+ }
+ handlePostsViewJumpRequest(type, post) {
+ switch (type) {
+ case Constants.PostsViewJumpTypes.BOTTOM:
+ this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
+ break;
+ case Constants.PostsViewJumpTypes.POST:
+ this.setState({
+ scrollType: PostsView.SCROLL_TYPE_POST,
+ scrollPost: post
+ });
+ break;
+ case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
+ this.setState({scrollType: PostsView.SIDEBAR_OPEN});
+ break;
+ }
+ }
+ onChannelChange() {
+ const postLists = Object.assign({}, this.state.postLists);
+ const channels = this.state.channels.slice();
+ const channelId = ChannelStore.getCurrentId();
+
+ // Has the channel really changed?
+ if (channelId === channels[this.state.currentChannelIndex]) {
+ return;
+ }
+
+ PostStore.clearUnseenDeletedPosts(channelId);
+
+ let lastViewed = Number.MAX_VALUE;
+ const member = ChannelStore.getMember(channelId);
+ if (member != null) {
+ lastViewed = member.last_viewed_at;
+ }
+
+ let newIndex = channels.indexOf(channelId);
+ if (newIndex === -1) {
+ newIndex = channels.length;
+ channels.push(channelId);
+ postLists[newIndex] = this.getChannelPosts(channelId);
+ }
+ this.setState({
+ currentChannelIndex: newIndex,
+ currentLastViewed: lastViewed,
+ scrollType: PostsView.SCROLL_TYPE_BOTTOM,
+ channels,
+ postLists});
+ }
+ onChannelLeave(id) {
+ const postLists = Object.assign({}, this.state.postLists);
+ const channels = this.state.channels.slice();
+ const index = channels.indexOf(id);
+ if (index !== -1) {
+ postLists.splice(index, 1);
+ channels.splice(index, 1);
+ }
+ this.setState({channels, postLists});
+ }
+ onPostsChange() {
+ const channels = this.state.channels;
+ const postLists = Object.assign({}, this.state.postLists);
+ const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]);
+
+ postLists[this.state.currentChannelIndex] = newPostsView;
+ this.setState({postLists});
+ }
+ getChannelPosts(id) {
+ const postList = PostStore.getPosts(id);
+
+ if (postList != null) {
+ const deletedPosts = PostStore.getUnseenDeletedPosts(id);
+
+ if (deletedPosts && Object.keys(deletedPosts).length > 0) {
+ for (const pid in deletedPosts) {
+ if (deletedPosts.hasOwnProperty(pid)) {
+ postList.posts[pid] = deletedPosts[pid];
+ postList.order.unshift(pid);
+ }
+ }
+
+ postList.order.sort((a, b) => {
+ if (postList.posts[a].create_at > postList.posts[b].create_at) {
+ return -1;
+ }
+ if (postList.posts[a].create_at < postList.posts[b].create_at) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ const pendingPostList = PostStore.getPendingPosts(id);
+
+ if (pendingPostList) {
+ postList.order = pendingPostList.order.concat(postList.order);
+ for (const ppid in pendingPostList.posts) {
+ if (pendingPostList.posts.hasOwnProperty(ppid)) {
+ postList.posts[ppid] = pendingPostList.posts[ppid];
+ }
+ }
+ }
+ }
+
+ return postList;
+ }
+ loadMorePostsTop() {
+ const postLists = this.state.postLists;
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const currentPostList = postLists[this.state.currentChannelIndex];
+
+ this.setState({numPostsToDisplay: this.state.numPostsToDisplay + Constants.POST_CHUNK_SIZE});
+
+ Client.getPostsPage(
+ currentChannelId,
+ currentPostList.order.length,
+ Constants.POST_CHUNK_SIZE,
+ this.postsLoaded,
+ this.postsLoadedFailure
+ );
+ }
+ postsLoaded(data) {
+ if (!data) {
+ return;
+ }
+
+ if (data.order.length === 0) {
+ return;
+ }
+
+ const postLists = this.state.postLists;
+ const currentPostList = postLists[this.state.currentChannelIndex];
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+
+ var newPostList = {};
+ newPostList.posts = Object.assign(currentPostList.posts, data.posts);
+ newPostList.order = currentPostList.order.concat(data.order);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_POSTS,
+ id: currentChannelId,
+ post_list: newPostList
+ });
+
+ Client.getProfiles();
+ }
+ postsLoadedFailure(err) {
+ AsyncClient.dispatchError(err, 'getPosts');
+ }
+ handlePostsViewScroll(atBottom) {
+ if (atBottom) {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
+ } else {
+ this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
+ }
+ }
+ shouldComponentUpdate(nextProps, nextState) {
+ if (Utils.areStatesEqual(this.state, nextState)) {
+ return false;
+ }
+
+ return true;
+ }
+ render() {
+ const postLists = this.state.postLists;
+ const channels = this.state.channels;
+ const currentChannelId = channels[this.state.currentChannelIndex];
+ const channel = ChannelStore.get(currentChannelId);
+
+ const postListCtls = [];
+ for (let i = 0; i < channels.length; i++) {
+ const isActive = (channels[i] === currentChannelId);
+ postListCtls.push(
+ <PostsView
+ key={'postsviewkey' + i}
+ isActive={isActive}
+ postList={postLists[i]}
+ scrollType={this.state.scrollType}
+ scrollPost={this.state.scrollPost}
+ postViewScrolled={this.handlePostsViewScroll}
+ loadMorePostsTopClicked={this.loadMorePostsTop}
+ numPostsToDisplay={this.state.numPostsToDisplay}
+ introText={channel ? createChannelIntroMessage(channel) : null}
+ messageSeparatorTime={this.state.currentLastViewed}
+ />
+ );
+ if ((!postLists[i] || !channel) && isActive) {
+ postListCtls.push(
+ <LoadingScreen
+ position='absolute'
+ key='loading'
+ />
+ );
+ }
+ }
+
+ return (
+ <div>{postListCtls}</div>
+ );
+ }
+}
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index 3d4d9bf45..c40409dcc 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -96,75 +96,74 @@ export default class RegisterAppModal extends React.Component {
var body = '';
if (this.state.clientId === '') {
body = (
- <div className='form-group user-settings'>
- <h3>{'Register a New Application'}</h3>
- <br/>
- <label className='col-sm-4 control-label'>{'Application Name'}</label>
- <div className='col-sm-7'>
- <input
- ref='name'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {nameError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
- <div className='col-sm-7'>
- <input
- ref='homepage'
- className='form-control'
- type='text'
- placeholder='Required'
- />
- {homepageError}
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Description'}</label>
- <div className='col-sm-7'>
- <input
- ref='desc'
- className='form-control'
- type='text'
- placeholder='Optional'
- />
- </div>
- <br/>
- <br/>
- <label className='col-sm-4 control-label'>{'Callback URL'}</label>
- <div className='col-sm-7'>
- <textarea
- ref='callback'
- className='form-control'
- type='text'
- placeholder='Required'
- rows='5'
- />
- {callbackError}
+ <div className='settings-modal'>
+ <div className='form-horizontal user-settings'>
+ <h4 className='padding-bottom x3'>{'Register a New Application'}</h4>
+ <div className='row'>
+ <label className='col-sm-4 control-label'>{'Application Name'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='name'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {nameError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Homepage URL'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='homepage'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ />
+ {homepageError}
+ </div>
+ </div>
+ <div className='row padding-top x2'>
+ <label className='col-sm-4 control-label'>{'Description'}</label>
+ <div className='col-sm-7'>
+ <input
+ ref='desc'
+ className='form-control'
+ type='text'
+ placeholder='Optional'
+ />
+ </div>
+ </div>
+ <div className='row padding-top padding-bottom x2'>
+ <label className='col-sm-4 control-label'>{'Callback URL'}</label>
+ <div className='col-sm-7'>
+ <textarea
+ ref='callback'
+ className='form-control'
+ type='text'
+ placeholder='Required'
+ rows='5'
+ />
+ {callbackError}
+ </div>
+ </div>
+ {serverError}
+ <hr />
+ <a
+ className='btn btn-sm theme pull-right'
+ href='#'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ {'Cancel'}
+ </a>
+ <a
+ className='btn btn-sm btn-primary pull-right'
+ onClick={this.register}
+ >
+ {'Register'}
+ </a>
</div>
- <br/>
- <br/>
- <br/>
- <br/>
- <br/>
- {serverError}
- <a
- className='btn btn-sm theme pull-right'
- href='#'
- data-dismiss='modal'
- aria-label='Close'
- >
- {'Cancel'}
- </a>
- <a
- className='btn btn-sm btn-primary pull-right'
- onClick={this.register}
- >
- {'Register'}
- </a>
</div>
);
} else {
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index cfff04fa2..8c6324c72 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -93,6 +93,7 @@ export default class RhsComment extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
+ data-refocusid='#reply_textbox'
data-title='Comment'
data-message={post.message}
data-postid={post.id}
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index deef389e2..21e52b438 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -79,6 +79,7 @@ export default class RhsRootPost extends React.Component {
role='menuitem'
data-toggle='modal'
data-target='#edit_post'
+ data-refocusid='#reply_textbox'
data-title={type}
data-message={post.message}
data-postid={post.id}
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index bcdec2870..fe57bed28 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -34,12 +34,12 @@ export default class RhsThread extends React.Component {
}
var channelId = postList.posts[postList.order[0]].channel_id;
- var pendingPostList = PostStore.getPendingPosts(channelId);
+ var pendingPostsList = PostStore.getPendingPosts(channelId);
- if (pendingPostList) {
- for (var pid in pendingPostList.posts) {
- if (pendingPostList.posts.hasOwnProperty(pid)) {
- postList.posts[pid] = pendingPostList.posts[pid];
+ if (pendingPostsList) {
+ for (var pid in pendingPostsList.posts) {
+ if (pendingPostsList.posts.hasOwnProperty(pid)) {
+ postList.posts[pid] = pendingPostsList.posts[pid];
}
}
}
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
index 03c7b894c..03e14ec49 100644
--- a/web/react/components/search_autocomplete.jsx
+++ b/web/react/components/search_autocomplete.jsx
@@ -10,6 +10,7 @@ const patterns = new Map([
['channels', /\b(?:in|channel):\s*(\S*)$/i],
['users', /\bfrom:\s*(\S*)$/i]
]);
+const Popover = ReactBootstrap.Popover;
export default class SearchAutocomplete extends React.Component {
constructor(props) {
@@ -36,6 +37,11 @@ export default class SearchAutocomplete extends React.Component {
$(document).on('click', this.handleDocumentClick);
}
+ componentDidUpdate() {
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar();
+ $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200);
+ }
+
componentWillUnmount() {
$(document).off('click', this.handleDocumentClick);
}
@@ -136,7 +142,10 @@ export default class SearchAutocomplete extends React.Component {
let channels = ChannelStore.getAll();
if (filter) {
- channels = channels.filter((channel) => channel.name.startsWith(filter));
+ channels = channels.filter((channel) => channel.name.startsWith(filter) && channel.type !== 'D');
+ } else {
+ // don't show direct channels
+ channels = channels.filter((channel) => channel.type !== 'D');
}
channels.sort((a, b) => a.name.localeCompare(b.name));
@@ -193,7 +202,7 @@ export default class SearchAutocomplete extends React.Component {
if (this.state.mode === 'channels') {
suggestions = this.state.suggestions.map((channel, index) => {
- let className = 'search-autocomplete__channel';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -211,7 +220,7 @@ export default class SearchAutocomplete extends React.Component {
});
} else if (this.state.mode === 'users') {
suggestions = this.state.suggestions.map((user, index) => {
- let className = 'search-autocomplete__user';
+ let className = 'search-autocomplete__item';
if (this.state.selection === index) {
className += ' selected';
}
@@ -224,7 +233,7 @@ export default class SearchAutocomplete extends React.Component {
className={className}
>
<img
- className='profile-img'
+ className='profile-img rounded'
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
/>
{user.username}
@@ -234,12 +243,15 @@ export default class SearchAutocomplete extends React.Component {
}
return (
- <div
- ref='container'
- className='search-autocomplete'
+ <Popover
+ ref='searchPopover'
+ onShow={this.componentDidMount}
+ id='search-autocomplete__popover'
+ className='search-help-popover autocomplete visible'
+ placement='bottom'
>
{suggestions}
- </div>
+ </Popover>
);
}
}
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index e1d36ad7d..90865475b 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -3,7 +3,7 @@
var client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx');
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
@@ -30,17 +30,17 @@ export default class SearchBar extends React.Component {
this.state = state;
}
getSearchTermStateFromStores() {
- var term = PostStore.getSearchTerm() || '';
+ var term = SearchStore.getSearchTerm() || '';
return {
searchTerm: term
};
}
componentDidMount() {
- PostStore.addSearchTermChangeListener(this.onListenerChange);
+ SearchStore.addSearchTermChangeListener(this.onListenerChange);
this.mounted = true;
}
componentWillUnmount() {
- PostStore.removeSearchTermChangeListener(this.onListenerChange);
+ SearchStore.removeSearchTermChangeListener(this.onListenerChange);
this.mounted = false;
}
onListenerChange(doSearch, isMentionSearch) {
@@ -84,20 +84,16 @@ export default class SearchBar extends React.Component {
}
handleUserInput(e) {
var term = e.target.value;
- PostStore.storeSearchTerm(term);
- PostStore.emitSearchTermChange(false);
+ SearchStore.storeSearchTerm(term);
+ SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: term});
this.refs.autocomplete.handleInputChange(e.target, term);
}
- handleMouseInput(e) {
- e.preventDefault();
- }
handleUserBlur() {
this.setState({focused: false});
}
- handleUserFocus(e) {
- e.target.select();
+ handleUserFocus() {
$('.search-bar__container').addClass('focused');
this.setState({focused: true});
@@ -105,6 +101,7 @@ export default class SearchBar extends React.Component {
performSearch(terms, isMentionSearch) {
if (terms.length) {
this.setState({isSearching: true});
+
client.search(
terms,
(data) => {
@@ -143,8 +140,8 @@ export default class SearchBar extends React.Component {
textbox.value = text;
utils.setCaretPosition(textbox, preText.length + word.length);
- PostStore.storeSearchTerm(text);
- PostStore.emitSearchTermChange(false);
+ SearchStore.storeSearchTerm(text);
+ SearchStore.emitSearchTermChange(false);
this.setState({searchTerm: text});
}
@@ -178,6 +175,7 @@ export default class SearchBar extends React.Component {
className='search__form relative-div'
onSubmit={this.handleSubmit}
style={{overflow: 'visible'}}
+ autoComplete='off'
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
<input
@@ -190,7 +188,6 @@ export default class SearchBar extends React.Component {
onBlur={this.handleUserBlur}
onChange={this.handleUserInput}
onKeyDown={this.handleKeyDown}
- onMouseUp={this.handleMouseInput}
/>
{isSearching}
<SearchAutocomplete
diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx
index 30e15d0ad..ce19c48f0 100644
--- a/web/react/components/search_results.jsx
+++ b/web/react/components/search_results.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var SearchBox = require('./search_bar.jsx');
var Utils = require('../utils/utils.jsx');
@@ -9,7 +9,7 @@ var SearchResultsHeader = require('./search_results_header.jsx');
var SearchResultsItem = require('./search_results_item.jsx');
function getStateFromStores() {
- return {results: PostStore.getSearchResults()};
+ return {results: SearchStore.getSearchResults()};
}
export default class SearchResults extends React.Component {
@@ -30,7 +30,7 @@ export default class SearchResults extends React.Component {
componentDidMount() {
this.mounted = true;
- PostStore.addSearchChangeListener(this.onChange);
+ SearchStore.addSearchChangeListener(this.onChange);
this.resize();
window.addEventListener('resize', this.handleResize);
}
@@ -40,7 +40,7 @@ export default class SearchResults extends React.Component {
}
componentWillUnmount() {
- PostStore.removeSearchChangeListener(this.onChange);
+ SearchStore.removeSearchChangeListener(this.onChange);
this.mounted = false;
window.removeEventListener('resize', this.handleResize);
}
@@ -78,7 +78,7 @@ export default class SearchResults extends React.Component {
searchForm = <SearchBox />;
}
var noResults = (!results || !results.order || !results.order.length);
- var searchTerm = PostStore.getSearchTerm();
+ var searchTerm = SearchStore.getSearchTerm();
var ctls = null;
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index d212e47a3..a8bd4db2c 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var UserProfile = require('./user_profile.jsx');
@@ -32,7 +32,7 @@ export default class SearchResultsItem extends React.Component {
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_POST_SELECTED,
post_list: data,
- from_search: PostStore.getSearchTerm()
+ from_search: SearchStore.getSearchTerm()
});
AppDispatcher.handleServerAction({
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index 774f98a43..d6c4b0d4b 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -35,8 +35,10 @@ export default class SettingItemMax extends React.Component {
var widthClass;
if (this.props.width === 'full') {
widthClass = 'col-sm-12';
- } else {
+ } else if (this.props.width === 'medium') {
widthClass = 'col-sm-10 col-sm-offset-2';
+ } else {
+ widthClass = 'col-sm-9 col-sm-offset-3';
}
return (
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx
index b6bcb13a6..e69412cca 100644
--- a/web/react/components/setting_picture.jsx
+++ b/web/react/components/setting_picture.jsx
@@ -42,7 +42,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src=''
/>
);
@@ -50,7 +50,7 @@ export default class SettingPicture extends React.Component {
img = (
<img
ref='image'
- className='profile-img'
+ className='profile-img rounded'
src={this.props.src}
/>
);
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index ed2c84057..c47919885 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -1,19 +1,26 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-const AsyncClient = require('../utils/async_client.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 MoreDirectChannels = require('./more_direct_channels.jsx');
const SearchBox = require('./search_bar.jsx');
const SidebarHeader = require('./sidebar_header.jsx');
-const TeamStore = require('../stores/team_store.jsx');
const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
+const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
+const TeamStore = require('../stores/team_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+
+const AsyncClient = require('../utils/async_client.jsx');
+const Client = require('../utils/client.jsx');
const Utils = require('../utils/utils.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
+
const Tooltip = ReactBootstrap.Tooltip;
const OverlayTrigger = ReactBootstrap.OverlayTrigger;
@@ -40,6 +47,9 @@ export default class Sidebar extends React.Component {
this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this);
this.createChannelElement = this.createChannelElement.bind(this);
+ this.updateTitle = this.updateTitle.bind(this);
+ this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this);
+ this.getUnreadCount = this.getUnreadCount.bind(this);
this.isLeaving = new Map();
@@ -48,8 +58,45 @@ export default class Sidebar extends React.Component {
state.showDirectChannelsModal = false;
state.loadingDMChannel = -1;
state.windowWidth = Utils.windowWidth();
-
this.state = state;
+
+ this.unreadCountPerChannel = {};
+ this.setUnreadCountPerChannel();
+ }
+ setUnreadCountPerChannel() {
+ const channels = ChannelStore.getAll();
+ const members = ChannelStore.getAllMembers();
+ const channelUnreadCounts = {};
+
+ channels.forEach((ch) => {
+ const chMember = members[ch.id];
+ let chMentionCount = chMember.mention_count;
+ let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount;
+
+ if (ch.type === 'D') {
+ chMentionCount = chUnreadCount;
+ chUnreadCount = 0;
+ }
+
+ channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount};
+ });
+
+ this.unreadCountPerChannel = channelUnreadCounts;
+ }
+ getUnreadCount(channelId) {
+ let mentions = 0;
+ let msgs = 0;
+
+ if (channelId) {
+ return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions};
+ }
+
+ Object.keys(this.unreadCountPerChannel).forEach((chId) => {
+ msgs += this.unreadCountPerChannel[chId].msgs;
+ mentions += this.unreadCountPerChannel[chId].mentions;
+ });
+
+ return {msgs, mentions};
}
getStateFromStores() {
const members = ChannelStore.getAllMembers();
@@ -96,7 +143,7 @@ export default class Sidebar extends React.Component {
channel.type = 'D';
}
- channel.display_name = teammate.username;
+ channel.display_name = Utils.displayUsername(teammate.id);
channel.teammate_id = teammate.id;
channel.status = UserStore.getStatus(teammate.id);
@@ -115,12 +162,15 @@ export default class Sidebar extends React.Component {
visibleDirectChannels.sort(this.sortChannelsByDisplayName);
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
return {
activeId: currentId,
channels: ChannelStore.getAll(),
members,
visibleDirectChannels,
- hiddenDirectChannelCount
+ hiddenDirectChannelCount,
+ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER
};
}
@@ -138,10 +188,6 @@ export default class Sidebar extends React.Component {
window.addEventListener('resize', this.handleResize);
}
shouldComponentUpdate(nextProps, nextState) {
- if (!Utils.areStatesEqual(nextProps, this.props)) {
- return true;
- }
-
if (!Utils.areStatesEqual(nextState, this.state)) {
return true;
}
@@ -192,7 +238,10 @@ export default class Sidebar extends React.Component {
currentChannelName = Utils.getDirectTeammate(channel.id).username;
}
- document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName;
+ const unread = this.getUnreadCount();
+ const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : '';
+ const unreadTitle = unread.msgs > 0 ? '* ' : '';
+ document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName;
}
}
onScroll() {
@@ -269,10 +318,56 @@ export default class Sidebar extends React.Component {
this.setState({showDirectChannelsModal: false});
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Channels'}</h4>
+ <p><strong>{'Channels'}</strong>{' organize conversations across different topics. They’re open to everyone on your team. To send private communications use '}<strong>{'Direct Messages'}</strong>{' for a single person or '}<strong>{'Private Groups'}</strong>{' for multiple people.'}
+ </p>
+ </div>
+ );
+
+ screens.push(
+ <div>
+ <h4>{'"Town Square" and "Off-Topic" channels'}</h4>
+ <p>{'Here are two public channels to start:'}</p>
+ <p>
+ <strong>{'Town Square'}</strong>{' is a place for team-wide communication. Everyone in your team is a member of this channel.'}
+ </p>
+ <p>
+ <strong>{'Off-Topic'}</strong>{' is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.'}
+ </p>
+ </div>
+ );
+
+ screens.push(
+ <div>
+ <h4>{'Creating and Joining Channels'}</h4>
+ <p>
+ {'Click '}<strong>{'"More..."'}</strong>{' to create a new channel or join an existing one.'}
+ </p>
+ <p>
+ {'You can also create a new channel or private group by clicking the '}<strong>{'"+" symbol'}</strong>{' next to the channel or private group header.'}
+ </p>
+ </div>
+ );
+
+ return (
+ <TutorialTip
+ placement='right'
+ screens={screens}
+ overlayClass='tip-overlay--sidebar'
+ />
+ );
+ }
+
createChannelElement(channel, index, arr, handleClose) {
var members = this.state.members;
var activeId = this.state.activeId;
var channelMember = members[channel.id];
+ var unreadCount = this.getUnreadCount(channel.id);
var msgCount;
var linkClass = '';
@@ -284,7 +379,7 @@ export default class Sidebar extends React.Component {
var unread = false;
if (channelMember) {
- msgCount = channel.total_msg_count - channelMember.msg_count;
+ msgCount = unreadCount.msgs + unreadCount.mentions;
unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0;
}
@@ -301,16 +396,8 @@ export default class Sidebar extends React.Component {
var badge = null;
if (channelMember) {
- if (channel.type === 'D') {
- // direct message channels show badges for any number of unread posts
- msgCount = channel.total_msg_count - channelMember.msg_count;
- if (msgCount > 0) {
- badge = <span className='badge pull-right small'>{msgCount}</span>;
- this.badgesActive = true;
- }
- } else if (channelMember.mention_count > 0) {
- // public and private channels only show badges for mentions
- badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>;
+ if (unreadCount.mentions) {
+ badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>;
this.badgesActive = true;
}
} else if (this.state.loadingDMChannel === index && channel.type === 'D') {
@@ -412,6 +499,11 @@ export default class Sidebar extends React.Component {
rowClass += ' has-close';
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip && channel.name === Constants.DEFAULT_CHANNEL) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<li
key={channel.name}
@@ -428,12 +520,15 @@ export default class Sidebar extends React.Component {
{badge}
{closeButton}
</a>
+ {tutorialTip}
</li>
);
}
render() {
this.badgesActive = false;
+ this.setUnreadCountPerChannel();
+
// keep track of the first and last unread channels so we can use them to set the unread indicators
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
@@ -505,9 +600,9 @@ export default class Sidebar extends React.Component {
/>
<SidebarHeader
- teamDisplayName={this.props.teamDisplayName}
- teamName={this.props.teamName}
- teamType={this.props.teamType}
+ teamDisplayName={TeamStore.getCurrent().display_name}
+ teamName={TeamStore.getCurrent().name}
+ teamType={TeamStore.getCurrent().type}
/>
<SearchBox />
@@ -593,11 +688,6 @@ export default class Sidebar extends React.Component {
}
Sidebar.defaultProps = {
- teamType: '',
- teamDisplayName: ''
};
Sidebar.propTypes = {
- teamType: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- teamName: React.PropTypes.string
};
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index de28a8374..3f777d93c 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -1,17 +1,42 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var NavbarDropdown = require('./navbar_dropdown.jsx');
-var UserStore = require('../stores/user_store.jsx');
+const NavbarDropdown = require('./navbar_dropdown.jsx');
+const TutorialTip = require('./tutorial/tutorial_tip.jsx');
+
+const UserStore = require('../stores/user_store.jsx');
+const PreferenceStore = require('../stores/preference_store.jsx');
+
const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+const TutorialSteps = Constants.TutorialSteps;
+
+const Tooltip = ReactBootstrap.Tooltip;
+const OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class SidebarHeader extends React.Component {
constructor(props) {
super(props);
this.toggleDropdown = this.toggleDropdown.bind(this);
+ this.onPreferenceChange = this.onPreferenceChange.bind(this);
+
+ this.state = this.getStateFromStores();
+ }
+ componentDidMount() {
+ PreferenceStore.addChangeListener(this.onPreferenceChange);
+ }
+ componentWillUnmount() {
+ PreferenceStore.removeChangeListener(this.onPreferenceChange);
+ }
+ getStateFromStores() {
+ const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
- this.state = {};
+ return {showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.MENU_POPOVER};
+ }
+ onPreferenceChange() {
+ this.setState(this.getStateFromStores());
}
toggleDropdown(e) {
e.preventDefault();
@@ -21,6 +46,38 @@ export default class SidebarHeader extends React.Component {
}
$('.team__header').find('.dropdown-toggle').dropdown('toggle');
}
+ createTutorialTip() {
+ const screens = [];
+
+ screens.push(
+ <div>
+ <h4>{'Main Menu'}</h4>
+ <p>
+ {'The '}<strong>{'Main Menu'}</strong>{' is where you can '}
+ <strong>{'Invite New Members'}</strong>
+ {', access your '}
+ <strong>{'Account Settings'}</strong>
+ {' and set your '}<strong>{'Theme Color'}</strong>{'.'}
+ </p>
+ <p>
+ {'Team administrators can also access their '}<strong>{'Team Settings'}</strong>{' from this menu.'}
+ </p>
+ </div>
+ );
+
+ return (
+ <div
+ onClick={this.toggleDropdown}
+ >
+ <TutorialTip
+ ref='tip'
+ placement='right'
+ screens={screens}
+ overlayClass='tip-overlay--header'
+ />
+ </div>
+ );
+ }
render() {
var me = UserStore.getCurrentUser();
var profilePicture = null;
@@ -38,8 +95,14 @@ export default class SidebarHeader extends React.Component {
);
}
+ let tutorialTip = null;
+ if (this.state.showTutorialTip) {
+ tutorialTip = this.createTutorialTip();
+ }
+
return (
<div className='team__header theme'>
+ {tutorialTip}
<a
href='#'
onClick={this.toggleDropdown}
@@ -47,7 +110,15 @@ export default class SidebarHeader extends React.Component {
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ delayShow={1000}
+ placement='bottom'
+ overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>}
+ ref='descriptionOverlay'
+ >
<div className='team__name'>{this.props.teamDisplayName}</div>
+ </OverlayTrigger>
</div>
</a>
<NavbarDropdown
diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx
index 4e6985a86..e2ef60959 100644
--- a/web/react/components/sidebar_right.jsx
+++ b/web/react/components/sidebar_right.jsx
@@ -3,11 +3,12 @@
var SearchResults = require('./search_results.jsx');
var RhsThread = require('./rhs_thread.jsx');
+var SearchStore = require('../stores/search_store.jsx');
var PostStore = require('../stores/post_store.jsx');
var Utils = require('../utils/utils.jsx');
function getStateFromStores() {
- return {search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch()};
+ return {search_visible: SearchStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: SearchStore.getIsMentionSearch()};
}
export default class SidebarRight extends React.Component {
@@ -19,23 +20,48 @@ export default class SidebarRight extends React.Component {
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
+ this.doStrangeThings = this.doStrangeThings.bind(this);
+
this.state = getStateFromStores();
}
componentDidMount() {
- PostStore.addSearchChangeListener(this.onSearchChange);
+ SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
+ this.doStrangeThings();
}
componentWillUnmount() {
- PostStore.removeSearchChangeListener(this.onSearchChange);
+ SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
}
- componentDidUpdate() {
- if (this.plScrolledToBottom) {
- var postHolder = $('.post-list-holder-by-time').not('.inactive');
- postHolder.scrollTop(postHolder[0].scrollHeight);
- } else {
- $('.top-visible-post')[0].scrollIntoView();
+ componentWillUpdate() {
+ PostStore.jumpPostsViewSidebarOpen();
+ }
+ doStrangeThings() {
+ // We should have a better way to do this stuff
+ // Hence the function name.
+ $('.inner__wrap').removeClass('.move--right');
+ $('.inner__wrap').addClass('move--left');
+ $('.sidebar--left').removeClass('move--right');
+ $('.sidebar--right').addClass('move--left');
+
+ //$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
+
+ if (!(this.state.search_visible || this.state.post_right_visible)) {
+ $('.inner__wrap').removeClass('move--left').removeClass('move--right');
+ $('.sidebar--right').removeClass('move--left');
+ return (
+ <div></div>
+ );
}
+
+ /*setTimeout(() => {
+ $('.sidebar__overlay').fadeOut('200', () => {
+ $('.sidebar__overlay').remove();
+ });
+ }, 500);*/
+ }
+ componentDidUpdate() {
+ this.doStrangeThings();
}
onSelectedChange(fromSearch) {
var newState = getStateFromStores(fromSearch);
@@ -51,30 +77,6 @@ export default class SidebarRight extends React.Component {
}
}
render() {
- var postHolder = $('.post-list-holder-by-time').not('.inactive');
- const position = postHolder.scrollTop() + postHolder.height() + 14;
- const bottom = postHolder[0].scrollHeight;
- this.plScrolledToBottom = position >= bottom;
-
- if (!(this.state.search_visible || this.state.post_right_visible)) {
- $('.inner__wrap').removeClass('move--left').removeClass('move--right');
- $('.sidebar--right').removeClass('move--left');
- return (
- <div></div>
- );
- }
-
- $('.inner__wrap').removeClass('.move--right').addClass('move--left');
- $('.sidebar--left').removeClass('move--right');
- $('.sidebar--right').addClass('move--left');
- $('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
-
- setTimeout(() => {
- $('.sidebar__overlay').fadeOut('200', function fadeOverlay() {
- $(this).remove();
- });
- }, 500);
-
var content = '';
if (this.state.search_visible) {
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index fddc98c9d..9350bbd42 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var client = require('../utils/client.jsx');
var utils = require('../utils/utils.jsx');
@@ -51,7 +52,7 @@ export default class SidebarRightMenu extends React.Component {
data-toggle='modal'
data-target='#get_link'
data-title='Team Invite'
- data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}
+ data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id}
><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a>
</li>
);
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 1858703ef..37760a2a2 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -36,18 +36,88 @@ export default class TeamSignUp extends React.Component {
}
render() {
+ var teamListing = null;
+
+ if (global.window.mm_config.EnableTeamListing === 'true') {
+ if (this.props.teams.length === 0) {
+ if (global.window.mm_config.EnableTeamCreation !== 'true') {
+ teamListing = (<div>{'There are no teams include in the Team Directory and team creation has been disabled.'}</div>);
+ }
+ } else {
+ teamListing = (
+ <div>
+ <h3>{'Choose a Team'}</h3>
+ <div className='signup-team-all'>
+ {
+ this.props.teams.map((team) => {
+ return (
+ <div
+ key={'team_' + team.name}
+ className='signup-team-dir'
+ >
+ <a
+ href={'/' + team.name}
+ >
+ <div className='signup-team-dir__group'>
+ <span className='signup-team-dir__name'>{team.display_name}</span>
+ <span
+ className='glyphicon glyphicon-menu-right right signup-team-dir__arrow'
+ aria-hidden='true'
+ />
+ </div>
+ </a>
+ </div>
+ );
+ })
+ }
+ </div>
+ </div>
+ );
+ }
+ }
+
+ if (global.window.mm_config.EnableTeamCreation !== 'true') {
+ if (teamListing == null) {
+ return (<div>{'Team creation has been disabled. Please contact an administrator for access.'}</div>);
+ }
+
+ return (
+ <div>
+ {teamListing}
+ </div>
+ );
+ }
+
if (this.state.page === 'choose') {
return (
- <ChoosePage
- updatePage={this.updatePage}
- />
+ <div>
+ {teamListing}
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
+ </div>
);
}
if (this.state.page === 'email') {
- return <EmailSignUpPage />;
+ return (
+ <div>
+ {teamListing}
+ <EmailSignUpPage />
+ </div>
+ );
} else if (this.state.page === 'gitlab') {
- return <SSOSignupPage service={Constants.GITLAB_SERVICE} />;
+ return (
+ <div>
+ {teamListing}
+ <SSOSignupPage service={Constants.GITLAB_SERVICE} />
+ </div>
+ );
}
}
}
+
+TeamSignUp.propTypes = {
+ teams: React.PropTypes.array
+};
+
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 923180e27..c7c4fa2ea 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -6,29 +6,111 @@ const SettingItemMax = require('./setting_item_max.jsx');
const Client = require('../utils/client.jsx');
const Utils = require('../utils/utils.jsx');
+const TeamStore = require('../stores/team_store.jsx');
export default class GeneralTab extends React.Component {
constructor(props) {
super(props);
this.handleNameSubmit = this.handleNameSubmit.bind(this);
+ this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this);
+ this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this);
+ this.handleTeamListingSubmit = this.handleTeamListingSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
- this.onUpdateSection = this.onUpdateSection.bind(this);
+ this.onUpdateNameSection = this.onUpdateNameSection.bind(this);
this.updateName = this.updateName.bind(this);
+ this.onUpdateInviteIdSection = this.onUpdateInviteIdSection.bind(this);
+ this.updateInviteId = this.updateInviteId.bind(this);
+ this.onUpdateOpenInviteSection = this.onUpdateOpenInviteSection.bind(this);
+ this.handleOpenInviteRadio = this.handleOpenInviteRadio.bind(this);
+ this.onUpdateTeamListingSection = this.onUpdateTeamListingSection.bind(this);
+ this.handleTeamListingRadio = this.handleTeamListingRadio.bind(this);
+ this.handleGenerateInviteId = this.handleGenerateInviteId.bind(this);
- this.state = {name: this.props.teamDisplayName, serverError: '', clientError: ''};
+ this.state = {
+ name: props.team.display_name,
+ invite_id: props.team.invite_id,
+ allow_open_invite: props.team.allow_open_invite,
+ allow_team_listing: props.team.allow_team_listing,
+ serverError: '',
+ clientError: ''
+ };
}
+
+ handleGenerateInviteId(e) {
+ e.preventDefault();
+
+ var newId = '';
+ for (var i = 0; i < 32; i++) {
+ newId += Math.floor(Math.random() * 16).toString(16);
+ }
+
+ this.setState({invite_id: newId});
+ }
+
+ handleOpenInviteRadio(openInvite) {
+ this.setState({allow_open_invite: openInvite});
+ }
+
+ handleTeamListingRadio(listing) {
+ if (global.window.mm_config.EnableTeamListing !== 'true' && listing) {
+ this.setState({clientError: 'Team directory has been disabled. Please ask a system admin to enable it.'});
+ } else {
+ this.setState({allow_team_listing: listing});
+ }
+ }
+
+ handleOpenInviteSubmit(e) {
+ e.preventDefault();
+
+ var state = {serverError: '', clientError: ''};
+
+ var data = this.props.team;
+ data.allow_open_invite = this.state.allow_open_invite;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
+ this.props.updateSection('');
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+
+ handleTeamListingSubmit(e) {
+ e.preventDefault();
+
+ var state = {serverError: '', clientError: ''};
+
+ var data = this.props.team;
+ data.allow_team_listing = this.state.allow_team_listing;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
+ this.props.updateSection('');
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+
handleNameSubmit(e) {
e.preventDefault();
- let state = {serverError: '', clientError: ''};
+ var state = {serverError: '', clientError: ''};
let valid = true;
const name = this.state.name.trim();
if (!name) {
state.clientError = 'This field is required';
valid = false;
- } else if (name === this.props.teamDisplayName) {
+ } else if (name === this.props.team.display_name) {
state.clientError = 'Please choose a new name for your team';
valid = false;
} else {
@@ -41,37 +123,76 @@ export default class GeneralTab extends React.Component {
return;
}
- let data = {};
- data.new_name = name;
+ var data = this.props.team;
+ data.display_name = this.state.name;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
+ this.props.updateSection('');
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+
+ handleInviteIdSubmit(e) {
+ e.preventDefault();
+
+ var state = {serverError: '', clientError: ''};
+ let valid = true;
- Client.updateTeamDisplayName(data,
- function nameChangeSuccess() {
+ const inviteId = this.state.invite_id.trim();
+ if (inviteId) {
+ state.clientError = '';
+ } else {
+ state.clientError = 'This field is required';
+ valid = false;
+ }
+
+ this.setState(state);
+
+ if (!valid) {
+ return;
+ }
+
+ var data = this.props.team;
+ data.invite_id = this.state.invite_id;
+ Client.updateTeam(data,
+ (team) => {
+ TeamStore.saveTeam(team);
+ TeamStore.emitChange();
this.props.updateSection('');
- $('#team_settings').modal('hide');
- window.location.reload();
- }.bind(this),
- function nameChangeFail(err) {
+ },
+ (err) => {
state.serverError = err.message;
this.setState(state);
- }.bind(this)
+ }
);
}
+
componentWillReceiveProps(newProps) {
if (newProps.team && newProps.teamDisplayName) {
this.setState({name: newProps.teamDisplayName});
}
}
+
handleClose() {
this.setState({clientError: '', serverError: ''});
this.props.updateSection('');
}
+
componentDidMount() {
$('#team_settings').on('hidden.bs.modal', this.handleClose);
}
+
componentWillUnmount() {
$('#team_settings').off('hidden.bs.modal', this.handleClose);
}
- onUpdateSection(e) {
+
+ onUpdateNameSection(e) {
e.preventDefault();
if (this.props.activeSection === 'name') {
this.props.updateSection('');
@@ -79,10 +200,44 @@ export default class GeneralTab extends React.Component {
this.props.updateSection('name');
}
}
+
+ onUpdateInviteIdSection(e) {
+ e.preventDefault();
+ if (this.props.activeSection === 'invite_id') {
+ this.props.updateSection('');
+ } else {
+ this.props.updateSection('invite_id');
+ }
+ }
+
+ onUpdateOpenInviteSection(e) {
+ e.preventDefault();
+ if (this.props.activeSection === 'open_invite') {
+ this.props.updateSection('');
+ } else {
+ this.props.updateSection('open_invite');
+ }
+ }
+
+ onUpdateTeamListingSection(e) {
+ e.preventDefault();
+ if (this.props.activeSection === 'team_listing') {
+ this.props.updateSection('');
+ } else {
+ this.props.updateSection('team_listing');
+ }
+ }
+
updateName(e) {
e.preventDefault();
this.setState({name: e.target.value});
}
+
+ updateInviteId(e) {
+ e.preventDefault();
+ this.setState({invite_id: e.target.value});
+ }
+
render() {
let clientError = null;
let serverError = null;
@@ -93,10 +248,180 @@ export default class GeneralTab extends React.Component {
serverError = this.state.serverError;
}
+ let teamListingSection;
+ if (this.props.activeSection === 'team_listing') {
+ const inputs = [
+ <div key='userTeamListingOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ name='userTeamListingOptions'
+ type='radio'
+ defaultChecked={this.state.allow_team_listing}
+ onChange={this.handleTeamListingRadio.bind(this, true)}
+ />
+ {'Yes'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ ref='teamListingRadioNo'
+ name='userTeamListingOptions'
+ type='radio'
+ defaultChecked={!this.state.allow_team_listing}
+ onChange={this.handleTeamListingRadio.bind(this, false)}
+ />
+ {'No'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div>
+ </div>
+ ];
+
+ teamListingSection = (
+ <SettingItemMax
+ title='Include this team in the Team Directory'
+ inputs={inputs}
+ submit={this.handleTeamListingSubmit}
+ server_error={serverError}
+ client_error={clientError}
+ updateSection={this.onUpdateTeamListingSection}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.allow_team_listing === true) {
+ describe = 'Yes';
+ } else {
+ describe = 'No';
+ }
+
+ teamListingSection = (
+ <SettingItemMin
+ title='Include this team in the Team Directory'
+ describe={describe}
+ updateSection={this.onUpdateTeamListingSection}
+ />
+ );
+ }
+
+ let openInviteSection;
+ if (this.props.activeSection === 'open_invite') {
+ const inputs = [
+ <div key='userOpenInviteOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ name='userOpenInviteOptions'
+ type='radio'
+ defaultChecked={this.state.allow_open_invite}
+ onChange={this.handleOpenInviteRadio.bind(this, true)}
+ />
+ {'Yes'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ name='userOpenInviteOptions'
+ type='radio'
+ defaultChecked={!this.state.allow_open_invite}
+ onChange={this.handleOpenInviteRadio.bind(this, false)}
+ />
+ {'No'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'When allowed, a link to account creation will be included on the sign-in page of this team and allow any visitor to sign-up.'}</div>
+ </div>
+ ];
+
+ openInviteSection = (
+ <SettingItemMax
+ title='Allow anyone to sign-up from login page'
+ inputs={inputs}
+ submit={this.handleOpenInviteSubmit}
+ server_error={serverError}
+ updateSection={this.onUpdateOpenInviteSection}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.allow_open_invite === true) {
+ describe = 'Yes';
+ } else {
+ describe = 'No';
+ }
+
+ openInviteSection = (
+ <SettingItemMin
+ title='Allow anyone to sign-up from login page'
+ describe={describe}
+ updateSection={this.onUpdateOpenInviteSection}
+ />
+ );
+ }
+
+ let inviteSection;
+
+ if (this.props.activeSection === 'invite_id') {
+ const inputs = [];
+
+ inputs.push(
+ <div
+ key='teamInviteSetting'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>{'Invite Code'}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateInviteId}
+ value={this.state.invite_id}
+ maxLength='32'
+ />
+ </div>
+ <div><br/>{'Your Invite Code is used in the URL sent to people to join your team. Regenerating your Invite Code will invalidate the URLs in previous invitations, unless "Allow anyone to sign-up from login page" is enabled.'}</div>
+ <div className='help-text'>
+ <button
+ className='btn btn-default'
+ onClick={this.handleGenerateInviteId}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ );
+
+ inviteSection = (
+ <SettingItemMax
+ title={`Invite Code`}
+ inputs={inputs}
+ submit={this.handleInviteIdSubmit}
+ server_error={serverError}
+ client_error={clientError}
+ updateSection={this.onUpdateInviteIdSection}
+ />
+ );
+ } else {
+ inviteSection = (
+ <SettingItemMin
+ title={`Invite Code`}
+ describe={`Click 'Edit' to regenerate Invite Code.`}
+ updateSection={this.onUpdateInviteIdSection}
+ />
+ );
+ }
+
let nameSection;
if (this.props.activeSection === 'name') {
- let inputs = [];
+ const inputs = [];
let teamNameLabel = 'Team Name';
if (Utils.isMobile()) {
@@ -127,17 +452,17 @@ export default class GeneralTab extends React.Component {
submit={this.handleNameSubmit}
server_error={serverError}
client_error={clientError}
- updateSection={this.onUpdateSection}
+ updateSection={this.onUpdateNameSection}
/>
);
} else {
- let describe = this.state.name;
+ var describe = this.state.name;
nameSection = (
<SettingItemMin
title={`Team Name`}
describe={describe}
- updateSection={this.onUpdateSection}
+ updateSection={this.onUpdateNameSection}
/>
);
}
@@ -158,16 +483,19 @@ export default class GeneralTab extends React.Component {
ref='title'
>
<i className='modal-back'></i>
- General Settings
+ {'General Settings'}
</h4>
</div>
<div
ref='wrapper'
className='user-settings'
>
- <h3 className='tab-header'>General Settings</h3>
+ <h3 className='tab-header'>{'General Settings'}</h3>
<div className='divider-dark first'/>
{nameSection}
+ {openInviteSection}
+ {teamListingSection}
+ {inviteSection}
<div className='divider-dark'/>
</div>
</div>
@@ -178,6 +506,5 @@ export default class GeneralTab extends React.Component {
GeneralTab.propTypes = {
updateSection: React.PropTypes.func.isRequired,
team: React.PropTypes.object.isRequired,
- activeSection: React.PropTypes.string.isRequired,
- teamDisplayName: React.PropTypes.string.isRequired
+ activeSection: React.PropTypes.string.isRequired
};
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index e14da4f04..09674f1ef 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -37,7 +37,6 @@ export default class TeamSettings extends React.Component {
team={this.state.team}
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
- teamDisplayName={this.props.teamDisplayName}
/>
</div>
);
@@ -72,12 +71,11 @@ export default class TeamSettings extends React.Component {
TeamSettings.defaultProps = {
activeTab: '',
- activeSection: '',
- teamDisplayName: ''
+ activeSection: ''
};
+
TeamSettings.propTypes = {
activeTab: React.PropTypes.string.isRequired,
activeSection: React.PropTypes.string.isRequired,
- updateSection: React.PropTypes.func.isRequired,
- teamDisplayName: React.PropTypes.string.isRequired
+ updateSection: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 5c5995020..17fe31c65 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -82,7 +82,6 @@ export default class TeamSettingsModal extends React.Component {
activeTab={this.state.activeTab}
activeSection={this.state.activeSection}
updateSection={this.updateSection}
- teamDisplayName={this.props.teamDisplayName}
/>
</div>
</div>
@@ -95,5 +94,4 @@ export default class TeamSettingsModal extends React.Component {
}
TeamSettingsModal.propTypes = {
- teamDisplayName: React.PropTypes.string.isRequired
};
diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx
index 4d08274e4..2005ecc31 100644
--- a/web/react/components/team_signup_display_name_page.jsx
+++ b/web/react/components/team_signup_display_name_page.jsx
@@ -25,6 +25,9 @@ export default class TeamSignupDisplayNamePage extends React.Component {
if (!displayName) {
this.setState({nameError: 'This field is required'});
return;
+ } else if (displayName.length < 4 || displayName.length > 15) {
+ this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
+ return;
}
this.props.state.wizard = 'team_url';
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 02d5cab8e..8972fda1a 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -35,8 +35,8 @@ export default class TeamSignupUrlPage extends React.Component {
if (cleanedName !== name || !urlRegex.test(name)) {
this.setState({nameError: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."});
return;
- } else if (cleanedName.length <= 2 || cleanedName.length > 15) {
- this.setState({nameError: 'Name must be 3 or more characters up to a maximum of 15'});
+ } else if (cleanedName.length < 4 || cleanedName.length > 15) {
+ this.setState({nameError: 'Name must be 4 or more characters up to a maximum of 15'});
return;
}
diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx
index ff4ccd4d8..021713f04 100644
--- a/web/react/components/team_signup_with_email.jsx
+++ b/web/react/components/team_signup_with_email.jsx
@@ -71,7 +71,7 @@ export default class EmailSignUpPage extends React.Component {
className='btn btn-md btn-primary'
type='submit'
>
- {'Sign up'}
+ {'Create Team'}
</button>
{serverError}
</div>
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index 86bb42f62..707033d8f 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -2,7 +2,7 @@
// See License.txt for license information.
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-const PostStore = require('../stores/post_store.jsx');
+const SearchStore = require('../stores/search_store.jsx');
const CommandList = require('./command_list.jsx');
const ErrorStore = require('../stores/error_store.jsx');
@@ -54,7 +54,7 @@ export default class Textbox extends React.Component {
}
componentDidMount() {
- PostStore.addAddMentionListener(this.onListenerChange);
+ SearchStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
this.resize();
@@ -62,7 +62,7 @@ export default class Textbox extends React.Component {
}
componentWillUnmount() {
- PostStore.removeAddMentionListener(this.onListenerChange);
+ SearchStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
diff --git a/web/react/components/time_since.jsx b/web/react/components/time_since.jsx
new file mode 100644
index 000000000..c37739b9c
--- /dev/null
+++ b/web/react/components/time_since.jsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Utils = require('../utils/utils.jsx');
+
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+
+export default class TimeSince extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ componentDidMount() {
+ this.intervalId = setInterval(() => {
+ this.forceUpdate();
+ }, 30000);
+ }
+ componentWillUnmount() {
+ clearInterval(this.intervalId);
+ }
+ render() {
+ const displayDate = Utils.displayDate(this.props.eventTime);
+ const displayTime = Utils.displayTime(this.props.eventTime);
+
+ const tooltip = (
+ <Tooltip id={'time-since-tooltip-' + this.props.eventTime}>
+ {displayDate + ' at ' + displayTime}
+ </Tooltip>
+ );
+
+ return (
+ <OverlayTrigger
+ delayShow={400}
+ placement='top'
+ overlay={tooltip}
+ >
+ <time className='post-profile-time'>
+ {Utils.displayDateTime(this.props.eventTime)}
+ </time>
+ </OverlayTrigger>
+ );
+ }
+}
+TimeSince.defaultProps = {
+ eventTime: 0
+};
+
+TimeSince.propTypes = {
+ eventTime: React.PropTypes.number.isRequired
+};
diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx
new file mode 100644
index 000000000..c7abccae3
--- /dev/null
+++ b/web/react/components/tutorial/tutorial_intro_screens.jsx
@@ -0,0 +1,152 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const ChannelStore = require('../../stores/channel_store.jsx');
+const TeamStore = require('../../stores/team_store.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+const Utils = require('../../utils/utils.jsx');
+const AsyncClient = require('../../utils/async_client.jsx');
+
+const Constants = require('../../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+
+export default class TutorialIntroScreens extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleNext = this.handleNext.bind(this);
+ this.createScreen = this.createScreen.bind(this);
+
+ this.state = {currentScreen: 0};
+ }
+ handleNext() {
+ if (this.state.currentScreen < 2) {
+ this.setState({currentScreen: this.state.currentScreen + 1});
+ return;
+ }
+
+ Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
+
+ let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
+ const newValue = (parseInt(preference.value, 10) + 1).toString();
+
+ preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
+ AsyncClient.savePreferences([preference]);
+ }
+ createScreen() {
+ switch (this.state.currentScreen) {
+ case 0:
+ return this.createScreenOne();
+ case 1:
+ return this.createScreenTwo();
+ case 2:
+ return this.createScreenThree();
+ }
+ }
+ createScreenOne() {
+ return (
+ <div>
+ <h3>{'Welcome to:'}</h3>
+ <h1>{'Mattermost'}</h1>
+ <p>{'Your team communications all in one place, instantly searchable and available anywhere.'}</p>
+ <p>{'Keep your team connected to help them achieve what matters most.'}</p>
+ <div className='tutorial__circles'>
+ <div className='circle active'/>
+ <div className='circle'/>
+ <div className='circle'/>
+ </div>
+ </div>
+ );
+ }
+ createScreenTwo() {
+ return (
+ <div>
+ <h3>{'How Mattermost works:'}</h3>
+ <p>{'Communication happens in public discussion channels, private groups and direct messages.'}</p>
+ <p>{'Everything is archived and searchable from any web-enabled laptop, tablet or phone.'}</p>
+ <div className='tutorial__circles'>
+ <div className='circle'/>
+ <div className='circle active'/>
+ <div className='circle'/>
+ </div>
+ </div>
+ );
+ }
+ createScreenThree() {
+ const team = TeamStore.getCurrent();
+ let inviteModalLink;
+ if (team.type === Constants.INVITE_TEAM) {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#invite_member'
+ >
+ {'Invite teammates'}
+ </a>
+ );
+ } else {
+ inviteModalLink = (
+ <a
+ className='intro-links'
+ href='#'
+ data-toggle='modal'
+ data-target='#get_link'
+ data-title='Team Invite'
+ data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id}
+ >
+ {'Invite teammates'}
+ </a>
+ );
+ }
+
+ return (
+ <div>
+ <h3>{'You’re all set'}</h3>
+ <p>
+ {inviteModalLink}
+ {' when you’re ready.'}
+ </p>
+ <p>
+ {'Need anything, just email us at '}
+ <a
+ href='mailto:feedback@mattermost.com'
+ target='_blank'
+ >
+ {'feedback@mattermost.com'}
+ </a>
+ {'.'}
+ </p>
+ {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'}
+ <div className='tutorial__circles'>
+ <div className='circle'/>
+ <div className='circle'/>
+ <div className='circle active'/>
+ </div>
+ </div>
+ );
+ }
+ render() {
+ const screen = this.createScreen();
+
+ return (
+ <div className='tutorial-steps__container'>
+ <div className='tutorial__content'>
+ <div className='tutorial__steps'>
+ {screen}
+ <button
+ className='btn btn-primary'
+ tabIndex='1'
+ onClick={this.handleNext}
+ >
+ {'Next'}
+ </button>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx
new file mode 100644
index 000000000..c85acb346
--- /dev/null
+++ b/web/react/components/tutorial/tutorial_tip.jsx
@@ -0,0 +1,131 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+const AsyncClient = require('../../utils/async_client.jsx');
+
+const Constants = require('../../utils/constants.jsx');
+const Preferences = Constants.Preferences;
+
+const Overlay = ReactBootstrap.Overlay;
+
+export default class TutorialTip extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleNext = this.handleNext.bind(this);
+ this.toggle = this.toggle.bind(this);
+
+ this.state = {currentScreen: 0, show: false};
+ }
+ toggle() {
+ const show = !this.state.show;
+ this.setState({show});
+
+ if (!show && this.state.currentScreen >= this.props.screens.length - 1) {
+ let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+
+ const newValue = (parseInt(preference.value, 10) + 1).toString();
+
+ preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
+ AsyncClient.savePreferences([preference]);
+ }
+ }
+ handleNext() {
+ if (this.state.currentScreen < this.props.screens.length - 1) {
+ this.setState({currentScreen: this.state.currentScreen + 1});
+ return;
+ }
+
+ this.toggle();
+ }
+ skipTutorial(e) {
+ e.preventDefault();
+ const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999');
+ AsyncClient.savePreferences([preference]);
+ }
+ render() {
+ const buttonText = this.state.currentScreen === this.props.screens.length - 1 ? 'Okay' : 'Next';
+
+ const dots = [];
+ if (this.props.screens.length > 1) {
+ for (let i = 0; i < this.props.screens.length; i++) {
+ if (i === this.state.currentScreen) {
+ dots.push(
+ <div
+ className='circle active'
+ key={'dotactive' + i}
+ />
+ );
+ } else {
+ dots.push(
+ <div
+ className='circle'
+ key={'dotinactive' + i}
+ />
+ );
+ }
+ }
+ }
+
+ return (
+ <div className={'tip-div ' + this.props.overlayClass}>
+ <img
+ className='tip-button'
+ src='/static/images/tutorialTip.gif'
+ width='35'
+ onClick={this.toggle}
+ ref='target'
+ />
+
+ <Overlay
+ show={this.state.show}
+ >
+ <div className='tip-backdrop'/>
+ </Overlay>
+
+ <Overlay
+ placement={this.props.placement}
+ show={this.state.show}
+ rootClose={true}
+ onHide={this.toggle}
+ target={() => this.refs.target}
+ >
+ <div className={'tip-overlay ' + this.props.overlayClass}>
+ <div className='arrow'></div>
+ {this.props.screens[this.state.currentScreen]}
+ <div className='tutorial__circles'>{dots}</div>
+ <div className='text-right'>
+ <button
+ className='btn btn-default'
+ onClick={this.handleNext}
+ >
+ {buttonText}
+ </button>
+ <div className='tip-opt'>
+ {'Seen this before? '}
+ <a
+ href='#'
+ onClick={this.skipTutorial}
+ >
+ {'Opt out of these tips.'}
+ </a>
+ </div>
+ </div>
+ </div>
+ </Overlay>
+ </div>
+ );
+ }
+}
+
+TutorialTip.defaultProps = {
+ overlayClass: ''
+};
+
+TutorialTip.propTypes = {
+ screens: React.PropTypes.array.isRequired,
+ placement: React.PropTypes.string.isRequired,
+ overlayClass: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx
new file mode 100644
index 000000000..eef4b24ba
--- /dev/null
+++ b/web/react/components/user_settings/code_theme_chooser.jsx
@@ -0,0 +1,55 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Constants = require('../../utils/constants.jsx');
+
+export default class CodeThemeChooser extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+ render() {
+ const theme = this.props.theme;
+
+ const premadeThemes = [];
+ for (const k in Constants.CODE_THEMES) {
+ if (Constants.CODE_THEMES.hasOwnProperty(k)) {
+ let activeClass = '';
+ if (k === theme.codeTheme) {
+ activeClass = 'active';
+ }
+
+ premadeThemes.push(
+ <div
+ className='col-xs-6 col-sm-3 premade-themes'
+ key={'premade-theme-key' + k}
+ >
+ <div
+ className={activeClass}
+ onClick={() => this.props.updateTheme(k)}
+ >
+ <label>
+ <img
+ className='img-responsive'
+ src={'/static/images/themes/code_themes/' + k + '.png'}
+ />
+ <div className='theme-label'>{Constants.CODE_THEMES[k]}</div>
+ </label>
+ </div>
+ </div>
+ );
+ }
+ }
+
+ return (
+ <div className='row'>
+ {premadeThemes}
+ </div>
+ );
+ }
+}
+
+CodeThemeChooser.propTypes = {
+ theme: React.PropTypes.object.isRequired,
+ updateTheme: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 44b3f4544..095e5b622 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -40,11 +40,12 @@ export default class CustomThemeChooser extends React.Component {
const theme = {type: 'custom'};
let index = 0;
Constants.THEME_ELEMENTS.forEach((element) => {
- if (index < colors.length) {
+ if (index < colors.length - 1) {
theme[element.id] = colors[index];
}
index++;
});
+ theme.codeTheme = colors[colors.length - 1];
this.props.updateTheme(theme);
}
@@ -78,6 +79,8 @@ export default class CustomThemeChooser extends React.Component {
colors += theme[element.id] + ',';
});
+ colors += theme.codeTheme;
+
const pasteBox = (
<div className='col-sm-12'>
<label className='custom-label'>
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
index 6e9b2205d..93be988d1 100644
--- a/web/react/components/user_settings/manage_outgoing_hooks.jsx
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -1,11 +1,12 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../../utils/client.jsx');
-var Constants = require('../../utils/constants.jsx');
-var ChannelStore = require('../../stores/channel_store.jsx');
-var LoadingScreen = require('../loading_screen.jsx');
+const LoadingScreen = require('../loading_screen.jsx');
+const ChannelStore = require('../../stores/channel_store.jsx');
+
+const Client = require('../../utils/client.jsx');
+const Constants = require('../../utils/constants.jsx');
export default class ManageOutgoingHooks extends React.Component {
constructor() {
@@ -45,10 +46,10 @@ export default class ManageOutgoingHooks extends React.Component {
hooks = [];
}
hooks.push(data);
- this.setState({hooks, serverError: null, channelId: '', triggerWords: '', callbackURLs: ''});
+ this.setState({hooks, addError: null, channelId: '', triggerWords: '', callbackURLs: ''});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({addError: err.message});
}
);
}
@@ -75,7 +76,7 @@ export default class ManageOutgoingHooks extends React.Component {
this.setState({hooks});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -94,10 +95,10 @@ export default class ManageOutgoingHooks extends React.Component {
}
}
- this.setState({hooks, serverError: null});
+ this.setState({hooks, editError: null});
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -105,11 +106,11 @@ export default class ManageOutgoingHooks extends React.Component {
Client.listOutgoingHooks(
(data) => {
if (data) {
- this.setState({hooks: data, getHooksComplete: true, serverError: null});
+ this.setState({hooks: data, getHooksComplete: true, editError: null});
}
},
(err) => {
- this.setState({serverError: err});
+ this.setState({editError: err.message});
}
);
}
@@ -123,9 +124,13 @@ export default class ManageOutgoingHooks extends React.Component {
this.setState({callbackURLs: e.target.value});
}
render() {
- let serverError;
- if (this.state.serverError) {
- serverError = <label className='has-error'>{this.state.serverError}</label>;
+ let addError;
+ if (this.state.addError) {
+ addError = <label className='has-error'>{this.state.addError}</label>;
+ }
+ let editError;
+ if (this.state.editError) {
+ addError = <label className='has-error'>{this.state.editError}</label>;
}
const channels = ChannelStore.getAll();
@@ -235,7 +240,9 @@ export default class ManageOutgoingHooks extends React.Component {
return (
<div key='addOutgoingHook'>
+ {'Create webhooks to send new message events to an external integration. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'}
<label className='control-label'>{'Add a new outgoing webhook'}</label>
+ <div className='padding-top divider-light'></div>
<div className='padding-top'>
<div>
<label className='control-label'>{'Channel'}</label>
@@ -274,10 +281,11 @@ export default class ManageOutgoingHooks extends React.Component {
resize={false}
rows={3}
onChange={this.updateCallbackURLs}
+ placeholder='Each URL must start with http:// or https://'
/>
</div>
<div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div>
- {serverError}
+ {addError}
</div>
<div className='padding-top padding-bottom'>
<a
@@ -291,6 +299,7 @@ export default class ManageOutgoingHooks extends React.Component {
</div>
</div>
{existingHooks}
+ {editError}
</div>
);
}
diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index 15bf961d6..546e26ca3 100644
--- a/web/react/components/user_settings/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -10,6 +10,7 @@ var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
var IntegrationsTab = require('./user_settings_integrations.jsx');
var DisplayTab = require('./user_settings_display.jsx');
+var AdvancedTab = require('./user_settings_advanced.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -110,6 +111,17 @@ export default class UserSettings extends React.Component {
/>
</div>
);
+ } else if (this.props.activeTab === 'advanced') {
+ return (
+ <div>
+ <AdvancedTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx
new file mode 100644
index 000000000..910444735
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_advanced.jsx
@@ -0,0 +1,169 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const Client = require('../../utils/client.jsx');
+const SettingItemMin = require('../setting_item_min.jsx');
+const SettingItemMax = require('../setting_item_max.jsx');
+const Constants = require('../../utils/constants.jsx');
+const PreferenceStore = require('../../stores/preference_store.jsx');
+
+export default class AdvancedSettingsDisplay extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateSection = this.updateSection.bind(this);
+ this.updateSetting = this.updateSetting.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.setupInitialState = this.setupInitialState.bind(this);
+
+ this.state = this.setupInitialState();
+ }
+
+ setupInitialState() {
+ const sendOnCtrlEnter = PreferenceStore.getPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ 'send_on_ctrl_enter',
+ {value: 'false'}
+ ).value;
+
+ return {
+ settings: {send_on_ctrl_enter: sendOnCtrlEnter}
+ };
+ }
+
+ updateSetting(setting, value) {
+ const settings = this.state.settings;
+ settings[setting] = value;
+ this.setState(settings);
+ }
+
+ handleSubmit(setting) {
+ const preference = PreferenceStore.setPreference(
+ Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ setting,
+ this.state.settings[setting]
+ );
+
+ Client.savePreferences([preference],
+ () => {
+ PreferenceStore.emitChange();
+ this.updateSection('');
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ updateSection(section) {
+ this.props.updateSection(section);
+ }
+
+ handleClose() {
+ this.updateSection('');
+ }
+
+ componentDidMount() {
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+
+ componentWillUnmount() {
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+
+ render() {
+ const serverError = this.state.serverError || null;
+ let ctrlSendSection;
+
+ if (this.props.activeSection === 'advancedCtrlSend') {
+ const ctrlSendActive = [
+ this.state.settings.send_on_ctrl_enter === 'true',
+ this.state.settings.send_on_ctrl_enter === 'false'
+ ];
+
+ const inputs = [
+ <div key='ctrlSendSetting'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[0]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')}
+ />
+ {'On'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={ctrlSendActive[1]}
+ onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')}
+ />
+ {'Off'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'If enabled \'Enter\' inserts a new line and \'Ctrl + Enter\' submits the message.'}</div>
+ </div>
+ ];
+
+ ctrlSendSection = (
+ <SettingItemMax
+ title='Send messages on Ctrl + Enter'
+ inputs={inputs}
+ submit={() => this.handleSubmit('send_on_ctrl_enter')}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ ctrlSendSection = (
+ <SettingItemMin
+ title='Send messages on Ctrl + Enter'
+ describe={this.state.settings.send_on_ctrl_enter === 'true' ? 'On' : 'Off'}
+ updateSection={() => this.props.updateSection('advancedCtrlSend')}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ {'Advanced Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Advanced Settings'}</h3>
+ <div className='divider-dark first'/>
+ {ctrlSendSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+AdvancedSettingsDisplay.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ updateTab: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 8c62a189d..7b4b54e27 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -7,6 +7,7 @@ var Utils = require('../../utils/utils.jsx');
const CustomThemeChooser = require('./custom_theme_chooser.jsx');
const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
+const CodeThemeChooser = require('./code_theme_chooser.jsx');
const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
const Constants = require('../../utils/constants.jsx');
const ActionTypes = Constants.ActionTypes;
@@ -18,12 +19,14 @@ export default class UserSettingsAppearance extends React.Component {
this.onChange = this.onChange.bind(this);
this.submitTheme = this.submitTheme.bind(this);
this.updateTheme = this.updateTheme.bind(this);
+ this.updateCodeTheme = this.updateCodeTheme.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleImportModal = this.handleImportModal.bind(this);
this.state = this.getStateFromStores();
this.originalTheme = this.state.theme;
+ this.originalCodeTheme = this.state.theme.codeTheme;
}
componentDidMount() {
UserStore.addChangeListener(this.onChange);
@@ -58,6 +61,10 @@ export default class UserSettingsAppearance extends React.Component {
type = 'custom';
}
+ if (!theme.codeTheme) {
+ theme.codeTheme = Constants.DEFAULT_CODE_THEME;
+ }
+
return {theme, type};
}
onChange() {
@@ -93,6 +100,15 @@ export default class UserSettingsAppearance extends React.Component {
);
}
updateTheme(theme) {
+ if (!theme.codeTheme) {
+ theme.codeTheme = this.state.theme.codeTheme;
+ }
+ this.setState({theme});
+ Utils.applyTheme(theme);
+ }
+ updateCodeTheme(codeTheme) {
+ var theme = this.state.theme;
+ theme.codeTheme = codeTheme;
this.setState({theme});
Utils.applyTheme(theme);
}
@@ -102,6 +118,7 @@ export default class UserSettingsAppearance extends React.Component {
handleClose() {
const state = this.getStateFromStores();
state.serverError = null;
+ state.theme.codeTheme = this.originalCodeTheme;
Utils.applyTheme(state.theme);
@@ -170,7 +187,13 @@ export default class UserSettingsAppearance extends React.Component {
</div>
{custom}
<hr />
- {serverError}
+ <strong className='radio'>{'Code Theme'}</strong>
+ <CodeThemeChooser
+ theme={this.state.theme}
+ updateTheme={this.updateCodeTheme}
+ />
+ <hr />
+ {serverError}
<a
className='btn btn-sm btn-primary'
href='#'
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index 22a62273c..d086c78a9 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -9,8 +9,12 @@ import PreferenceStore from '../../stores/preference_store.jsx';
function getDisplayStateFromStores() {
const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
+ const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'});
- return {militaryTime: militaryTime.value};
+ return {
+ militaryTime: militaryTime.value,
+ nameFormat: nameFormat.value
+ };
}
export default class UserSettingsDisplay extends React.Component {
@@ -19,15 +23,17 @@ export default class UserSettingsDisplay extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClockRadio = this.handleClockRadio.bind(this);
+ this.handleNameRadio = this.handleNameRadio.bind(this);
this.updateSection = this.updateSection.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = getDisplayStateFromStores();
}
handleSubmit() {
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
+ const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
+ const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat);
- savePreferences([preference],
+ savePreferences([timePreference, namePreference],
() => {
PreferenceStore.emitChange();
this.updateSection('');
@@ -40,6 +46,9 @@ export default class UserSettingsDisplay extends React.Component {
handleClockRadio(militaryTime) {
this.setState({militaryTime});
}
+ handleNameRadio(nameFormat) {
+ this.setState({nameFormat});
+ }
updateSection(section) {
this.setState(getDisplayStateFromStores());
this.props.updateSection(section);
@@ -56,6 +65,7 @@ export default class UserSettingsDisplay extends React.Component {
render() {
const serverError = this.state.serverError || null;
let clockSection;
+ let nameFormatSection;
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
@@ -127,6 +137,88 @@ export default class UserSettingsDisplay extends React.Component {
);
}
+ if (this.props.activeSection === 'name_format') {
+ const nameFormat = [false, false, false];
+ if (this.state.nameFormat === 'nickname_full_name') {
+ nameFormat[0] = true;
+ } else if (this.state.nameFormat === 'full_name') {
+ nameFormat[2] = true;
+ } else {
+ nameFormat[1] = true;
+ }
+
+ const inputs = [
+ <div key='userDisplayNameOptions'>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[0]}
+ onChange={this.handleNameRadio.bind(this, 'nickname_full_name')}
+ />
+ {'Show nickname if one exists, otherwise show first and last name (team default)'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[1]}
+ onChange={this.handleNameRadio.bind(this, 'username')}
+ />
+ {'Show username'}
+ </label>
+ <br/>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ checked={nameFormat[2]}
+ onChange={this.handleNameRadio.bind(this, 'full_name')}
+ />
+ {'Show first and last name'}
+ </label>
+ <br/>
+ </div>
+ <div><br/>{'How should other users be shown in Direct Messages list?'}</div>
+ </div>
+ ];
+
+ nameFormatSection = (
+ <SettingItemMax
+ title='Show real names, nick names or usernames?'
+ inputs={inputs}
+ submit={this.handleSubmit}
+ server_error={serverError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.state.nameFormat === 'username') {
+ describe = 'Show username';
+ } else if (this.state.nameFormat === 'full_name') {
+ describe = 'Show first and last name';
+ } else {
+ describe = 'Show nickname if one exists, otherwise show first and last name (team default)';
+ }
+
+ nameFormatSection = (
+ <SettingItemMin
+ title='Show real names, nick names or usernames?'
+ describe={describe}
+ updateSection={() => {
+ this.props.updateSection('name_format');
+ }}
+ />
+ );
+ }
+
return (
<div>
<div className='modal-header'>
@@ -151,6 +243,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='divider-dark first'/>
{clockSection}
<div className='divider-dark'/>
+ {nameFormatSection}
+ <div className='divider-dark'/>
</div>
</div>
);
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 70e559c30..3adac197a 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}.bind(this),
function imageUploadFailure(err) {
var state = this.setupInitialState(this.props);
- state.serverError = err;
+ state.serverError = err.message;
this.setState(state);
}.bind(this)
);
@@ -570,6 +570,7 @@ export default class UserSettingsGeneralTab extends React.Component {
/>
);
}
+
return (
<div>
<div className='modal-header'>
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 4b1e5e532..4a9915a1f 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMax
title='Incoming Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -54,7 +55,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
incomingHooksSection = (
<SettingItemMin
title='Incoming Webhooks'
- describe='Manage your incoming webhooks (Developer feature)'
+ width='medium'
+ describe='Manage your incoming webhooks'
updateSection={() => {
this.updateSection('incoming-hooks');
}}
@@ -72,6 +74,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMax
title='Outgoing Webhooks'
+ width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
@@ -83,6 +86,7 @@ export default class UserSettingsIntegrationsTab extends React.Component {
outgoingHooksSection = (
<SettingItemMin
title='Outgoing Webhooks'
+ width='medium'
describe='Manage your outgoing webhooks'
updateSection={() => {
this.updateSection('outgoing-hooks');
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 5449ae91e..18dd490e7 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -43,6 +43,7 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
+ tabs.push({name: 'advanced', uiName: 'Advanced', icon: 'glyphicon glyphicon-list-alt'});
return (
<div
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 61d49acb2..2b904763c 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -37,18 +37,18 @@ function getNotificationsStateFromStores() {
if (user.notify_props.mention_keys) {
var keys = user.notify_props.mention_keys.split(',');
- if (keys.indexOf(user.username) !== -1) {
+ if (keys.indexOf(user.username) === -1) {
+ usernameKey = false;
+ } else {
usernameKey = true;
keys.splice(keys.indexOf(user.username), 1);
- } else {
- usernameKey = false;
}
- if (keys.indexOf('@' + user.username) !== -1) {
+ if (keys.indexOf('@' + user.username) === -1) {
+ mentionKey = false;
+ } else {
mentionKey = true;
keys.splice(keys.indexOf('@' + user.username), 1);
- } else {
- mentionKey = false;
}
customKeys = keys.join(',');