summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/about_build_modal.jsx2
-rw-r--r--web/react/components/activity_log_modal.jsx10
-rw-r--r--web/react/components/admin_console/admin_controller.jsx12
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx4
-rw-r--r--web/react/components/admin_console/admin_sidebar_header.jsx3
-rw-r--r--web/react/components/admin_console/email_settings.jsx8
-rw-r--r--web/react/components/admin_console/log_settings.jsx18
-rw-r--r--web/react/components/admin_console/service_settings.jsx36
-rw-r--r--web/react/components/admin_console/team_settings.jsx34
-rw-r--r--web/react/components/admin_console/user_item.jsx2
-rw-r--r--web/react/components/channel_header.jsx16
-rw-r--r--web/react/components/channel_loader.jsx1
-rw-r--r--web/react/components/channel_notifications.jsx24
-rw-r--r--web/react/components/create_comment.jsx39
-rw-r--r--web/react/components/create_post.jsx138
-rw-r--r--web/react/components/edit_post_modal.jsx4
-rw-r--r--web/react/components/email_verify.jsx4
-rw-r--r--web/react/components/file_attachment.jsx160
-rw-r--r--web/react/components/file_preview.jsx2
-rw-r--r--web/react/components/file_upload_overlay.jsx18
-rw-r--r--web/react/components/invite_member_modal.jsx10
-rw-r--r--web/react/components/login.jsx15
-rw-r--r--web/react/components/member_list_item.jsx2
-rw-r--r--web/react/components/member_list_team_item.jsx2
-rw-r--r--web/react/components/mention.jsx3
-rw-r--r--web/react/components/more_channels.jsx6
-rw-r--r--web/react/components/more_direct_channels.jsx93
-rw-r--r--web/react/components/msg_typing.jsx15
-rw-r--r--web/react/components/navbar_dropdown.jsx4
-rw-r--r--web/react/components/password_reset_form.jsx2
-rw-r--r--web/react/components/popover_list_members.jsx24
-rw-r--r--web/react/components/post.jsx14
-rw-r--r--web/react/components/post_body.jsx68
-rw-r--r--web/react/components/post_header.jsx2
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/post_list.jsx157
-rw-r--r--web/react/components/rhs_comment.jsx28
-rw-r--r--web/react/components/rhs_root_post.jsx6
-rw-r--r--web/react/components/rhs_thread.jsx27
-rw-r--r--web/react/components/search_autocomplete.jsx249
-rw-r--r--web/react/components/search_bar.jsx87
-rw-r--r--web/react/components/search_results.jsx27
-rw-r--r--web/react/components/search_results_item.jsx2
-rw-r--r--web/react/components/setting_item_max.jsx2
-rw-r--r--web/react/components/setting_picture.jsx2
-rw-r--r--web/react/components/settings_sidebar.jsx4
-rw-r--r--web/react/components/sidebar.jsx143
-rw-r--r--web/react/components/sidebar_header.jsx5
-rw-r--r--web/react/components/sidebar_right_menu.jsx6
-rw-r--r--web/react/components/signup_team.jsx8
-rw-r--r--web/react/components/signup_user_complete.jsx25
-rw-r--r--web/react/components/team_settings_modal.jsx1
-rw-r--r--web/react/components/team_signup_choose_auth.jsx4
-rw-r--r--web/react/components/team_signup_password_page.jsx19
-rw-r--r--web/react/components/team_signup_send_invites_page.jsx7
-rw-r--r--web/react/components/team_signup_url_page.jsx16
-rw-r--r--web/react/components/team_signup_username_page.jsx7
-rw-r--r--web/react/components/team_signup_welcome_page.jsx18
-rw-r--r--web/react/components/user_profile.jsx46
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx80
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx297
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx10
-rw-r--r--web/react/components/user_settings/user_settings_display.jsx21
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx8
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx87
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx5
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx67
-rw-r--r--web/react/components/view_image.jsx100
-rw-r--r--web/react/components/view_image_popover_bar.jsx2
-rw-r--r--web/react/pages/admin_console.jsx7
-rw-r--r--web/react/pages/channel.jsx10
-rw-r--r--web/react/pages/home.jsx7
-rw-r--r--web/react/stores/browser_store.jsx95
-rw-r--r--web/react/stores/error_store.jsx2
-rw-r--r--web/react/stores/post_store.jsx33
-rw-r--r--web/react/stores/socket_store.jsx195
-rw-r--r--web/react/stores/team_store.jsx64
-rw-r--r--web/react/stores/user_store.jsx168
-rw-r--r--web/react/utils/async_client.jsx23
-rw-r--r--web/react/utils/client.jsx68
-rw-r--r--web/react/utils/constants.jsx29
-rw-r--r--web/react/utils/emoticons.jsx55
-rw-r--r--web/react/utils/markdown.jsx11
-rw-r--r--web/react/utils/text_formatting.jsx13
-rw-r--r--web/react/utils/utils.jsx43
85 files changed, 2316 insertions, 877 deletions
diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx
index e8a46086a..6962876d4 100644
--- a/web/react/components/about_build_modal.jsx
+++ b/web/react/components/about_build_modal.jsx
@@ -14,7 +14,7 @@ export default class AboutBuildModal extends React.Component {
}
render() {
- const config = global.window.config;
+ const config = global.window.mm_config;
return (
<Modal
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 74d6c64e3..2c944913f 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -81,6 +81,7 @@ export default class ActivityLogModal extends React.Component {
const currentSession = this.state.sessions[i];
const lastAccessTime = new Date(currentSession.last_activity_at);
const firstAccessTime = new Date(currentSession.create_at);
+ let devicePlatform = currentSession.props.platform;
let devicePicture = '';
if (currentSession.props.platform === 'Windows') {
@@ -88,7 +89,12 @@ export default class ActivityLogModal extends React.Component {
} else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') {
devicePicture = 'fa fa-apple';
} else if (currentSession.props.platform === 'Linux') {
- devicePicture = 'fa fa-linux';
+ if (currentSession.props.os.indexOf('Android') >= 0) {
+ devicePlatform = 'Android';
+ devicePicture = 'fa fa-android';
+ } else {
+ devicePicture = 'fa fa-linux';
+ }
}
let moreInfo;
@@ -119,7 +125,7 @@ export default class ActivityLogModal extends React.Component {
className='activity-log__table'
>
<div className='activity-log__report'>
- <div className='report__platform'><i className={devicePicture} />{currentSession.props.platform}</div>
+ <div className='report__platform'><i className={devicePicture} />{devicePlatform}</div>
<div className='report__info'>
<div>{`Last activity: ${lastAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}</div>
{moreInfo}
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index f2fb8ac78..f770d166c 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -40,9 +40,13 @@ export default class AdminController extends React.Component {
config: AdminStore.getConfig(),
teams: AdminStore.getAllTeams(),
selectedTeams,
- selected: 'service_settings',
- selectedTeam: null
+ selected: props.tab || 'service_settings',
+ selectedTeam: props.teamId || null
};
+
+ if (!props.tab) {
+ history.replaceState(null, null, `/admin_console/${this.state.selected}`);
+ }
}
componentDidMount() {
@@ -142,7 +146,9 @@ export default class AdminController extends React.Component {
} else if (this.state.selected === 'service_settings') {
tab = <ServiceSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'team_users') {
- tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
+ if (this.state.teams) {
+ tab = <TeamUsersTab 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 4c2a473b6..b0e01ff17 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -24,6 +24,7 @@ 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 || ''}`);
}
isSelected(name, teamId) {
@@ -53,6 +54,9 @@ export default class AdminSidebar extends React.Component {
}
componentDidMount() {
+ if ($(window).width() > 768) {
+ $('.nav-pills__container').perfectScrollbar();
+ }
}
showTeamSelect(e) {
diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx
index c80811bcd..fd6d92c4a 100644
--- a/web/react/components/admin_console/admin_sidebar_header.jsx
+++ b/web/react/components/admin_console/admin_sidebar_header.jsx
@@ -3,6 +3,7 @@
var AdminNavbarDropdown = require('./admin_navbar_dropdown.jsx');
var UserStore = require('../../stores/user_store.jsx');
+var Utils = require('../../utils/utils.jsx');
export default class SidebarHeader extends React.Component {
constructor(props) {
@@ -36,7 +37,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
+ src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
/>
);
}
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index 01759b222..40e00ff04 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -440,9 +440,11 @@ export default class EmailSettings extends React.Component {
className='table table-bordered'
cellPadding='5'
>
- <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
- <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
- <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ <tbody>
+ <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
+ <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
+ <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ </tbody>
</table>
</div>
<div className='help-text'>
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
index 931818bb8..7e9eda89b 100644
--- a/web/react/components/admin_console/log_settings.jsx
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -249,22 +249,24 @@ export default class LogSettings extends React.Component {
onChange={this.handleChange}
disabled={!this.state.fileEnable}
/>
- <p className='help-text'>
+ <div className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
<div className='help-text'>
<table
className='table table-bordered'
cellPadding='5'
>
- <tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
- <tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
- <tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
- <tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
- <tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
- <tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
+ <tbody>
+ <tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
+ <tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
+ <tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
+ <tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
+ <tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
+ <tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
+ </tbody>
</table>
</div>
- </p>
+ </div>
</div>
</div>
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 4105ba6da..53c89a942 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -36,6 +36,7 @@ export default class ServiceSettings extends React.Component {
config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnableOutgoingWebhooks = React.findDOMNode(this.refs.EnableOutgoingWebhooks).checked;
config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked;
config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
@@ -207,7 +208,40 @@ export default class ServiceSettings extends React.Component {
</div>
</div>
- <div className='form-group'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableOutgoingWebhooks'
+ >
+ {'Enable Outgoing Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOutgoingWebhooks'
+ value='true'
+ ref='EnableOutgoingWebhooks'
+ defaultChecked={this.props.config.ServiceSettings.EnableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOutgoingWebhooks'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, outgoing webhooks will be allowed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnablePostUsernameOverride'
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
index da4299714..9ecd14a1e 100644
--- a/web/react/components/admin_console/team_settings.jsx
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -31,6 +31,7 @@ export default class TeamSettings extends React.Component {
config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
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;
var MaxUsersPerTeam = 50;
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
@@ -209,6 +210,39 @@ export default class TeamSettings extends React.Component {
</div>
<div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictTeamNames'
+ >
+ {'Restrict Team Names: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='true'
+ ref='RestrictTeamNames'
+ defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='RestrictTeamNames'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
index 395e22e6c..f7e92672d 100644
--- a/web/react/components/admin_console/user_item.jsx
+++ b/web/react/components/admin_console/user_item.jsx
@@ -215,7 +215,7 @@ export default class UserItem extends React.Component {
<div className='row member-div'>
<img
className='post-profile-img pull-left'
- src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
height='36'
width='36'
/>
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 7582de6c4..1b709336f 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -4,7 +4,6 @@
const ChannelStore = require('../stores/channel_store.jsx');
const UserStore = require('../stores/user_store.jsx');
const PostStore = require('../stores/post_store.jsx');
-const SocketStore = require('../stores/socket_store.jsx');
const NavbarSearchBox = require('./search_bar.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
@@ -25,7 +24,6 @@ export default class ChannelHeader extends React.Component {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
- this.onSocketChange = this.onSocketChange.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.searchMentions = this.searchMentions.bind(this);
@@ -45,7 +43,6 @@ export default class ChannelHeader extends React.Component {
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
PostStore.addSearchChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
- SocketStore.addChangeListener(this.onSocketChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
@@ -60,16 +57,9 @@ export default class ChannelHeader extends React.Component {
}
$('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
}
- onSocketChange(msg) {
- if (msg.action === 'new_user' ||
- msg.action === 'user_added' ||
- (msg.action === 'user_removed' && msg.user_id !== UserStore.getCurrentId())) {
- AsyncClient.getChannelExtraInfo(true);
- }
- }
handleLeave() {
Client.leaveChannel(this.state.channel.id,
- function handleLeaveSuccess() {
+ () => {
AppDispatcher.handleViewAction({
type: ActionTypes.LEAVE_CHANNEL,
id: this.state.channel.id
@@ -77,8 +67,8 @@ export default class ChannelHeader extends React.Component {
const townsquare = ChannelStore.getByName('town-square');
Utils.switchChannel(townsquare);
- }.bind(this),
- function handleLeaveError(err) {
+ },
+ (err) => {
AsyncClient.dispatchError(err, 'handleLeave');
}
);
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 270631db2..55b4a55c0 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -26,7 +26,6 @@ export default class ChannelLoader extends React.Component {
}
componentDidMount() {
/* Initial aysnc loads */
- AsyncClient.getMe();
AsyncClient.getPosts(ChannelStore.getCurrentId());
AsyncClient.getChannels(true, true);
AsyncClient.getChannelExtraInfo(true);
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 6151d4bdd..43700bf36 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -136,16 +136,15 @@ export default class ChannelNotifications extends React.Component {
var inputs = [];
inputs.push(
- <div>
+ <div key='channel-notification-level-radio'>
<div className='radio'>
<label>
<input
type='radio'
checked={notifyActive[0]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
- >
+ />
{`Global default (${globalNotifyLevelName})`}
- </input>
</label>
<br/>
</div>
@@ -155,9 +154,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[1]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
- >
+ />
{'For all activity'}
- </input>
</label>
<br/>
</div>
@@ -167,9 +165,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[2]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
- >
+ />
{'Only for mentions'}
- </input>
</label>
<br/>
</div>
@@ -179,9 +176,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={notifyActive[3]}
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
- >
+ />
{'Never'}
- </input>
</label>
</div>
</div>
@@ -274,16 +270,15 @@ export default class ChannelNotifications extends React.Component {
if (this.state.activeSection === 'markUnreadLevel') {
const inputs = [(
- <div>
+ <div key='channel-notification-unread-radio'>
<div className='radio'>
<label>
<input
type='radio'
checked={this.state.markUnreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
- >
+ />
{'For all unread messages'}
- </input>
</label>
<br />
</div>
@@ -293,9 +288,8 @@ export default class ChannelNotifications extends React.Component {
type='radio'
checked={this.state.markUnreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
- >
+ />
{'Only for mentions'}
- </input>
</label>
<br />
</div>
@@ -370,7 +364,7 @@ export default class ChannelNotifications extends React.Component {
data-dismiss='modal'
>
<span aria-hidden='true'>&times;</span>
- <span className='sr-only'>Close</span>
+ <span className='sr-only'>{'Close'}</span>
</button>
<h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4>
</div>
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 2df3dc40f..435c7d542 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -13,8 +13,10 @@ const MsgTyping = require('./msg_typing.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 ActionTypes = Constants.ActionTypes;
+const KeyCodes = Constants.KeyCodes;
export default class CreateComment extends React.Component {
constructor(props) {
@@ -25,6 +27,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.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
@@ -32,6 +35,7 @@ export default class CreateComment extends React.Component {
this.removePreview = this.removePreview.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
+ this.handleResize = this.handleResize.bind(this);
PostStore.clearCommentDraftUploads();
@@ -40,13 +44,23 @@ export default class CreateComment extends React.Component {
messageText: draft.message,
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
- submitting: false
+ submitting: false,
+ windowWidth: Utils.windowWidth()
};
}
+ componentDidMount() {
+ window.addEventListener('resize', this.handleResize);
+ }
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize);
+ }
+ handleResize() {
+ this.setState({windowWidth: Utils.windowWidth()});
+ }
componentDidUpdate(prevProps, prevState) {
if (prevState.uploadsInProgress < this.state.uploadsInProgress) {
$('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
- if ($(window).width() > 768) {
+ if (this.state.windowWidth > 768) {
$('.post-right__scroll').perfectScrollbar('update');
}
}
@@ -147,6 +161,26 @@ export default class CreateComment extends React.Component {
$('.post-right__scroll').perfectScrollbar('update');
this.setState({messageText: messageText});
}
+ handleArrowUp(e) {
+ if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
+ e.preventDefault();
+
+ const channelId = ChannelStore.getCurrentId();
+ const lastPost = PostStore.getCurrentUsersLatestPost(channelId, this.props.rootId);
+ if (!lastPost) {
+ return;
+ }
+
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.RECIEVED_EDIT_POST,
+ refocusId: '#reply_textbox',
+ title: 'Comment',
+ message: lastPost.message,
+ postId: lastPost.id,
+ channelId: lastPost.channel_id
+ });
+ }
+ }
handleUploadStart(clientIds) {
let draft = PostStore.getCommentDraft(this.props.rootId);
@@ -279,6 +313,7 @@ export default class CreateComment extends React.Component {
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.commentMsgKeyPress}
+ onKeyDown={this.handleArrowUp}
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 2581bdcca..055be112d 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -37,6 +37,8 @@ export default class CreatePost extends React.Component {
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleArrowUp = this.handleArrowUp.bind(this);
+ this.handleResize = this.handleResize.bind(this);
+ this.sendMessage = this.sendMessage.bind(this);
PostStore.clearDraftUploads();
@@ -48,9 +50,17 @@ export default class CreatePost extends React.Component {
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
- initialText: draft.messageText
+ initialText: draft.messageText,
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
};
}
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
+ }
componentDidUpdate(prevProps, prevState) {
if (prevState.previews.length !== this.state.previews.length) {
this.resizePostHolder();
@@ -61,6 +71,11 @@ export default class CreatePost extends React.Component {
this.resizePostHolder();
return;
}
+
+ if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeight) {
+ this.resizePostHolder();
+ return;
+ }
}
getCurrentDraft() {
const draft = PostStore.getCurrentDraft();
@@ -108,6 +123,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});
@@ -116,63 +136,70 @@ 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);
+ 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();
@@ -194,10 +221,9 @@ export default class CreatePost extends React.Component {
PostStore.storeCurrentDraft(draft);
}
resizePostHolder() {
- const height = $(window).height() - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50;
+ const height = this.state.windowHeight - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50;
$('.post-list-holder-by-time').css('height', `${height}px`);
- $(window).trigger('resize');
- if ($(window).width() > 960) {
+ if (this.state.windowWidth > 960) {
$('#post_textbox').focus();
}
}
@@ -274,9 +300,11 @@ export default class CreatePost extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
this.resizePostHolder();
+ window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
+ window.removeEventListener('resize', this.handleResize);
}
onChange() {
const channelId = ChannelStore.getCurrentId();
diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx
index 90d9696e7..b259b3c18 100644
--- a/web/react/components/edit_post_modal.jsx
+++ b/web/react/components/edit_post_modal.jsx
@@ -70,7 +70,7 @@ export default class EditPostModal extends React.Component {
refocusId: options.refocusId || ''
});
- $(React.findDOMNode(this.refs.modal)).modal('show');
+ $(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
}
componentDidMount() {
var self = this;
@@ -92,7 +92,7 @@ export default class EditPostModal extends React.Component {
$('#edit_textbox').get(0).focus();
});
- $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
+ $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
if (self.state.refocusId !== '') {
setTimeout(() => {
$(self.state.refocusId).get(0).focus();
diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx
index 940b01f8d..9c07853b7 100644
--- a/web/react/components/email_verify.jsx
+++ b/web/react/components/email_verify.jsx
@@ -19,10 +19,10 @@ export default class EmailVerify extends React.Component {
var resend = '';
var resendConfirm = '';
if (this.props.isVerified === 'true') {
- title = global.window.config.SiteName + ' Email Verified';
+ title = global.window.mm_config.SiteName + ' Email Verified';
body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>;
} else {
- title = global.window.config.SiteName + ': You are almost done';
+ title = global.window.mm_config.SiteName + ': You are almost done';
body = <p>Please verify your email address. Check your inbox for an email.</p>;
resend = (
<button
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c6dff6550..4d4e8390c 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -10,9 +10,12 @@ export default class FileAttachment extends React.Component {
super(props);
this.loadFiles = this.loadFiles.bind(this);
+ this.playGif = this.playGif.bind(this);
+ this.stopGif = this.stopGif.bind(this);
+ this.addBackgroundImage = this.addBackgroundImage.bind(this);
this.canSetState = false;
- this.state = {fileSize: -1};
+ this.state = {fileSize: -1, mime: '', playing: false, loading: false, format: ''};
}
componentDidMount() {
this.loadFiles();
@@ -28,18 +31,12 @@ export default class FileAttachment extends React.Component {
var filename = this.props.filename;
if (filename) {
- var fileInfo = utils.splitFileLocation(filename);
+ var fileInfo = this.getFileInfoFromName(filename);
var type = utils.getFileType(fileInfo.ext);
- // This is a temporary patch to fix issue with old files using absolute paths
- if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
- fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
- }
- fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
-
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
- $('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
+ $('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
return function loader() {
$(this).remove();
if (name in self.refs) {
@@ -58,11 +55,7 @@ export default class FileAttachment extends React.Component {
$(imgDiv).addClass('normal');
}
- var re1 = new RegExp(' ', 'g');
- var re2 = new RegExp('\\(', 'g');
- var re3 = new RegExp('\\)', 'g');
- var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
- $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
+ self.addBackgroundImage(name, path);
}
};
}(fileInfo.path, filename));
@@ -93,6 +86,75 @@ export default class FileAttachment extends React.Component {
return true;
}
+ playGif(e, filename) {
+ var img = new Image();
+ var fileUrl = utils.getFileUrl(filename);
+
+ this.setState({loading: true});
+ img.load(fileUrl);
+ img.onload = () => {
+ var state = {playing: true, loading: false};
+
+ switch (true) {
+ case img.width > img.height:
+ state.format = 'landscape';
+ break;
+ case img.height > img.width:
+ state.format = 'portrait';
+ break;
+ default:
+ state.format = 'quadrat';
+ break;
+ }
+
+ this.setState(state);
+
+ // keep displaying background image for a short moment while browser is
+ // loading gif, to prevent white background flashing through
+ setTimeout(() => this.removeBackgroundImage.bind(this)(filename), 100);
+ };
+ img.onError = () => this.setState({loading: false});
+
+ e.stopPropagation();
+ }
+ stopGif(e, filename) {
+ this.setState({playing: false});
+ this.addBackgroundImage(filename);
+ e.stopPropagation();
+ }
+ getFileInfoFromName(name) {
+ var fileInfo = utils.splitFileLocation(name);
+
+ // This is a temporary patch to fix issue with old files using absolute paths
+ if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
+ fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
+ }
+ fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
+
+ return fileInfo;
+ }
+ addBackgroundImage(name, path) {
+ var fileUrl = path;
+
+ if (name in this.refs) {
+ if (!path) {
+ fileUrl = this.getFileInfoFromName(name).path;
+ }
+
+ var imgDiv = ReactDOM.findDOMNode(this.refs[name]);
+ var re1 = new RegExp(' ', 'g');
+ var re2 = new RegExp('\\(', 'g');
+ var re3 = new RegExp('\\)', 'g');
+ var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
+
+ $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
+ }
+ }
+ removeBackgroundImage(name) {
+ if (name in this.refs) {
+ $(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
+ }
+ }
render() {
var filename = this.props.filename;
@@ -100,15 +162,71 @@ export default class FileAttachment extends React.Component {
var fileUrl = utils.getFileUrl(filename);
var type = utils.getFileType(fileInfo.ext);
- var thumbnail;
- if (type === 'image') {
- thumbnail = (
+ var playbackControls = '';
+ var loadedFile = '';
+ var loadingIndicator = '';
+ if (this.state.mime === 'image/gif') {
+ playbackControls = (
<div
- ref={filename}
- className='post__load'
- style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ className='file-playback-controls play'
+ onClick={(e) => this.playGif(e, filename)}
+ >
+ {"►"}
+ </div>
+ );
+ }
+ if (this.state.playing) {
+ loadedFile = (
+ <img
+ className={'file__loaded ' + this.state.format}
+ src={fileUrl}
+ />
+ );
+ playbackControls = (
+ <div
+ className='file-playback-controls stop'
+ onClick={(e) => this.stopGif(e, filename)}
+ >
+ {"■"}
+ </div>
+ );
+ }
+ if (this.state.loading) {
+ loadingIndicator = (
+ <img
+ className='spinner file__loading'
+ src='/static/images/load.gif'
/>
);
+ playbackControls = '';
+ }
+
+ var thumbnail;
+ if (type === 'image') {
+ if (this.state.playing) {
+ thumbnail = (
+ <div
+ ref={filename}
+ className='post__load'
+ style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ >
+ {playbackControls}
+ {loadedFile}
+ </div>
+ );
+ } else {
+ thumbnail = (
+ <div
+ ref={filename}
+ className='post__load'
+ style={{backgroundImage: 'url(/static/images/load.gif)'}}
+ >
+ {loadingIndicator}
+ {playbackControls}
+ {loadedFile}
+ </div>
+ );
+ }
} else {
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
}
@@ -119,7 +237,7 @@ export default class FileAttachment extends React.Component {
filename,
function success(data) {
if (this.canSetState) {
- this.setState({fileSize: parseInt(data.size, 10)});
+ this.setState({fileSize: parseInt(data.size, 10), mime: data.mime});
}
}.bind(this),
function error() {}
diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx
index a40ed1dcf..df5deb8bc 100644
--- a/web/react/components/file_preview.jsx
+++ b/web/react/components/file_preview.jsx
@@ -34,7 +34,7 @@ export default class FilePreview extends React.Component {
if (filename.indexOf('/api/v1/files/get') !== -1) {
filename = filename.split('/api/v1/files/get')[1];
}
- filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename;
+ filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + Utils.getSessionIndex();
if (type === 'image') {
previews.push(
diff --git a/web/react/components/file_upload_overlay.jsx b/web/react/components/file_upload_overlay.jsx
index 4fcee6cb0..dbba00022 100644
--- a/web/react/components/file_upload_overlay.jsx
+++ b/web/react/components/file_upload_overlay.jsx
@@ -12,9 +12,21 @@ export default class FileUploadOverlay extends React.Component {
return (
<div className={overlayClass}>
- <div>
- <i className='fa fa-upload'></i>
- <span>Drop a file to upload it.</span>
+ <div className='overlay__indent'>
+ <div className='overlay__circle'>
+ <img
+ className='overlay__files'
+ src='/static/images/filesOverlay.png'
+ alt='Files'
+ />
+ <span><i className='fa fa-upload'></i>{'Drop a file to upload it.'}</span>
+ <img
+ className='overlay__logo'
+ src='/static/images/logoWhite.png'
+ width='100'
+ alt='Logo'
+ />
+ </div>
</div>
</div>
);
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 90290099d..86a4b04cf 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -21,7 +21,7 @@ export default class InviteMemberModal extends React.Component {
emailErrors: {},
firstNameErrors: {},
lastNameErrors: {},
- emailEnabled: global.window.config.SendEmailNotifications === 'true'
+ emailEnabled: global.window.mm_config.SendEmailNotifications === 'true'
};
}
@@ -260,6 +260,12 @@ export default class InviteMemberModal extends React.Component {
var content = null;
var sendButton = null;
+
+ var sendButtonLabel = 'Send Invitation';
+ if (this.state.inviteIds.length > 1) {
+ sendButtonLabel = 'Send Invitations';
+ }
+
if (this.state.emailEnabled) {
content = (
<div>
@@ -281,7 +287,7 @@ export default class InviteMemberModal extends React.Component {
onClick={this.handleSubmit}
type='button'
className='btn btn-primary'
- >Send Invitations</button>
+ >{sendButtonLabel}</button>
);
} else {
var teamInviteLink = null;
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index c982d57ca..108735caf 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -16,7 +16,7 @@ export default class Login extends React.Component {
}
handleSubmit(e) {
e.preventDefault();
- let state = {};
+ var state = {};
const name = this.props.teamName;
if (!name) {
@@ -49,8 +49,7 @@ export default class Login extends React.Component {
this.setState(state);
Client.loginByEmail(name, email, password,
- function loggedIn(data) {
- UserStore.setCurrentUser(data);
+ () => {
UserStore.setLastEmail(email);
const redirect = Utils.getUrlParameter('redirect');
@@ -60,7 +59,7 @@ export default class Login extends React.Component {
window.location.href = '/' + name + '/channels/town-square';
}
},
- function loginFailed(err) {
+ (err) => {
if (err.message === 'Login failed because email address has not been verified') {
window.location.href = '/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email);
return;
@@ -68,7 +67,7 @@ export default class Login extends React.Component {
state.serverError = err.message;
this.valid = false;
this.setState(state);
- }.bind(this)
+ }
);
}
render() {
@@ -95,7 +94,7 @@ export default class Login extends React.Component {
}
let loginMessage = [];
- if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -124,7 +123,7 @@ export default class Login extends React.Component {
}
let emailSignup;
- if (global.window.config.EnableSignUpWithEmail === 'true') {
+ if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className={'form-group' + errorClass}>
@@ -186,7 +185,7 @@ export default class Login extends React.Component {
<div className='signup-team__container'>
<h5 className='margin--less'>Sign in to:</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>on {global.window.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}>
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index 5c3695ad4..8ed94680e 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -105,7 +105,7 @@ export default class MemberListItem extends React.Component {
<div className='row member-div'>
<img
className='post-profile-img pull-left'
- src={'/api/v1/users/' + member.id + '/image?time=' + timestamp}
+ src={'/api/v1/users/' + member.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
height='36'
width='36'
/>
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx
index 3af1d3800..14db05cdb 100644
--- a/web/react/components/member_list_team_item.jsx
+++ b/web/react/components/member_list_team_item.jsx
@@ -169,7 +169,7 @@ export default class MemberListTeamItem extends React.Component {
<div className='row member-div'>
<img
className='post-profile-img pull-left'
- src={`/api/v1/users/${user.id}/image?time=${timestamp}`}
+ src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`}
height='36'
width='36'
/>
diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx
index aeed724a8..050887c6f 100644
--- a/web/react/components/mention.jsx
+++ b/web/react/components/mention.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
export default class Mention extends React.Component {
constructor(props) {
@@ -25,7 +26,7 @@ export default class Mention extends React.Component {
<span>
<img
className='mention-img'
- src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp}
+ src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
/>
</span>
);
diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx
index a20c5cad5..a0084ad30 100644
--- a/web/react/components/more_channels.jsx
+++ b/web/react/components/more_channels.jsx
@@ -83,7 +83,7 @@ export default class MoreChannels extends React.Component {
moreChannels = <LoadingScreen />;
} else if (channels.length) {
moreChannels = (
- <table className='more-channel-table table'>
+ <table className='more-table table'>
<tbody>
{channels.map(function cMap(channel, index) {
var joinButton;
@@ -108,8 +108,8 @@ export default class MoreChannels extends React.Component {
return (
<tr key={channel.id}>
<td>
- <p className='more-channel-name'>{channel.display_name}</p>
- <p className='more-channel-description'>{channel.description}</p>
+ <p className='more-name'>{channel.display_name}</p>
+ <p className='more-description'>{channel.description}</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 08b64de8b..41746d1d7 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -31,7 +31,7 @@ export default class MoreDirectChannels extends React.Component {
getUsersFromStore() {
const currentId = UserStore.getCurrentId();
- const profiles = UserStore.getProfiles();
+ const profiles = UserStore.getActiveOnlyProfiles();
const users = [];
for (const id in profiles) {
@@ -142,7 +142,6 @@ export default class MoreDirectChannels extends React.Component {
details.push(
<span
key={`${user.nickname}__nickname`}
- className='nickname'
>
{separator + user.nickname}
</span>
@@ -170,50 +169,61 @@ export default class MoreDirectChannels extends React.Component {
}
return (
- <li
- key={user.id}
- className='direct-channel'
- >
- <div className='col-xs-1 image-div'>
+ <tr key={'direct-channel-row-user' + user.id}>
+ <td
+ key={user.id}
+ className='direct-channel'
+ >
<img
- className='profile-image'
- src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ className='profile-img pull-left'
+ width='38'
+ height='38'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
/>
- </div>
- <div className='col-xs-9'>
- <div className='username'>
+ <div className='more-name'>
{user.username}
</div>
- <div>
+ <div className='more-description'>
{details}
</div>
- </div>
- <div className='col-xs-2 btn-div'>
+ </td>
+ <td className='td--action lg'>
{joinButton}
- </div>
- </li>
+ </td>
+ </tr>
);
}
+ componentDidUpdate(prevProps) {
+ if (!prevProps.show && this.props.show) {
+ $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300);
+ if ($(window).width() > 768) {
+ $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar();
+ }
+ }
+ }
+
render() {
if (!this.props.show) {
return null;
}
let users = this.state.users;
- if (this.state.filter !== '') {
+ if (this.state.filter) {
+ const filter = this.state.filter.toLowerCase();
+
users = users.filter((user) => {
- return user.username.indexOf(this.state.filter) !== -1 ||
- user.first_name.indexOf(this.state.filter) !== -1 ||
- user.last_name.indexOf(this.state.filter) !== -1 ||
- user.nickname.indexOf(this.state.filter) !== -1;
+ return user.username.toLowerCase().indexOf(filter) !== -1 ||
+ user.first_name.toLowerCase().indexOf(filter) !== -1 ||
+ user.last_name.toLowerCase().indexOf(filter) !== -1 ||
+ user.nickname.toLowerCase().indexOf(filter) !== -1;
});
}
const userEntries = users.map(this.createRowForUser);
if (userEntries.length === 0) {
- userEntries.push(<li key='no-users-found'>{'No users found :('}</li>);
+ userEntries.push(<tr key='no-users-found'><td>{'No users found :('}</td></tr>);
}
let memberString = 'Member';
@@ -232,26 +242,35 @@ export default class MoreDirectChannels extends React.Component {
<Modal
className='modal-direct-channels'
show={this.props.show}
- bsSize='large'
onHide={this.handleHide}
>
<Modal.Header closeButton={true}>
- <Modal.Title>{'More Direct Messages'}</Modal.Title>
+ <Modal.Title>{'Direct Messages'}</Modal.Title>
</Modal.Header>
<Modal.Body>
- <div>
- <input
- ref='filter'
- className='form-control filter-textbox'
- placeholder='Search members'
- onInput={this.handleFilterChange}
- style={{width: '200px', display: 'inline'}}
- />
- <span className='member-count pull-right'>{count}</span>
+ <div className='row filter-row'>
+ <div className='col-sm-6'>
+ <input
+ ref='filter'
+ className='form-control filter-textbox'
+ placeholder='Search members'
+ onInput={this.handleFilterChange}
+ />
+ </div>
+ <div className='col-sm-6'>
+ <span className='member-count'>{count}</span>
+ </div>
+ </div>
+ <div
+ ref='userList'
+ className='user-list'
+ >
+ <table className='more-table table'>
+ <tbody>
+ {userEntries}
+ </tbody>
+ </table>
</div>
- <ul className='user-list'>
- {userEntries}
- </ul>
</Modal.Body>
<Modal.Footer>
<button
diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx
index 569942390..1bd23c55c 100644
--- a/web/react/components/msg_typing.jsx
+++ b/web/react/components/msg_typing.jsx
@@ -1,8 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var SocketStore = require('../stores/socket_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
+const SocketStore = require('../stores/socket_store.jsx');
+const UserStore = require('../stores/user_store.jsx');
+
+const Constants = require('../utils/constants.jsx');
+const SocketEvents = Constants.SocketEvents;
export default class MsgTyping extends React.Component {
constructor(props) {
@@ -33,9 +36,9 @@ export default class MsgTyping extends React.Component {
}
onChange(msg) {
- if (msg.action === 'typing' &&
- this.props.channelId === msg.channel_id &&
- this.props.parentId === msg.props.parent_id) {
+ 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';
@@ -52,7 +55,7 @@ export default class MsgTyping extends React.Component {
}
}.bind(this), 3000);
}
- } else if (msg.action === 'posted' && msg.channel_id === this.props.channelId) {
+ } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
this.setState({text: ''});
}
}
diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx
index 1cb13bbe5..2b68645e5 100644
--- a/web/react/components/navbar_dropdown.jsx
+++ b/web/react/components/navbar_dropdown.jsx
@@ -152,7 +152,7 @@ export default class NavbarDropdown extends React.Component {
sysAdminLink = (
<li>
<a
- href='/admin_console'
+ href={'/admin_console?' + Utils.getSessionIndex()}
>
{'System Console'}
</a>
@@ -178,7 +178,7 @@ export default class NavbarDropdown extends React.Component {
});
}
- if (global.window.config.EnableTeamCreation === 'true') {
+ if (global.window.mm_config.EnableTeamCreation === 'true') {
teams.push(
<li key='newTeam_li'>
<a
diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx
index 217f1b393..b452c40b7 100644
--- a/web/react/components/password_reset_form.jsx
+++ b/web/react/components/password_reset_form.jsx
@@ -61,7 +61,7 @@ export default class PasswordResetForm extends React.Component {
<div className='signup-team__container'>
<h3>Password Reset</h3>
<form onSubmit={this.handlePasswordReset}>
- <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.config.SiteName + ' account.'}</p>
+ <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p>
<div className={formClass}>
<input
type='password'
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index 16ae693fa..155e88600 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -35,13 +35,20 @@ export default class PopoverListMembers extends React.Component {
const teamMembers = UserStore.getProfilesUsernameMap();
if (members && teamMembers) {
- members.sort(function compareByLocal(a, b) {
+ members.sort((a, b) => {
return a.username.localeCompare(b.username);
});
- members.forEach(function addMemberElement(m) {
+ members.forEach((m, i) => {
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
- popoverHtml.push(<div className='text--nowrap'>{m.username}</div>);
+ popoverHtml.push(
+ <div
+ className='text--nowrap'
+ key={'popover-member-' + i}
+ >
+ {m.username}
+ </div>
+ );
count++;
}
});
@@ -57,8 +64,15 @@ export default class PopoverListMembers extends React.Component {
<OverlayTrigger
trigger='click'
placement='bottom'
- rootClose='true'
- overlay={<Popover title='Members'>{popoverHtml}</Popover>}
+ rootClose={true}
+ overlay={
+ <Popover
+ title='Members'
+ id='member-list-popover'
+ >
+ {popoverHtml}
+ </Popover>
+ }
>
<div id='member_popover'>
<div>
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index 64d6776b4..dedac8951 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -120,6 +120,10 @@ export default class Post extends React.Component {
var parentPost = this.props.parentPost;
var posts = this.props.posts;
+ if (!post.props) {
+ post.props = {};
+ }
+
var type = 'Post';
if (post.root_id && post.root_id.length > 0) {
type = 'Comment';
@@ -140,7 +144,7 @@ export default class Post extends React.Component {
}
var currentUserCss = '';
- if (UserStore.getCurrentId() === post.user_id) {
+ if (UserStore.getCurrentId() === post.user_id && !post.props.from_webhook) {
currentUserCss = 'current--user';
}
@@ -158,8 +162,8 @@ export default class Post extends React.Component {
var profilePic = null;
if (!this.props.hideProfilePic) {
- let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
- if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') {
+ let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
+ if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
}
@@ -200,6 +204,7 @@ export default class Post extends React.Component {
posts={posts}
handleCommentClick={this.handleCommentClick}
retryPost={this.retryPost}
+ resize={this.props.resize}
/>
<PostInfo
ref='info'
@@ -223,5 +228,6 @@ Post.propTypes = {
sameUser: React.PropTypes.bool,
sameRoot: React.PropTypes.bool,
hideProfilePic: React.PropTypes.bool,
- isLastComment: React.PropTypes.bool
+ isLastComment: React.PropTypes.bool,
+ resize: React.PropTypes.func
};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index 1db0b12e7..45eae8c6a 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -13,8 +13,12 @@ export default class PostBody extends React.Component {
super(props);
this.receivedYoutubeData = false;
+ this.isGifLoading = false;
this.parseEmojis = this.parseEmojis.bind(this);
+ this.createEmbed = this.createEmbed.bind(this);
+ this.createGifEmbed = this.createGifEmbed.bind(this);
+ this.loadGif = this.loadGif.bind(this);
this.createYoutubeEmbed = this.createYoutubeEmbed.bind(this);
const linkData = Utils.extractLinks(this.props.post.message);
@@ -46,6 +50,7 @@ export default class PostBody extends React.Component {
componentDidUpdate() {
this.parseEmojis();
+ this.props.resize();
}
componentWillReceiveProps(nextProps) {
@@ -53,6 +58,52 @@ export default class PostBody extends React.Component {
this.setState({links: linkData.links, message: linkData.text});
}
+ createEmbed(link) {
+ let embed = this.createYoutubeEmbed(link);
+
+ if (embed != null) {
+ return embed;
+ }
+
+ embed = this.createGifEmbed(link);
+
+ return embed;
+ }
+
+ loadGif(src) {
+ if (this.isGifLoading) {
+ return;
+ }
+
+ this.isGifLoading = true;
+
+ const gif = new Image();
+ gif.src = src;
+ gif.onload = (
+ () => {
+ this.setState({gifLoaded: true});
+ }
+ );
+ }
+
+ createGifEmbed(link) {
+ if (link.substring(link.length - 4) !== '.gif') {
+ return null;
+ }
+
+ if (!this.state.gifLoaded) {
+ this.loadGif(link);
+ return null;
+ }
+
+ return (
+ <img
+ className='gif-div'
+ src={link}
+ />
+ );
+ }
+
handleYoutubeTime(link) {
const timeRegex = /[\\?&]t=([0-9hms]+)/;
@@ -116,15 +167,15 @@ export default class PostBody extends React.Component {
}
var metadata = data.items[0].snippet;
this.receivedYoutubeData = true;
- this.setState({youtubeUploader: metadata.channelTitle, youtubeTitle: metadata.title});
+ this.setState({youtubeTitle: metadata.title});
}
- if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) {
+ if (global.window.mm_config.GoogleDeveloperKey && !this.receivedYoutubeData) {
$.ajax({
async: true,
url: 'https://www.googleapis.com/youtube/v3/videos',
type: 'GET',
- data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey},
+ data: {part: 'snippet', id: youtubeId, key: global.window.mm_config.GoogleDeveloperKey},
success: success.bind(this)
});
}
@@ -134,18 +185,12 @@ export default class PostBody extends React.Component {
header = header + ' - ';
}
- let uploader = this.state.youtubeUploader;
- if (!uploader) {
- uploader = 'unknown';
- }
-
return (
<div className='post-comment'>
<h4>
<span className='video-type'>{header}</span>
<span className='video-title'><a href={link}>{this.state.youtubeTitle}</a></span>
</h4>
- <h4 className='video-uploader'>{uploader}</h4>
<div
className='video-div embed-responsive-item'
id={youtubeId}
@@ -253,7 +298,7 @@ export default class PostBody extends React.Component {
let embed;
if (filenames.length === 0 && this.state.links) {
- embed = this.createYoutubeEmbed(this.state.links[0]);
+ embed = this.createEmbed(this.state.links[0]);
}
let fileAttachmentHolder = '';
@@ -293,5 +338,6 @@ PostBody.propTypes = {
post: React.PropTypes.object.isRequired,
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
- handleCommentClick: React.PropTypes.func.isRequired
+ handleCommentClick: React.PropTypes.func.isRequired,
+ resize: React.PropTypes.func.isRequired
};
diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx
index 0ba5ce6b5..45e60c767 100644
--- a/web/react/components/post_header.jsx
+++ b/web/react/components/post_header.jsx
@@ -16,7 +16,7 @@ export default class PostHeader extends React.Component {
let botIndicator;
if (post.props && post.props.from_webhook) {
- if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') {
+ if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
userProfile = (
<UserProfile
userId={post.user_id}
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index a95095ff6..36260d77c 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -150,7 +150,7 @@ export default class PostInfo extends React.Component {
<ul className='post-header post-info'>
<li className='post-header-col'>
<OverlayTrigger
- delayShow='500'
+ delayShow={500}
container={this}
placement='top'
overlay={tooltip}
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 29728d368..3ceef478c 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -1,20 +1,24 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var PostStore = require('../stores/post_store.jsx');
-var ChannelStore = require('../stores/channel_store.jsx');
-var UserStore = require('../stores/user_store.jsx');
-var PreferenceStore = require('../stores/preference_store.jsx');
-var UserProfile = require('./user_profile.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var Post = require('./post.jsx');
-var LoadingScreen = require('./loading_screen.jsx');
-var SocketStore = require('../stores/socket_store.jsx');
-var utils = require('../utils/utils.jsx');
-var Client = require('../utils/client.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
-var ActionTypes = Constants.ActionTypes;
+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) {
@@ -36,11 +40,14 @@ export default class PostList extends React.Component {
this.loadFirstPosts = this.loadFirstPosts.bind(this);
this.activate = this.activate.bind(this);
this.deactivate = this.deactivate.bind(this);
- this.resize = this.resize.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;
}
@@ -58,7 +65,7 @@ export default class PostList extends React.Component {
}
}
- postList.order.sort(function postSort(a, b) {
+ postList.order.sort((a, b) => {
if (postList.posts[a].create_at > postList.posts[b].create_at) {
return -1;
}
@@ -82,7 +89,7 @@ export default class PostList extends React.Component {
}
return {
- postList: postList
+ postList
};
}
componentDidMount() {
@@ -111,12 +118,7 @@ export default class PostList extends React.Component {
const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
- $(window).resize(() => {
- this.resize();
- if (!this.scrolled) {
- this.scrollToBottom();
- }
- });
+ window.addEventListener('resize', this.handleResize);
postHolder.on('scroll', () => {
const position = postHolder.scrollTop() + postHolder.height() + 14;
@@ -150,7 +152,7 @@ export default class PostList extends React.Component {
this.loadFirstPosts(this.props.channelId);
}
- this.resize();
+ this.resizePostList();
this.onChange();
this.scrollToBottom();
}
@@ -160,7 +162,9 @@ export default class PostList extends React.Component {
SocketStore.removeChangeListener(this.onSocketChange);
PreferenceStore.removeChangeListener(this.onTimeChange);
$('body').off('click.userpopover');
- $(window).off('resize');
+
+ window.removeEventListener('resize', this.handleResize);
+
var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist));
postHolder.off('scroll');
}
@@ -169,6 +173,13 @@ export default class PostList extends React.Component {
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');
@@ -195,10 +206,11 @@ export default class PostList extends React.Component {
this.scrollToBottom();
// there's a new post and
- // it's by the user and not a comment
+ // it's by the user (and not from their webhook) and not a comment
} else if (isNewPost &&
userId === firstPost.user_id &&
- !utils.isComment(firstPost)) {
+ !firstPost.props.from_webhook &&
+ !Utils.isComment(firstPost)) {
this.scrollToBottom(true);
// the user clicked 'load more messages'
@@ -227,10 +239,20 @@ export default class PostList extends React.Component {
this.deactivate();
}
}
- resize() {
+ 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 = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
+ const height = this.state.windowHeight - $('#create_post').height() - $('#error_bar').outerHeight() - 50;
postHolder.css('height', height + 'px');
}
}
@@ -263,46 +285,34 @@ export default class PostList extends React.Component {
Client.getPosts(
id,
PostStore.getLatestUpdate(id),
- function success() {
+ () => {
this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
- }.bind(this),
- function fail() {
+ },
+ () => {
this.loadInProgress = false;
this.setState({isFirstLoadComplete: true});
- }.bind(this)
+ }
);
}
onChange() {
var newState = this.getStateFromStores(this.props.channelId);
- if (!utils.areStatesEqual(newState.postList, this.state.postList)) {
+ if (!Utils.areStatesEqual(newState.postList, this.state.postList)) {
this.setState(newState);
}
}
onSocketChange(msg) {
- var post;
- if (msg.action === 'posted' || msg.action === 'post_edited') {
- post = JSON.parse(msg.props.post);
- PostStore.storePost(post);
- } else if (msg.action === 'post_deleted') {
+ 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;
}
- post = JSON.parse(msg.props.post);
-
- PostStore.storeUnseenDeletedPost(post);
- PostStore.removePost(post, true);
- PostStore.emitChange();
-
if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) {
$('#post_deleted').modal('show');
}
- } else if (msg.action === 'new_user') {
- AsyncClient.getProfiles();
}
}
onTimeChange() {
@@ -318,7 +328,7 @@ export default class PostList extends React.Component {
}
}
createDMIntroMessage(channel) {
- var teammate = utils.getDirectTeammate(channel.id);
+ var teammate = Utils.getDirectTeammate(channel.id);
if (teammate) {
var teammateName = teammate.username;
@@ -331,7 +341,7 @@ export default class PostList extends React.Component {
<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}
+ src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()}
height='50'
width='50'
/>
@@ -352,7 +362,7 @@ export default class PostList extends React.Component {
data-title={channel.display_name}
data-channelid={channel.id}
>
- <i className='fa fa-pencil'></i>Set a description
+ <i className='fa fa-pencil'></i>{'Set a description'}
</a>
</div>
);
@@ -378,13 +388,13 @@ export default class PostList extends React.Component {
createDefaultIntroMessage(channel) {
return (
<div className='channel-intro'>
- <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4>
+ <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4>
<p className='channel-intro__content'>
- Welcome to {channel.display_name}!
+ {'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.
+ {'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…”.
+ {'To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”.'}
<br/>
</p>
</div>
@@ -393,7 +403,7 @@ export default class PostList extends React.Component {
createOffTopicIntroMessage(channel) {
return (
<div className='channel-intro'>
- <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4>
+ <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/>
@@ -407,7 +417,7 @@ export default class PostList extends React.Component {
data-title={channel.display_name}
data-channelid={channel.id}
>
- <i className='fa fa-pencil'></i>Set a description
+ <i className='fa fa-pencil'></i>{'Set a description'}
</a>
<a
className='intro-links'
@@ -415,7 +425,7 @@ export default class PostList extends React.Component {
data-toggle='modal'
data-target='#channel_invite'
>
- <i className='fa fa-user-plus'></i>Invite others to this channel
+ <i className='fa fa-user-plus'></i>{'Invite others to this channel'}
</a>
</div>
);
@@ -430,7 +440,7 @@ export default class PostList extends React.Component {
var members = ChannelStore.getExtraInfo(channel.id).members;
for (var i = 0; i < members.length; i++) {
- if (utils.isAdmin(members[i].roles)) {
+ if (Utils.isAdmin(members[i].roles)) {
return members[i].username;
}
}
@@ -451,14 +461,14 @@ export default class PostList extends React.Component {
var createMessage;
if (creatorName === '') {
- createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.';
+ 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>);
+ 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>
+ <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4>
<p className='channel-intro__content'>
{createMessage}
{memberMessage}
@@ -473,7 +483,7 @@ export default class PostList extends React.Component {
data-title={channel.display_name}
data-channelid={channel.id}
>
- <i className='fa fa-pencil'></i>Set a description
+ <i className='fa fa-pencil'></i>{'Set a description'}
</a>
<a
className='intro-links'
@@ -481,7 +491,7 @@ export default class PostList extends React.Component {
data-toggle='modal'
data-target='#channel_invite'
>
- <i className='fa fa-user-plus'></i>Invite others to this {uiType}
+ <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType}
</a>
</div>
);
@@ -515,7 +525,7 @@ export default class PostList extends React.Component {
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);
+ 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,
@@ -524,8 +534,8 @@ export default class PostList extends React.Component {
// 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) &&
+ !Utils.isComment(prevPost) &&
+ !Utils.isComment(post) &&
(!post.props || !post.props.from_webhook) &&
(!prevPost.props || !prevPost.props.from_webhook)) {
hideProfilePic = true;
@@ -534,7 +544,7 @@ export default class PostList extends React.Component {
// 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 isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
var postCtl = (
<Post
@@ -547,10 +557,11 @@ export default class PostList extends React.Component {
posts={posts}
hideProfilePic={hideProfilePic}
isLastComment={isLastComment}
+ resize={this.updateScroll}
/>
);
- let currentPostDay = utils.getDateForUnixTicks(post.create_at);
+ const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
postCtls.push(
<div
@@ -566,9 +577,9 @@ export default class PostList extends React.Component {
if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) {
renderedLastViewed = true;
- // Temporary fix to solve ie10/11 rendering issue
+ // Temporary fix to solve ie11 rendering issue
let newSeparatorId = '';
- if (!utils.isBrowserIE()) {
+ if (!Utils.isBrowserIE()) {
newSeparatorId = 'new_message_' + this.props.channelId;
}
postCtls.push(
@@ -580,7 +591,7 @@ export default class PostList extends React.Component {
<hr
className='separator__hr'
/>
- <div className='separator__text'>New Messages</div>
+ <div className='separator__text'>{'New Messages'}</div>
</div>
);
}
@@ -646,7 +657,7 @@ export default class PostList extends React.Component {
order = this.state.postList.order;
}
- var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>;
+ var moreMessages = <p className='beginning-messages-text'>{'Beginning of Channel'}</p>;
if (channel != null) {
if (order.length >= this.state.numToDisplay) {
moreMessages = (
@@ -656,7 +667,7 @@ export default class PostList extends React.Component {
href='#'
onClick={this.loadMorePosts}
>
- Load more messages
+ {'Load more messages'}
</a>
);
} else {
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 402e64080..cfff04fa2 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -29,7 +29,7 @@ export default class RhsComment extends React.Component {
var post = this.props.post;
Client.createPost(post, post.channel_id,
- function success(data) {
+ (data) => {
AsyncClient.getPosts(post.channel_id);
var channel = ChannelStore.get(post.channel_id);
@@ -43,11 +43,11 @@ export default class RhsComment extends React.Component {
post: data
});
},
- function fail() {
+ () => {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
this.forceUpdate();
- }.bind(this)
+ }
);
post.state = Constants.POST_LOADING;
@@ -84,7 +84,10 @@ export default class RhsComment extends React.Component {
if (isOwner) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ role='presentation'
+ key='edit-button'
+ >
<a
href='#'
role='menuitem'
@@ -95,7 +98,7 @@ export default class RhsComment extends React.Component {
data-postid={post.id}
data-channelid={post.channel_id}
>
- Edit
+ {'Edit'}
</a>
</li>
);
@@ -103,7 +106,10 @@ export default class RhsComment extends React.Component {
if (isOwner || isAdmin) {
dropdownContents.push(
- <li role='presentation'>
+ <li
+ role='presentation'
+ key='delete-button'
+ >
<a
href='#'
role='menuitem'
@@ -114,7 +120,7 @@ export default class RhsComment extends React.Component {
data-channelid={post.channel_id}
data-comments={0}
>
- Delete
+ {'Delete'}
</a>
</li>
);
@@ -162,7 +168,7 @@ export default class RhsComment extends React.Component {
href='#'
onClick={this.retryComment}
>
- Retry
+ {'Retry'}
</a>
);
} else if (post.state === Constants.POST_LOADING) {
@@ -193,7 +199,7 @@ export default class RhsComment extends React.Component {
<div className='post-profile-img__container'>
<img
className='post-profile-img'
- src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp}
+ src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()}
height='36'
width='36'
/>
@@ -213,14 +219,14 @@ export default class RhsComment extends React.Component {
</li>
</ul>
<div className='post-body'>
- <p className={postClass}>
+ <div className={postClass}>
{loading}
<div
ref='message_holder'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
/>
- </p>
+ </div>
{fileAttachment}
</div>
</div>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index a9f1fcd30..deef389e2 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -121,7 +121,7 @@ export default class RhsRootPost extends React.Component {
let botIndicator;
if (post.props && post.props.from_webhook) {
- if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') {
+ if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
userProfile = (
<UserProfile
userId={post.user_id}
@@ -134,8 +134,8 @@ export default class RhsRootPost extends React.Component {
botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>;
}
- let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
- if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') {
+ let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex();
+ if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
}
diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx
index 467d74681..bcdec2870 100644
--- a/web/react/components/rhs_thread.jsx
+++ b/web/react/components/rhs_thread.jsx
@@ -4,7 +4,7 @@
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var PreferenceStore = require('../stores/preference_store.jsx');
-var utils = require('../utils/utils.jsx');
+var Utils = require('../utils/utils.jsx');
var SearchBox = require('./search_bar.jsx');
var CreateComment = require('./create_comment.jsx');
var RhsHeaderPost = require('./rhs_header_post.jsx');
@@ -20,8 +20,12 @@ export default class RhsThread extends React.Component {
this.onChange = this.onChange.bind(this);
this.onChangeAll = this.onChangeAll.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
+ this.handleResize = this.handleResize.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.windowWidth = Utils.windowWidth();
+ state.windowHeight = Utils.windowHeight();
+ this.state = state;
}
getStateFromStores() {
var postList = PostStore.getSelectedPost();
@@ -47,9 +51,7 @@ export default class RhsThread extends React.Component {
PostStore.addChangeListener(this.onChangeAll);
PreferenceStore.addChangeListener(this.forceUpdateInfo);
this.resize();
- $(window).resize(function resize() {
- this.resize();
- }.bind(this));
+ window.addEventListener('resize', this.handleResize);
}
componentDidUpdate() {
if ($('.post-right__scroll')[0]) {
@@ -61,6 +63,7 @@ export default class RhsThread extends React.Component {
PostStore.removeSelectedPostChangeListener(this.onChange);
PostStore.removeChangeListener(this.onChangeAll);
PreferenceStore.removeChangeListener(this.forceUpdateInfo);
+ window.removeEventListener('resize', this.handleResize);
}
forceUpdateInfo() {
if (this.state.postList) {
@@ -71,9 +74,15 @@ export default class RhsThread extends React.Component {
}
}
}
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
+ }
onChange() {
var newState = this.getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -103,15 +112,15 @@ export default class RhsThread extends React.Component {
}
var newState = this.getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
resize() {
- var height = $(window).height() - $('#error_bar').outerHeight() - 100;
+ var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
$('.post-right__scroll').css('height', height + 'px');
$('.post-right__scroll').scrollTop(100000);
- if ($(window).width() > 768) {
+ if (this.state.windowWidth > 768) {
$('.post-right__scroll').perfectScrollbar();
$('.post-right__scroll').perfectScrollbar('update');
}
diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx
new file mode 100644
index 000000000..03c7b894c
--- /dev/null
+++ b/web/react/components/search_autocomplete.jsx
@@ -0,0 +1,249 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const ChannelStore = require('../stores/channel_store.jsx');
+const KeyCodes = require('../utils/constants.jsx').KeyCodes;
+const UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
+
+const patterns = new Map([
+ ['channels', /\b(?:in|channel):\s*(\S*)$/i],
+ ['users', /\bfrom:\s*(\S*)$/i]
+]);
+
+export default class SearchAutocomplete extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+ this.handleDocumentClick = this.handleDocumentClick.bind(this);
+ this.handleInputChange = this.handleInputChange.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
+
+ this.completeWord = this.completeWord.bind(this);
+ this.updateSuggestions = this.updateSuggestions.bind(this);
+
+ this.state = {
+ show: false,
+ mode: '',
+ filter: '',
+ selection: 0,
+ suggestions: new Map()
+ };
+ }
+
+ componentDidMount() {
+ $(document).on('click', this.handleDocumentClick);
+ }
+
+ componentWillUnmount() {
+ $(document).off('click', this.handleDocumentClick);
+ }
+
+ handleClick(value) {
+ this.completeWord(value);
+ }
+
+ handleDocumentClick(e) {
+ const container = $(ReactDOM.findDOMNode(this.refs.container));
+
+ if (!(container.is(e.target) || container.has(e.target).length > 0)) {
+ this.setState({
+ show: false
+ });
+ }
+ }
+
+ handleInputChange(textbox, text) {
+ const caret = Utils.getCaretPosition(textbox);
+ const preText = text.substring(0, caret);
+
+ let mode = '';
+ let filter = '';
+ for (const [modeForPattern, pattern] of patterns) {
+ const result = pattern.exec(preText);
+
+ if (result) {
+ mode = modeForPattern;
+ filter = result[1];
+ break;
+ }
+ }
+
+ if (mode !== this.state.mode || filter !== this.state.filter) {
+ this.updateSuggestions(mode, filter);
+ }
+
+ this.setState({
+ mode,
+ filter,
+ show: mode || filter
+ });
+ }
+
+ handleKeyDown(e) {
+ if (!this.state.show || this.state.suggestions.length === 0) {
+ return;
+ }
+
+ if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) {
+ e.preventDefault();
+
+ let selection = this.state.selection;
+
+ if (e.which === KeyCodes.UP) {
+ selection -= 1;
+ } else {
+ selection += 1;
+ }
+
+ if (selection >= 0 && selection < this.state.suggestions.length) {
+ this.setState({
+ selection
+ });
+ }
+ } else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
+ e.preventDefault();
+
+ this.completeSelectedWord();
+ }
+ }
+
+ completeSelectedWord() {
+ if (this.state.mode === 'channels') {
+ this.completeWord(this.state.suggestions[this.state.selection].name);
+ } else if (this.state.mode === 'users') {
+ this.completeWord(this.state.suggestions[this.state.selection].username);
+ }
+ }
+
+ completeWord(value) {
+ // add a space so that anything else typed doesn't interfere with the search flag
+ this.props.completeWord(this.state.filter, value + ' ');
+
+ this.setState({
+ show: false,
+ mode: '',
+ filter: '',
+ selection: 0
+ });
+ }
+
+ updateSuggestions(mode, filter) {
+ let suggestions = [];
+
+ if (mode === 'channels') {
+ let channels = ChannelStore.getAll();
+
+ if (filter) {
+ channels = channels.filter((channel) => channel.name.startsWith(filter));
+ }
+
+ channels.sort((a, b) => a.name.localeCompare(b.name));
+
+ suggestions = channels;
+ } else if (mode === 'users') {
+ let users = UserStore.getActiveOnlyProfileList();
+
+ if (filter) {
+ users = users.filter((user) => user.username.startsWith(filter));
+ }
+
+ users.sort((a, b) => a.username.localeCompare(b.username));
+
+ suggestions = users;
+ }
+
+ let selection = this.state.selection;
+
+ // keep the same user/channel selected if it's still visible as a suggestion
+ if (selection > 0 && this.state.suggestions.length > 0) {
+ // we can't just use indexOf to find if the selection is still in the list since they are different javascript objects
+ const currentSelectionId = this.state.suggestions[selection].id;
+ let found = false;
+
+ for (let i = 0; i < suggestions.length; i++) {
+ if (suggestions[i].id === currentSelectionId) {
+ selection = i;
+ found = true;
+
+ break;
+ }
+ }
+
+ if (!found) {
+ selection = 0;
+ }
+ } else {
+ selection = 0;
+ }
+
+ this.setState({
+ suggestions,
+ selection
+ });
+ }
+
+ render() {
+ if (!this.state.show || this.state.suggestions.length === 0) {
+ return null;
+ }
+
+ let suggestions = [];
+
+ if (this.state.mode === 'channels') {
+ suggestions = this.state.suggestions.map((channel, index) => {
+ let className = 'search-autocomplete__channel';
+ if (this.state.selection === index) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ key={channel.name}
+ ref={channel.name}
+ onClick={this.handleClick.bind(this, channel.name)}
+ className={className}
+ >
+ {channel.name}
+ </div>
+ );
+ });
+ } else if (this.state.mode === 'users') {
+ suggestions = this.state.suggestions.map((user, index) => {
+ let className = 'search-autocomplete__user';
+ if (this.state.selection === index) {
+ className += ' selected';
+ }
+
+ return (
+ <div
+ key={user.username}
+ ref={user.username}
+ onClick={this.handleClick.bind(this, user.username)}
+ className={className}
+ >
+ <img
+ className='profile-img'
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
+ />
+ {user.username}
+ </div>
+ );
+ });
+ }
+
+ return (
+ <div
+ ref='container'
+ className='search-autocomplete'
+ >
+ {suggestions}
+ </div>
+ );
+ }
+}
+
+SearchAutocomplete.propTypes = {
+ completeWord: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx
index 2e9764bd9..0da43e8cd 100644
--- a/web/react/components/search_bar.jsx
+++ b/web/react/components/search_bar.jsx
@@ -8,6 +8,8 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
+var Popover = ReactBootstrap.Popover;
+var SearchAutocomplete = require('./search_autocomplete.jsx');
export default class SearchBar extends React.Component {
constructor() {
@@ -15,11 +17,17 @@ export default class SearchBar extends React.Component {
this.mounted = false;
this.onListenerChange = this.onListenerChange.bind(this);
+ this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
+ this.handleUserFocus = this.handleUserFocus.bind(this);
+ this.handleUserBlur = this.handleUserBlur.bind(this);
this.performSearch = this.performSearch.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
+ this.completeWord = this.completeWord.bind(this);
- this.state = this.getSearchTermStateFromStores();
+ const state = this.getSearchTermStateFromStores();
+ state.focused = false;
+ this.state = state;
}
getSearchTermStateFromStores() {
var term = PostStore.getSearchTerm() || '';
@@ -69,25 +77,44 @@ export default class SearchBar extends React.Component {
results: null
});
}
+ handleKeyDown(e) {
+ if (this.refs.autocomplete) {
+ this.refs.autocomplete.handleKeyDown(e);
+ }
+ }
handleUserInput(e) {
var term = e.target.value;
PostStore.storeSearchTerm(term);
PostStore.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();
$('.search-bar__container').addClass('focused');
+
+ this.setState({focused: true});
}
performSearch(terms, isMentionSearch) {
if (terms.length) {
this.setState({isSearching: true});
+
+ // append * if not present
+ let searchTerms = terms;
+ if (searchTerms.search(/\*\s*$/) === -1) {
+ searchTerms = searchTerms + '*';
+ }
+
client.search(
- terms,
- function success(data) {
+ searchTerms,
+ (data) => {
this.setState({isSearching: false});
if (utils.isMobile()) {
ReactDOM.findDOMNode(this.refs.search).value = '';
@@ -98,11 +125,11 @@ export default class SearchBar extends React.Component {
results: data,
is_mention_search: isMentionSearch
});
- }.bind(this),
- function error(err) {
+ },
+ (err) => {
this.setState({isSearching: false});
AsyncClient.dispatchError(err, 'search');
- }.bind(this)
+ }
);
}
}
@@ -110,11 +137,35 @@ export default class SearchBar extends React.Component {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
}
+
+ completeWord(partialWord, word) {
+ const textbox = ReactDOM.findDOMNode(this.refs.search);
+ let text = textbox.value;
+
+ const caret = utils.getCaretPosition(textbox);
+ const preText = text.substring(0, caret - partialWord.length);
+ const postText = text.substring(caret);
+ text = preText + word + postText;
+
+ textbox.value = text;
+ utils.setCaretPosition(textbox, preText.length + word.length);
+
+ PostStore.storeSearchTerm(text);
+ PostStore.emitSearchTermChange(false);
+ this.setState({searchTerm: text});
+ }
+
render() {
var isSearching = null;
if (this.state.isSearching) {
isSearching = <span className={'glyphicon glyphicon-refresh glyphicon-refresh-animate'}></span>;
}
+
+ let helpClass = 'search-help-popover';
+ if (!this.state.searchTerm && this.state.focused) {
+ helpClass += ' visible';
+ }
+
return (
<div>
<div
@@ -127,12 +178,13 @@ export default class SearchBar extends React.Component {
className='search__clear'
onClick={this.clearFocus}
>
- Cancel
+ {'Cancel'}
</span>
<form
role='form'
className='search__form relative-div'
onSubmit={this.handleSubmit}
+ style={{overflow: 'visible'}}
>
<span className='glyphicon glyphicon-search sidebar__search-icon' />
<input
@@ -142,10 +194,31 @@ export default class SearchBar extends React.Component {
placeholder='Search'
value={this.state.searchTerm}
onFocus={this.handleUserFocus}
+ onBlur={this.handleUserBlur}
onChange={this.handleUserInput}
+ onKeyDown={this.handleKeyDown}
onMouseUp={this.handleMouseInput}
/>
{isSearching}
+ <SearchAutocomplete
+ ref='autocomplete'
+ completeWord={this.completeWord}
+ />
+ <Popover
+ id='searchbar-help-popup'
+ placement='bottom'
+ className={helpClass}
+ >
+ <h4>{'Search Options'}</h4>
+ <ul>
+ <li>
+ <span>{'Use '}</span><b>{'"quotation marks"'}</b><span>{' to search for phrases'}</span>
+ </li>
+ <li>
+ <span>{'Use '}</span><b>{'from:'}</b><span>{' to find posts from specific users and '}</span><b>{'in:'}</b><span>{' to find posts in specific channels'}</span>
+ </li>
+ </ul>
+ </Popover>
</form>
</div>
);
diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx
index e55fd3752..30e15d0ad 100644
--- a/web/react/components/search_results.jsx
+++ b/web/react/components/search_results.jsx
@@ -4,7 +4,7 @@
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var SearchBox = require('./search_bar.jsx');
-var utils = require('../utils/utils.jsx');
+var Utils = require('../utils/utils.jsx');
var SearchResultsHeader = require('./search_results_header.jsx');
var SearchResultsItem = require('./search_results_item.jsx');
@@ -20,18 +20,19 @@ export default class SearchResults extends React.Component {
this.onChange = this.onChange.bind(this);
this.resize = this.resize.bind(this);
+ this.handleResize = this.handleResize.bind(this);
- this.state = getStateFromStores();
+ const state = getStateFromStores();
+ state.windowWidth = Utils.windowWidth();
+ state.windowHeight = Utils.windowHeight();
+ this.state = state;
}
componentDidMount() {
this.mounted = true;
PostStore.addSearchChangeListener(this.onChange);
this.resize();
- var self = this;
- $(window).resize(function resize() {
- self.resize();
- });
+ window.addEventListener('resize', this.handleResize);
}
componentDidUpdate() {
@@ -41,22 +42,30 @@ export default class SearchResults extends React.Component {
componentWillUnmount() {
PostStore.removeSearchChangeListener(this.onChange);
this.mounted = false;
+ window.removeEventListener('resize', this.handleResize);
+ }
+
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
}
onChange() {
if (this.mounted) {
var newState = getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
+ if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
}
resize() {
- var height = $(window).height() - $('#error_bar').outerHeight() - 100;
+ var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
$('#search-items-container').css('height', height + 'px');
$('#search-items-container').scrollTop(0);
- if ($(window).width() > 768) {
+ if (this.state.windowWidth > 768) {
$('#search-items-container').perfectScrollbar();
}
}
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 75d2e7a45..d212e47a3 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -77,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
<div className='post-profile-img__container'>
<img
className='post-profile-img'
- src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp}
+ src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()}
height='36'
width='36'
/>
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx
index 4f0fe3ed0..774f98a43 100644
--- a/web/react/components/setting_item_max.jsx
+++ b/web/react/components/setting_item_max.jsx
@@ -36,7 +36,7 @@ export default class SettingItemMax extends React.Component {
if (this.props.width === 'full') {
widthClass = 'col-sm-12';
} else {
- widthClass = 'col-sm-9 col-sm-offset-3';
+ widthClass = 'col-sm-10 col-sm-offset-2';
}
return (
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx
index 2f577fe39..b6bcb13a6 100644
--- a/web/react/components/setting_picture.jsx
+++ b/web/react/components/setting_picture.jsx
@@ -79,7 +79,7 @@ export default class SettingPicture extends React.Component {
>Save</a>
);
}
- var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.config.ProfileWidth + 'px in width and ' + global.window.config.ProfileHeight + 'px height.';
+ var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.mm_config.ProfileWidth + 'px in width and ' + global.window.mm_config.ProfileHeight + 'px height.';
var self = this;
return (
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index 66568e1c8..4af46c35a 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -2,6 +2,10 @@
// See License.txt for license information.
export default class SettingsSidebar extends React.Component {
+ componentDidUpdate() {
+ $('.settings-modal').find('.modal-body').scrollTop(0);
+ $('.settings-modal').find('.modal-body').perfectScrollbar('update');
+ }
constructor(props) {
super(props);
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 89506c028..ed2c84057 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
const AsyncClient = require('../utils/async_client.jsx');
-const BrowserStore = require('../stores/browser_store.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
const Client = require('../utils/client.jsx');
const Constants = require('../utils/constants.jsx');
@@ -11,7 +10,6 @@ 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 SocketStore = require('../stores/socket_store.jsx');
const TeamStore = require('../stores/team_store.jsx');
const UnreadChannelIndicator = require('./unread_channel_indicator.jsx');
const UserStore = require('../stores/user_store.jsx');
@@ -31,9 +29,10 @@ export default class Sidebar extends React.Component {
this.onChange = this.onChange.bind(this);
this.onScroll = this.onScroll.bind(this);
- this.onResize = this.onResize.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
+ this.updateScrollbar = this.updateScrollbar.bind(this);
+ this.handleResize = this.handleResize.bind(this);
this.showNewChannelModal = this.showNewChannelModal.bind(this);
this.hideNewChannelModal = this.hideNewChannelModal.bind(this);
@@ -46,8 +45,9 @@ export default class Sidebar extends React.Component {
const state = this.getStateFromStores();
state.newChannelModalType = '';
- state.showMoreDirectChannelsModal = false;
+ state.showDirectChannelsModal = false;
state.loadingDMChannel = -1;
+ state.windowWidth = Utils.windowWidth();
this.state = state;
}
@@ -129,15 +129,13 @@ export default class Sidebar extends React.Component {
UserStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
TeamStore.addChangeListener(this.onChange);
- SocketStore.addChangeListener(this.onSocketChange);
PreferenceStore.addChangeListener(this.onChange);
- $('.nav-pills__container').perfectScrollbar();
-
this.updateTitle();
this.updateUnreadIndicators();
+ this.updateScrollbar();
- $(window).on('resize', this.onResize);
+ window.addEventListener('resize', this.handleResize);
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areStatesEqual(nextProps, this.props)) {
@@ -152,117 +150,41 @@ export default class Sidebar extends React.Component {
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
+ this.updateScrollbar();
}
componentWillUnmount() {
- $(window).off('resize', this.onResize);
+ window.removeEventListener('resize', this.handleResize);
ChannelStore.removeChangeListener(this.onChange);
UserStore.removeChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange);
- SocketStore.removeChangeListener(this.onSocketChange);
PreferenceStore.removeChangeListener(this.onChange);
}
+ handleResize() {
+ this.setState({
+ windowWidth: Utils.windowWidth(),
+ windowHeight: Utils.windowHeight()
+ });
+ }
+ updateScrollbar() {
+ if (this.state.windowWidth > 768) {
+ $('.nav-pills__container').perfectScrollbar();
+ $('.nav-pills__container').perfectScrollbar('update');
+ }
+ }
onChange() {
var newState = this.getStateFromStores();
if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
- onSocketChange(msg) {
- if (msg.action === 'posted') {
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- if (window.isActive) {
- AsyncClient.updateLastViewedAt();
- }
- } else {
- AsyncClient.getChannels();
- }
-
- if (UserStore.getCurrentId() !== msg.user_id) {
- var mentions = [];
- if (msg.props.mentions) {
- mentions = JSON.parse(msg.props.mentions);
- }
- var channel = ChannelStore.get(msg.channel_id);
-
- const user = UserStore.getCurrentUser();
- const member = ChannelStore.getMember(msg.channel_id);
-
- var notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default';
- if (notifyLevel === 'default') {
- notifyLevel = user.notify_props.desktop;
- }
-
- if (notifyLevel === 'none') {
- return;
- } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') {
- return;
- }
-
- var username = 'Someone';
- if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
-
- var title = 'Posted';
- if (channel) {
- title = channel.display_name;
- }
-
- var repRegex = new RegExp('<br>', 'g');
- var post = JSON.parse(msg.props.post);
- var msgProps = msg.props;
- var notifyText = post.message.replace(repRegex, '\n').replace(/\n+/g, ' ').replace('<mention>', '').replace('</mention>', '');
-
- if (notifyText.length > 50) {
- notifyText = notifyText.substring(0, 49) + '...';
- }
-
- if (notifyText.length === 0) {
- if (msgProps.image) {
- Utils.notifyMe(title, username + ' uploaded an image', channel);
- } else if (msgProps.otherFile) {
- Utils.notifyMe(title, username + ' uploaded a file', channel);
- } else {
- Utils.notifyMe(title, username + ' did something new', channel);
- }
- } else {
- Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel);
- }
- if (!user.notify_props || user.notify_props.desktop_sound === 'true') {
- Utils.ding();
- }
- }
- } else if (msg.action === 'viewed') {
- if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) {
- AsyncClient.getChannel(msg.channel_id);
- }
- } else if (msg.action === 'user_added') {
- if (UserStore.getCurrentId() === msg.user_id) {
- AsyncClient.getChannel(msg.channel_id);
- }
- } else if (msg.action === 'user_removed') {
- if (msg.user_id === UserStore.getCurrentId()) {
- AsyncClient.getChannels(true);
-
- if (msg.props.remover !== msg.user_id && msg.props.channel_id === ChannelStore.getCurrentId() && $('#removed_from_channel').length > 0) {
- var sentState = {};
- sentState.channelName = ChannelStore.getCurrent().display_name;
- sentState.remover = UserStore.getProfile(msg.props.remover).username;
-
- BrowserStore.setItem('channel-removed-state', sentState);
- $('#removed_from_channel').modal('show');
- }
- }
- }
- }
updateTitle() {
const channel = ChannelStore.getCurrent();
if (channel) {
let currentSiteName = '';
- if (global.window.config.SiteName != null) {
- currentSiteName = global.window.config.SiteName;
+ if (global.window.mm_config.SiteName != null) {
+ currentSiteName = global.window.mm_config.SiteName;
}
let currentChannelName = channel.display_name;
@@ -276,9 +198,6 @@ export default class Sidebar extends React.Component {
onScroll() {
this.updateUnreadIndicators();
}
- onResize() {
- this.updateUnreadIndicators();
- }
updateUnreadIndicators() {
const container = $(ReactDOM.findDOMNode(this.refs.container));
@@ -471,11 +390,13 @@ export default class Sidebar extends React.Component {
}
let closeButton = null;
- const removeTooltip = <Tooltip>{'Remove from list'}</Tooltip>;
+ const removeTooltip = (
+ <Tooltip id='remove-dm-tooltip'>{'Remove from list'}</Tooltip>
+ );
if (handleClose && !badge) {
closeButton = (
<OverlayTrigger
- delayShow='1000'
+ delayShow={1000}
placement='top'
overlay={removeTooltip}
>
@@ -564,8 +485,12 @@ export default class Sidebar extends React.Component {
showChannelModal = true;
}
- const createChannelTootlip = <Tooltip>{'Create new channel'}</Tooltip>;
- const createGroupTootlip = <Tooltip>{'Create new group'}</Tooltip>;
+ const createChannelTootlip = (
+ <Tooltip id='new-channel-tooltip' >{'Create new channel'}</Tooltip>
+ );
+ const createGroupTootlip = (
+ <Tooltip id='new-group-tooltip'>{'Create new group'}</Tooltip>
+ );
return (
<div>
@@ -607,7 +532,7 @@ export default class Sidebar extends React.Component {
<h4>
{'Channels'}
<OverlayTrigger
- delayShow='500'
+ delayShow={500}
placement='top'
overlay={createChannelTootlip}
>
@@ -640,7 +565,7 @@ export default class Sidebar extends React.Component {
<h4>
{'Private Groups'}
<OverlayTrigger
- delayShow='500'
+ delayShow={500}
placement='top'
overlay={createGroupTootlip}
>
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index c3709bc0a..de28a8374 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -3,6 +3,7 @@
var NavbarDropdown = require('./navbar_dropdown.jsx');
var UserStore = require('../stores/user_store.jsx');
+const Utils = require('../utils/utils.jsx');
export default class SidebarHeader extends React.Component {
constructor(props) {
@@ -32,7 +33,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
+ src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
/>
);
}
@@ -61,7 +62,7 @@ export default class SidebarHeader extends React.Component {
}
SidebarHeader.defaultProps = {
- teamDisplayName: global.window.config.SiteName,
+ teamDisplayName: global.window.mm_config.SiteName,
teamType: ''
};
SidebarHeader.propTypes = {
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index ac101d631..fddc98c9d 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -84,7 +84,7 @@ export default class SidebarRightMenu extends React.Component {
consoleLink = (
<li>
<a
- href='/admin_console'
+ href={'/admin_console?' + utils.getSessionIndex()}
>
<i className='glyphicon glyphicon-wrench'></i>System Console</a>
</li>
@@ -92,8 +92,8 @@ export default class SidebarRightMenu extends React.Component {
}
var siteName = '';
- if (global.window.config.SiteName != null) {
- siteName = global.window.config.SiteName;
+ if (global.window.mm_config.SiteName != null) {
+ siteName = global.window.mm_config.SiteName;
}
var teamDisplayName = siteName;
if (this.props.teamDisplayName) {
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index 48cf2c73c..1858703ef 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -14,19 +14,19 @@ export default class TeamSignUp extends React.Component {
var count = 0;
- if (global.window.config.EnableSignUpWithEmail === 'true') {
+ if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
count = count + 1;
}
- if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
count = count + 1;
}
if (count > 1) {
this.state = {page: 'choose'};
- } else if (global.window.config.EnableSignUpWithEmail === 'true') {
+ } else if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
this.state = {page: 'email'};
- } else if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ } else if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
this.state = {page: 'gitlab'};
}
}
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index f74c29d27..d70ea5065 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -82,30 +82,29 @@ export default class SignupUserComplete extends React.Component {
});
client.createUser(user, this.props.data, this.props.hash,
- function createUserSuccess() {
+ () => {
client.track('signup', 'signup_user_02_complete');
client.loginByEmail(this.props.teamName, user.email, user.password,
- function emailLoginSuccess(data) {
+ () => {
UserStore.setLastEmail(user.email);
- UserStore.setCurrentUser(data);
if (this.props.hash > 0) {
BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'}));
}
window.location.href = '/' + this.props.teamName + '/channels/town-square';
- }.bind(this),
- function emailLoginFailure(err) {
+ },
+ (err) => {
if (err.message === 'Login failed because email address has not been verified') {
window.location.href = '/verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.props.teamName);
} else {
this.setState({serverError: err.message});
}
- }.bind(this)
+ }
);
- }.bind(this),
- function createUserFailure(err) {
+ },
+ (err) => {
this.setState({serverError: err.message});
- }.bind(this)
+ }
);
}
render() {
@@ -149,7 +148,7 @@ export default class SignupUserComplete extends React.Component {
// set up the email entry and hide it if an email was provided
var yourEmailIs = '';
if (this.state.user.email) {
- yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.config.SiteName}.</span>;
+ yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.mm_config.SiteName}.</span>;
}
var emailContainerStyle = 'margin--extra';
@@ -177,7 +176,7 @@ export default class SignupUserComplete extends React.Component {
);
var signupMessage = [];
- if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
signupMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -190,7 +189,7 @@ export default class SignupUserComplete extends React.Component {
}
var emailSignup;
- if (global.window.config.EnableSignUpWithEmail === 'true') {
+ if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className='inner__content'>
@@ -259,7 +258,7 @@ export default class SignupUserComplete extends React.Component {
/>
<h5 className='margin--less'>Welcome to:</h5>
<h2 className='signup-team__name'>{this.props.teamDisplayName}</h2>
- <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2>
+ <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2>
<h4 className='color--light'>Let's create your account</h4>
{signupMessage}
{emailSignup}
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index b55373dba..5c5995020 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -19,6 +19,7 @@ export default class TeamSettingsModal extends React.Component {
componentDidMount() {
$('body').on('click', '.modal-back', function handleBackClick() {
$(this).closest('.modal-dialog').removeClass('display--content');
+ $(this).closest('.modal-dialog').find('.settings-table .nav li.active').removeClass('active');
});
$('body').on('click', '.modal-header .close', () => {
setTimeout(() => {
diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx
index fa898f63c..0254c8b4e 100644
--- a/web/react/components/team_signup_choose_auth.jsx
+++ b/web/react/components/team_signup_choose_auth.jsx
@@ -8,7 +8,7 @@ export default class ChooseAuthPage extends React.Component {
}
render() {
var buttons = [];
- if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
buttons.push(
<a
className='btn btn-custom-login gitlab btn-full'
@@ -26,7 +26,7 @@ export default class ChooseAuthPage extends React.Component {
);
}
- if (global.window.config.EnableSignUpWithEmail === 'true') {
+ if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
buttons.push(
<a
className='btn btn-custom-login email btn-full'
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index daa898b53..67fd686bc 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -36,15 +36,14 @@ export default class TeamSignupPasswordPage extends React.Component {
delete teamSignup.wizard;
Client.createTeamFromSignup(teamSignup,
- function success() {
+ () => {
Client.track('signup', 'signup_team_08_complete');
var props = this.props;
Client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password,
- function loginSuccess(data) {
+ () => {
UserStore.setLastEmail(teamSignup.team.email);
- UserStore.setCurrentUser(data);
if (this.props.hash > 0) {
BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'}));
}
@@ -54,21 +53,21 @@ export default class TeamSignupPasswordPage extends React.Component {
props.updateParent(props.state, true);
window.location.href = '/' + teamSignup.team.name + '/channels/town-square';
- }.bind(this),
- function loginFail(err) {
+ },
+ (err) => {
if (err.message === 'Login failed because email address has not been verified') {
window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name);
} else {
this.setState({serverError: err.message});
$('#finish-button').button('reset');
}
- }.bind(this)
+ }
);
- }.bind(this),
- function error(err) {
+ },
+ (err) => {
this.setState({serverError: err.message});
$('#finish-button').button('reset');
- }.bind(this)
+ }
);
}
render() {
@@ -129,7 +128,7 @@ export default class TeamSignupPasswordPage extends React.Component {
Finish
</button>
</div>
- <p>By proceeding to create your account and use {global.window.config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.config.SiteName}.</p>
+ <p>By proceeding to create your account and use {global.window.mm_config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.mm_config.SiteName}.</p>
<div className='margin--extra'>
<a
href='#'
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx
index e7bc0272d..7b4db8fae 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/team_signup_send_invites_page.jsx
@@ -13,13 +13,8 @@ export default class TeamSignupSendInvitesPage extends React.Component {
this.submitSkip = this.submitSkip.bind(this);
this.keySubmit = this.keySubmit.bind(this);
this.state = {
- emailEnabled: global.window.config.SendEmailNotifications === 'true'
+ emailEnabled: global.window.mm_config.SendEmailNotifications === 'true'
};
-
- if (!this.state.emailEnabled) {
- this.props.state.wizard = 'username';
- this.props.updateParent(this.props.state);
- }
}
submitBack(e) {
e.preventDefault();
diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx
index 67e4c9dd7..02d5cab8e 100644
--- a/web/react/components/team_signup_url_page.jsx
+++ b/web/react/components/team_signup_url_page.jsx
@@ -40,10 +40,12 @@ export default class TeamSignupUrlPage extends React.Component {
return;
}
- for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
- if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
- this.setState({nameError: 'URL is taken or contains a reserved word'});
- return;
+ if (global.window.mm_config.RestrictTeamNames === 'true') {
+ for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
+ if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
+ this.setState({nameError: 'URL is taken or contains a reserved word'});
+ return;
+ }
}
}
@@ -52,7 +54,11 @@ export default class TeamSignupUrlPage extends React.Component {
if (data) {
this.setState({nameError: 'This URL is unavailable. Please try another.'});
} else {
- this.props.state.wizard = 'send_invites';
+ if (global.window.mm_config.SendEmailNotifications === 'true') {
+ this.props.state.wizard = 'send_invites';
+ } else {
+ this.props.state.wizard = 'username';
+ }
this.props.state.team.type = 'O';
this.props.state.team.name = name;
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index fa8a031a0..d8d0dbf2c 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -15,7 +15,12 @@ export default class TeamSignupUsernamePage extends React.Component {
}
submitBack(e) {
e.preventDefault();
- this.props.state.wizard = 'send_invites';
+ if (global.window.mm_config.SendEmailNotifications === 'true') {
+ this.props.state.wizard = 'send_invites';
+ } else {
+ this.props.state.wizard = 'team_url';
+ }
+
this.props.updateParent(this.props.state);
}
submitNext(e) {
diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx
index 1e9d8df0a..9448413ce 100644
--- a/web/react/components/team_signup_welcome_page.jsx
+++ b/web/react/components/team_signup_welcome_page.jsx
@@ -104,21 +104,19 @@ export default class TeamSignupWelcomePage extends React.Component {
return (
<div>
- <p>
- <img
- className='signup-team-logo'
- src='/static/images/logo.png'
- />
- <h3 className='sub-heading'>Welcome to:</h3>
- <h1 className='margin--top-none'>{global.window.config.SiteName}</h1>
- </p>
+ <img
+ className='signup-team-logo'
+ src='/static/images/logo.png'
+ />
+ <h3 className='sub-heading'>Welcome to:</h3>
+ <h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1>
<p className='margin--less'>Let's set up your new team</p>
- <p>
+ <div>
Please confirm your email address:<br />
<div className='inner__content'>
<div className='block--gray'>{this.props.state.team.email}</div>
</div>
- </p>
+ </div>
<p className='margin--extra color--light'>
Your account will administer the new team site. <br />
You can add other administrators later.
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 715161b4f..c4402ae23 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -65,22 +65,33 @@ export default class UserProfile extends React.Component {
var dataContent = [];
dataContent.push(
- <img className='user-popover__image'
- src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at}
+ <img
+ className='user-popover__image'
+ src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex()}
height='128'
width='128'
+ key='user-popover-image'
/>
);
- if (!global.window.config.ShowEmailAddress === 'true') {
- dataContent.push(<div className='text-nowrap'>{'Email not shared'}</div>);
+
+ if (!global.window.mm_config.ShowEmailAddress === 'true') {
+ dataContent.push(
+ <div
+ className='text-nowrap'
+ key='user-popover-no-email'
+ >
+ {'Email not shared'}
+ </div>
+ );
} else {
dataContent.push(
<div
data-toggle='tooltip'
- title="' + this.state.profile.email + '"
+ title={this.state.profile.email}
+ key='user-popover-email'
>
<a
- href="mailto:' + this.state.profile.email + '"
+ href={'mailto:' + this.state.profile.email}
className='text-nowrap text-lowercase user-popover__email'
>
{this.state.profile.email}
@@ -93,15 +104,22 @@ export default class UserProfile extends React.Component {
<OverlayTrigger
trigger='click'
placement='right'
- rootClose='true'
- overlay={<Popover title={this.state.profile.username}>{dataContent}</Popover>}
- >
- <div
- className='user-popover'
- id={'profile_' + this.uniqueId}
+ rootClose={true}
+ overlay={
+ <Popover
+ title={this.state.profile.username}
+ id='user-profile-popover'
+ >
+ {dataContent}
+ </Popover>
+ }
>
- {name}
- </div>
+ <div
+ className='user-popover'
+ id={'profile_' + this.uniqueId}
+ >
+ {name}
+ </div>
</OverlayTrigger>
);
}
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index f5a2774a0..6b8c09718 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -96,7 +96,14 @@ export default class ManageIncomingHooks extends React.Component {
const options = [];
channels.forEach((channel) => {
if (channel.type !== Constants.DM_CHANNEL) {
- options.push(<option value={channel.id}>{channel.name}</option>);
+ options.push(
+ <option
+ key={'incoming-hook' + channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
}
});
@@ -108,26 +115,30 @@ export default class ManageIncomingHooks extends React.Component {
const hooks = [];
this.state.hooks.forEach((hook) => {
const c = ChannelStore.get(hook.channel_id);
- hooks.push(
- <div className='font--small'>
- <div className='padding-top x2 divider-light'></div>
- <div className='padding-top x2'>
- <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
- </div>
- <div className='padding-top'>
- <strong>{'Channel: '}</strong>{c.name}
- </div>
- <div className='padding-top'>
+ if (c) {
+ hooks.push(
+ <div
+ key={hook.id}
+ className='webhook__item'
+ >
+ <div className='padding-top x2 webhook__url'>
+ <strong>{'URL: '}</strong>
+ <span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
+ </div>
+ <div className='padding-top'>
+ <strong>{'Channel: '}</strong>{c.display_name}
+ </div>
<a
- className={'text-danger'}
+ className={'webhook__remove'}
href='#'
onClick={this.removeHook.bind(this, hook.id)}
>
- {'Remove'}
+ <span aria-hidden='true'>{'×'}</span>
</a>
+ <div className='padding-top x2 divider-light'></div>
</div>
- </div>
- );
+ );
+ }
});
let displayHooks;
@@ -136,35 +147,38 @@ export default class ManageIncomingHooks extends React.Component {
} else if (hooks.length > 0) {
displayHooks = hooks;
} else {
- displayHooks = <label>{': None'}</label>;
+ displayHooks = <div className='padding-top x2'>{'None'}</div>;
}
const existingHooks = (
- <div className='padding-top x2'>
+ <div className='webhooks__container'>
<label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label>
- {displayHooks}
+ <div className='padding-top divider-light'></div>
+ <div className='webhooks__list'>
+ {displayHooks}
+ </div>
</div>
);
return (
<div key='addIncomingHook'>
{'Create webhook URLs for use in external integrations. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'}
- <br/>
- <br/>
- <label className='control-label'>{'Add a new incoming webhook'}</label>
- <div className='padding-top'>
- <select
- ref='channelName'
- className='form-control'
- value={this.state.channelId}
- onChange={this.updateChannelId}
- >
- {options}
- </select>
- {serverError}
- <div className='padding-top'>
+ <label className='control-label padding-top x2'>{'Add a new incoming webhook'}</label>
+ <div className='row padding-top'>
+ <div className='col-sm-10 padding-bottom'>
+ <select
+ ref='channelName'
+ className='form-control'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ >
+ {options}
+ </select>
+ {serverError}
+ </div>
+ <div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'>
<a
- className={'btn btn-sm btn-primary' + disableButton}
+ className={'btn form-control no-padding btn-sm btn-primary' + disableButton}
href='#'
onClick={this.addNewHook}
>
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
new file mode 100644
index 000000000..6e9b2205d
--- /dev/null
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -0,0 +1,297 @@
+// 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');
+
+
+export default class ManageOutgoingHooks extends React.Component {
+ constructor() {
+ super();
+
+ this.getHooks = this.getHooks.bind(this);
+ this.addNewHook = this.addNewHook.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+ this.updateTriggerWords = this.updateTriggerWords.bind(this);
+ this.updateCallbackURLs = this.updateCallbackURLs.bind(this);
+
+ this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false};
+ }
+ componentDidMount() {
+ this.getHooks();
+ }
+ addNewHook(e) {
+ e.preventDefault();
+
+ if ((this.state.channelId === '' && this.state.triggerWords === '') ||
+ this.state.callbackURLs === '') {
+ return;
+ }
+
+ const hook = {};
+ hook.channel_id = this.state.channelId;
+ if (this.state.triggerWords.length !== 0) {
+ hook.trigger_words = this.state.triggerWords.trim().split(',');
+ }
+ hook.callback_urls = this.state.callbackURLs.split('\n');
+
+ Client.addOutgoingHook(
+ hook,
+ (data) => {
+ let hooks = Object.assign([], this.state.hooks);
+ if (!hooks) {
+ hooks = [];
+ }
+ hooks.push(data);
+ this.setState({hooks, serverError: null, channelId: '', triggerWords: '', callbackURLs: ''});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ removeHook(id) {
+ const data = {};
+ data.id = id;
+
+ Client.deleteOutgoingHook(
+ data,
+ () => {
+ const hooks = this.state.hooks;
+ let index = -1;
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ hooks.splice(index, 1);
+ }
+
+ this.setState({hooks});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ regenToken(id) {
+ const regenData = {};
+ regenData.id = id;
+
+ Client.regenOutgoingHookToken(
+ regenData,
+ (data) => {
+ const hooks = Object.assign([], this.state.hooks);
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ hooks[i] = data;
+ break;
+ }
+ }
+
+ this.setState({hooks, serverError: null});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ getHooks() {
+ Client.listOutgoingHooks(
+ (data) => {
+ if (data) {
+ this.setState({hooks: data, getHooksComplete: true, serverError: null});
+ }
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ updateChannelId(e) {
+ this.setState({channelId: e.target.value});
+ }
+ updateTriggerWords(e) {
+ this.setState({triggerWords: e.target.value});
+ }
+ updateCallbackURLs(e) {
+ this.setState({callbackURLs: e.target.value});
+ }
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <label className='has-error'>{this.state.serverError}</label>;
+ }
+
+ const channels = ChannelStore.getAll();
+ const options = [];
+ options.push(
+ <option
+ key='select-channel'
+ value=''
+ >
+ {'--- Select a channel ---'}
+ </option>
+ );
+
+ channels.forEach((channel) => {
+ if (channel.type === Constants.OPEN_CHANNEL) {
+ options.push(
+ <option
+ key={'outgoing-hook' + channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
+ }
+ });
+
+ const hooks = [];
+ this.state.hooks.forEach((hook) => {
+ const c = ChannelStore.get(hook.channel_id);
+
+ if (!c && hook.channel_id && hook.channel_id.length !== 0) {
+ return;
+ }
+
+ let channelDiv;
+ if (c) {
+ channelDiv = (
+ <div className='padding-top'>
+ <strong>{'Channel: '}</strong>{c.display_name}
+ </div>
+ );
+ }
+
+ let triggerDiv;
+ if (hook.trigger_words && hook.trigger_words.length !== 0) {
+ triggerDiv = (
+ <div className='padding-top'>
+ <strong>{'Trigger Words: '}</strong>{hook.trigger_words.join(', ')}
+ </div>
+ );
+ }
+
+ hooks.push(
+ <div
+ key={hook.id}
+ className='webhook__item'
+ >
+ <div className='padding-top x2'>
+ <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
+ </div>
+ {channelDiv}
+ {triggerDiv}
+ <div className='padding-top'>
+ <strong>{'Token: '}</strong>{hook.token}
+ </div>
+ <div className='padding-top'>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.regenToken.bind(this, hook.id)}
+ >
+ {'Regen Token'}
+ </a>
+ <a
+ className='webhook__remove'
+ href='#'
+ onClick={this.removeHook.bind(this, hook.id)}
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </a>
+ </div>
+ <div className='padding-top x2 divider-light'></div>
+ </div>
+ );
+ });
+
+ let displayHooks;
+ if (!this.state.getHooksComplete) {
+ displayHooks = <LoadingScreen/>;
+ } else if (hooks.length > 0) {
+ displayHooks = hooks;
+ } else {
+ displayHooks = <div className='padding-top x2'>{'None'}</div>;
+ }
+
+ const existingHooks = (
+ <div className='webhooks__container'>
+ <label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label>
+ <div className='padding-top divider-light'></div>
+ <div className='webhooks__list'>
+ {displayHooks}
+ </div>
+ </div>
+ );
+
+ const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === '';
+
+ return (
+ <div key='addOutgoingHook'>
+ <label className='control-label'>{'Add a new outgoing webhook'}</label>
+ <div className='padding-top'>
+ <div>
+ <label className='control-label'>{'Channel'}</label>
+ <div className='padding-top'>
+ <select
+ ref='channelName'
+ className='form-control'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ >
+ {options}
+ </select>
+ </div>
+ <div className='padding-top'>{'Only public channels can be used'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Trigger Words:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='triggerWords'
+ className='form-control'
+ value={this.state.triggerWords}
+ onChange={this.updateTriggerWords}
+ placeholder='Optional if channel selected'
+ />
+ </div>
+ <div className='padding-top'>{'Comma separated words to trigger on'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Callback URLs:'}</label>
+ <div className='padding-top'>
+ <textarea
+ ref='callbackURLs'
+ className='form-control no-resize'
+ value={this.state.callbackURLs}
+ resize={false}
+ rows={3}
+ onChange={this.updateCallbackURLs}
+ />
+ </div>
+ <div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div>
+ {serverError}
+ </div>
+ <div className='padding-top padding-bottom'>
+ <a
+ className={'btn btn-sm btn-primary'}
+ href='#'
+ disabled={disableButton}
+ onClick={this.addNewHook}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingHooks}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
index 7f363e92e..8c62a189d 100644
--- a/web/react/components/user_settings/user_settings_appearance.jsx
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -152,9 +152,8 @@ export default class UserSettingsAppearance extends React.Component {
<input type='radio'
checked={!displayCustom}
onChange={this.updateType.bind(this, 'premade')}
- >
- {'Theme Colors'}
- </input>
+ />
+ {'Theme Colors'}
</label>
<br/>
</div>
@@ -164,9 +163,8 @@ export default class UserSettingsAppearance extends React.Component {
<input type='radio'
checked={displayCustom}
onChange={this.updateType.bind(this, 'custom')}
- >
- {'Custom Theme'}
- </input>
+ />
+ {'Custom Theme'}
</label>
<br/>
</div>
diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx
index ec209c218..22a62273c 100644
--- a/web/react/components/user_settings/user_settings_display.jsx
+++ b/web/react/components/user_settings/user_settings_display.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import { savePreferences } from '../../utils/client.jsx';
+import {savePreferences} from '../../utils/client.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from '../../utils/constants.jsx';
@@ -38,7 +38,7 @@ export default class UserSettingsDisplay extends React.Component {
);
}
handleClockRadio(militaryTime) {
- this.setState({militaryTime: militaryTime});
+ this.setState({militaryTime});
}
updateSection(section) {
this.setState(getDisplayStateFromStores());
@@ -57,7 +57,7 @@ export default class UserSettingsDisplay extends React.Component {
const serverError = this.state.serverError || null;
let clockSection;
if (this.props.activeSection === 'clock') {
- let clockFormat = [false, false];
+ const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
clockFormat[1] = true;
} else {
@@ -77,9 +77,8 @@ export default class UserSettingsDisplay extends React.Component {
type='radio'
checked={clockFormat[0]}
onChange={this.handleClockRadio.bind(this, 'false')}
- >
- 12-hour clock (example: 4:00 PM)
- </input>
+ />
+ {'12-hour clock (example: 4:00 PM)'}
</label>
<br/>
</div>
@@ -89,9 +88,8 @@ export default class UserSettingsDisplay extends React.Component {
type='radio'
checked={clockFormat[1]}
onChange={this.handleClockRadio.bind(this, 'true')}
- >
- 24-hour clock (example: 16:00)
- </input>
+ />
+ {'24-hour clock (example: 16:00)'}
</label>
<br/>
</div>
@@ -99,7 +97,6 @@ export default class UserSettingsDisplay extends React.Component {
</div>
];
-
clockSection = (
<SettingItemMax
title='Clock Display'
@@ -138,13 +135,13 @@ export default class UserSettingsDisplay extends React.Component {
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>
{'Display Settings'}
</h4>
diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 9c03f77a6..70e559c30 100644
--- a/web/react/components/user_settings/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -122,7 +122,7 @@ export default class UserSettingsGeneralTab extends React.Component {
() => {
this.updateSection('');
AsyncClient.getMe();
- const verificationEnabled = global.window.config.SendEmailNotifications === 'true' && global.window.config.RequireEmailVerification === 'true' && emailUpdated;
+ const verificationEnabled = global.window.mm_config.SendEmailNotifications === 'true' && global.window.mm_config.RequireEmailVerification === 'true' && emailUpdated;
if (verificationEnabled) {
ErrorStore.storeLastError({message: 'Check your email at ' + user.email + ' to verify the address.'});
@@ -451,8 +451,8 @@ export default class UserSettingsGeneralTab extends React.Component {
}
var emailSection;
if (this.props.activeSection === 'email') {
- const emailEnabled = global.window.config.SendEmailNotifications === 'true';
- const emailVerificationEnabled = global.window.config.RequireEmailVerification === 'true';
+ const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true';
+ const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true';
let helpText = 'Email is used for notifications, and requires verification if changed.';
if (!emailEnabled) {
@@ -542,7 +542,7 @@ export default class UserSettingsGeneralTab extends React.Component {
<SettingPicture
title='Profile Picture'
submit={this.submitPicture}
- src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update}
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + utils.getSessionIndex()}
server_error={serverError}
client_error={clientError}
updateSection={function clearSection(e) {
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 3be062ad3..4b1e5e532 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -4,6 +4,7 @@
var SettingItemMin = require('../setting_item_min.jsx');
var SettingItemMax = require('../setting_item_max.jsx');
var ManageIncomingHooks = require('./manage_incoming_hooks.jsx');
+var ManageOutgoingHooks = require('./manage_outgoing_hooks.jsx');
export default class UserSettingsIntegrationsTab extends React.Component {
constructor(props) {
@@ -19,6 +20,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
}
handleClose() {
this.updateSection('');
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
}
componentDidMount() {
$('#user_settings').on('hidden.bs.modal', this.handleClose);
@@ -28,35 +31,65 @@ export default class UserSettingsIntegrationsTab extends React.Component {
}
render() {
let incomingHooksSection;
+ let outgoingHooksSection;
var inputs = [];
- if (this.props.activeSection === 'incoming-hooks') {
- inputs.push(
- <ManageIncomingHooks />
- );
+ if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (this.props.activeSection === 'incoming-hooks') {
+ inputs.push(
+ <ManageIncomingHooks key='incoming-hook-ui' />
+ );
- incomingHooksSection = (
- <SettingItemMax
- title='Incoming Webhooks'
- width = 'full'
- inputs={inputs}
- updateSection={function clearSection(e) {
- this.updateSection('');
- e.preventDefault();
- }.bind(this)}
- />
- );
- } else {
- incomingHooksSection = (
- <SettingItemMin
- title='Incoming Webhooks'
- width = 'full'
- describe='Manage your incoming webhooks (Developer feature)'
- updateSection={function updateNameSection() {
- this.updateSection('incoming-hooks');
- }.bind(this)}
- />
- );
+ incomingHooksSection = (
+ <SettingItemMax
+ title='Incoming Webhooks'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ incomingHooksSection = (
+ <SettingItemMin
+ title='Incoming Webhooks'
+ describe='Manage your incoming webhooks (Developer feature)'
+ updateSection={() => {
+ this.updateSection('incoming-hooks');
+ }}
+ />
+ );
+ }
+ }
+
+ if (global.window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (this.props.activeSection === 'outgoing-hooks') {
+ inputs.push(
+ <ManageOutgoingHooks key='outgoing-hook-ui' />
+ );
+
+ outgoingHooksSection = (
+ <SettingItemMax
+ title='Outgoing Webhooks'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ outgoingHooksSection = (
+ <SettingItemMin
+ title='Outgoing Webhooks'
+ describe='Manage your outgoing webhooks'
+ updateSection={() => {
+ this.updateSection('outgoing-hooks');
+ }}
+ />
+ );
+ }
}
return (
@@ -82,6 +115,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
<h3 className='tab-header'>{'Integration Settings'}</h3>
<div className='divider-dark first'/>
{incomingHooksSection}
+ <div className='divider-light'/>
+ {outgoingHooksSection}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 692fb26ee..5449ae91e 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -35,10 +35,11 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'});
tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'});
tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'});
- if (global.window.config.EnableOAuthServiceProvider === 'true') {
+ if (global.window.mm_config.EnableOAuthServiceProvider === 'true') {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
}
- if (global.window.config.EnableIncomingWebhooks === 'true') {
+
+ if (global.window.mm_config.EnableIncomingWebhooks === 'true' || global.window.mm_config.EnableOutgoingWebhooks === 'true') {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 4dbb9b96f..61d49acb2 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -228,9 +228,8 @@ export default class NotificationsTab extends React.Component {
<input type='radio'
checked={notifyActive[0]}
onChange={this.handleNotifyRadio.bind(this, 'all')}
- >
- For all activity
- </input>
+ />
+ {'For all activity'}
</label>
<br/>
</div>
@@ -240,9 +239,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
- >
- Only for mentions and direct messages
- </input>
+ />
+ {'Only for mentions and direct messages'}
</label>
<br/>
</div>
@@ -252,9 +250,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={notifyActive[2]}
onChange={this.handleNotifyRadio.bind(this, 'none')}
- >
- Never
- </input>
+ />
+ {'Never'}
</label>
</div>
</div>
@@ -320,9 +317,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={soundActive[0]}
onChange={this.handleSoundRadio.bind(this, 'true')}
- >
- On
- </input>
+ />
+ {'On'}
</label>
<br/>
</div>
@@ -332,9 +328,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={soundActive[1]}
onChange={this.handleSoundRadio.bind(this, 'false')}
- >
- Off
- </input>
+ />
+ {'Off'}
</label>
<br/>
</div>
@@ -402,9 +397,8 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={emailActive[0]}
onChange={this.handleEmailRadio.bind(this, 'true')}
- >
- On
- </input>
+ />
+ {'On'}
</label>
<br/>
</div>
@@ -414,13 +408,12 @@ export default class NotificationsTab extends React.Component {
type='radio'
checked={emailActive[1]}
onChange={this.handleEmailRadio.bind(this, 'false')}
- >
- Off
- </input>
+ />
+ {'Off'}
</label>
<br/>
</div>
- <div><br/>{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.config.SiteName + ' for more than 5 minutes.'}</div>
+ <div><br/>{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.mm_config.SiteName + ' for more than 5 minutes.'}</div>
</div>
);
@@ -482,9 +475,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.firstNameKey}
onChange={handleUpdateFirstNameKey}
- >
- {'Your case sensitive first name "' + user.first_name + '"'}
- </input>
+ />
+ {'Your case sensitive first name "' + user.first_name + '"'}
</label>
</div>
</div>
@@ -502,9 +494,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.usernameKey}
onChange={handleUpdateUsernameKey}
- >
- {'Your non-case sensitive username "' + user.username + '"'}
- </input>
+ />
+ {'Your non-case sensitive username "' + user.username + '"'}
</label>
</div>
</div>
@@ -521,9 +512,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.mentionKey}
onChange={handleUpdateMentionKey}
- >
- {'Your username mentioned "@' + user.username + '"'}
- </input>
+ />
+ {'Your username mentioned "@' + user.username + '"'}
</label>
</div>
</div>
@@ -540,9 +530,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.allKey}
onChange={handleUpdateAllKey}
- >
- {'Team-wide mentions "@all"'}
- </input>
+ />
+ {'Team-wide mentions "@all"'}
</label>
</div>
</div>
@@ -559,9 +548,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.channelKey}
onChange={handleUpdateChannelKey}
- >
- {'Channel-wide mentions "@channel"'}
- </input>
+ />
+ {'Channel-wide mentions "@channel"'}
</label>
</div>
</div>
@@ -576,9 +564,8 @@ export default class NotificationsTab extends React.Component {
type='checkbox'
checked={this.state.customKeysChecked}
onChange={this.updateCustomMentionKeys}
- >
- {'Other non-case sensitive words, separated by commas:'}
- </input>
+ />
+ {'Other non-case sensitive words, separated by commas:'}
</label>
</div>
<input
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index f75693470..92d7cd835 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -6,6 +6,7 @@ const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
const ViewImagePopoverBar = require('./view_image_popover_bar.jsx');
const Modal = ReactBootstrap.Modal;
+const KeyCodes = Constants.KeyCodes;
export default class ViewImageModal extends React.Component {
constructor(props) {
@@ -37,7 +38,10 @@ export default class ViewImageModal extends React.Component {
progress: progress,
images: {},
fileSizes: {},
- showFooter: false
+ fileMimes: {},
+ showFooter: false,
+ isPlaying: {},
+ isLoading: {}
};
}
handleNext(e) {
@@ -63,11 +67,11 @@ export default class ViewImageModal extends React.Component {
this.loadImage(id);
}
handleKeyPress(e) {
- if (!e) {
+ if (!e || !this.props.show) {
return;
- } else if (e.keyCode === 39) {
+ } else if (e.keyCode === KeyCodes.RIGHT) {
this.handleNext();
- } else if (e.keyCode === 37) {
+ } else if (e.keyCode === KeyCodes.LEFT) {
this.handlePrev();
}
}
@@ -121,6 +125,36 @@ export default class ViewImageModal extends React.Component {
this.setState({loaded});
}
}
+ playGif(e, filename, fileUrl) {
+ var isLoading = this.state.isLoading;
+ var isPlaying = this.state.isPlaying;
+
+ isLoading[filename] = fileUrl;
+ this.setState({isLoading});
+
+ var img = new Image();
+ img.load(fileUrl);
+ img.onload = () => {
+ delete isLoading[filename];
+ isPlaying[filename] = fileUrl;
+ this.setState({isPlaying, isLoading});
+ };
+ img.onError = () => {
+ delete isLoading[filename];
+ this.setState({isLoading});
+ };
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ stopGif(e, filename) {
+ var isPlaying = this.state.isPlaying;
+ delete isPlaying[filename];
+ this.setState({isPlaying});
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
componentDidMount() {
$(window).on('keyup', this.handleKeyPress);
@@ -153,13 +187,17 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
+ if (filename in this.state.isPlaying) {
+ return this.state.isPlaying[filename];
+ }
+
// This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
}
fileInfo.path = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
- return fileInfo.path + '_preview.jpg';
+ return fileInfo.path + '_preview.jpg' + '?' + Utils.getSessionIndex();
}
// only images have proper previews, so just use a placeholder icon for non-images
@@ -188,12 +226,62 @@ export default class ViewImageModal extends React.Component {
var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') {
+ if (!(filename in this.state.fileMimes)) {
+ Client.getFileInfo(
+ filename,
+ (data) => {
+ if (this.canSetState) {
+ var fileMimes = this.state.fileMimes;
+ fileMimes[filename] = data.mime;
+ this.setState(fileMimes);
+ }
+ },
+ () => {}
+ );
+ }
+
+ var playbackControls = '';
+ if (this.state.fileMimes[filename] === 'image/gif' && !(filename in this.state.isLoading)) {
+ if (filename in this.state.isPlaying) {
+ playbackControls = (
+ <div
+ className='file-playback-controls stop'
+ onClick={(e) => this.stopGif(e, filename)}
+ >
+ {"■"}
+ </div>
+ );
+ } else {
+ playbackControls = (
+ <div
+ className='file-playback-controls play'
+ onClick={(e) => this.playGif(e, filename, fileUrl)}
+ >
+ {"►"}
+ </div>
+ );
+ }
+ }
+
+ var loadingIndicator = '';
+ if (this.state.isLoading[filename] === fileUrl) {
+ loadingIndicator = (
+ <img
+ className='spinner file__loading'
+ src='/static/images/load.gif'
+ />
+ );
+ playbackControls = '';
+ }
+
// image files just show a preview of the file
content = (
<a
href={fileUrl}
target='_blank'
>
+ {loadingIndicator}
+ {playbackControls}
<img
style={{maxHeight: this.state.imgHeight}}
ref='image'
@@ -218,7 +306,7 @@ export default class ViewImageModal extends React.Component {
width={width}
height={height}
>
- <source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} />
+ <source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + Utils.getSessionIndex()} />
</video>
);
} else {
diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx
index 5b3ee540c..1287f4fba 100644
--- a/web/react/components/view_image_popover_bar.jsx
+++ b/web/react/components/view_image_popover_bar.jsx
@@ -7,7 +7,7 @@ export default class ViewImagePopoverBar extends React.Component {
}
render() {
var publicLink = '';
- if (global.window.config.EnablePublicLink === 'true') {
+ if (global.window.mm_config.EnablePublicLink === 'true') {
publicLink = (
<div>
<a
diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx
index c89cb4edc..ea9ae06f4 100644
--- a/web/react/pages/admin_console.jsx
+++ b/web/react/pages/admin_console.jsx
@@ -5,9 +5,12 @@ var ErrorBar = require('../components/error_bar.jsx');
var SelectTeamModal = require('../components/admin_console/select_team_modal.jsx');
var AdminController = require('../components/admin_console/admin_controller.jsx');
-export function setupAdminConsolePage() {
+export function setupAdminConsolePage(props) {
ReactDOM.render(
- <AdminController />,
+ <AdminController
+ tab={props.ActiveTab}
+ teamId={props.TeamId}
+ />,
document.getElementById('admin_controller')
);
diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx
index 20ed1bf0a..03e049db0 100644
--- a/web/react/pages/channel.jsx
+++ b/web/react/pages/channel.jsx
@@ -35,26 +35,18 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal.
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx');
-var TeamStore = require('../stores/team_store.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
function setupChannelPage(props) {
- TeamStore.setCurrentId(props.TeamId);
-
AppDispatcher.handleViewAction({
type: ActionTypes.CLICK_CHANNEL,
name: props.ChannelName,
id: props.ChannelId
});
- AppDispatcher.handleViewAction({
- type: ActionTypes.CLICK_TEAM,
- id: props.TeamId
- });
-
AsyncClient.getAllPreferences();
// ChannelLoader must be rendered first
@@ -237,7 +229,7 @@ function setupChannelPage(props) {
document.getElementById('register_app_modal')
);
- if (global.window.config.SendEmailNotifications === 'false') {
+ if (global.window.mm_config.SendEmailNotifications === 'false') {
ErrorStore.storeLastError({message: 'Preview Mode: Email notifications have not been configured'});
ErrorStore.emitChange();
}
diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx
index 5f0fa9d96..a59f2afd0 100644
--- a/web/react/pages/home.jsx
+++ b/web/react/pages/home.jsx
@@ -2,14 +2,15 @@
// See License.txt for license information.
var ChannelStore = require('../stores/channel_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
var Constants = require('../utils/constants.jsx');
-function setupHomePage(props) {
+function setupHomePage() {
var last = ChannelStore.getLastVisitedName();
if (last == null || last.length === 0) {
- window.location = props.TeamURL + '/channels/' + Constants.DEFAULT_CHANNEL;
+ window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL;
} else {
- window.location = props.TeamURL + '/channels/' + last;
+ window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last;
}
}
diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx
index c2e7df58e..75fb8aa3c 100644
--- a/web/react/stores/browser_store.jsx
+++ b/web/react/stores/browser_store.jsx
@@ -1,12 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore;
function getPrefix() {
- if (!UserStore) {
- UserStore = require('./user_store.jsx'); //eslint-disable-line global-require
+ if (global.window.mm_user) {
+ return global.window.mm_user.id + '_';
}
- return UserStore.getCurrentId() + '_';
+
+ return 'unknown_';
}
class BrowserStoreClass {
@@ -17,35 +17,55 @@ class BrowserStoreClass {
this.setGlobalItem = this.setGlobalItem.bind(this);
this.getGlobalItem = this.getGlobalItem.bind(this);
this.removeGlobalItem = this.removeGlobalItem.bind(this);
- this.clear = this.clear.bind(this);
this.actionOnItemsWithPrefix = this.actionOnItemsWithPrefix.bind(this);
+ this.actionOnGlobalItemsWithPrefix = this.actionOnGlobalItemsWithPrefix.bind(this);
this.isLocalStorageSupported = this.isLocalStorageSupported.bind(this);
+ this.getLastServerVersion = this.getLastServerVersion.bind(this);
+ this.setLastServerVersion = this.setLastServerVersion.bind(this);
+ this.clear = this.clear.bind(this);
+ this.clearAll = this.clearAll.bind(this);
- var currentVersion = localStorage.getItem('local_storage_version');
- if (currentVersion !== global.window.config.Version) {
- this.clear();
- localStorage.setItem('local_storage_version', global.window.config.Version);
+ var currentVersion = sessionStorage.getItem('storage_version');
+ if (currentVersion !== global.window.mm_config.Version) {
+ sessionStorage.clear();
+ sessionStorage.setItem('storage_version', global.window.mm_config.Version);
}
}
getItem(name, defaultValue) {
- return this.getGlobalItem(getPrefix() + name, defaultValue);
+ var result = null;
+ try {
+ result = JSON.parse(sessionStorage.getItem(getPrefix() + name));
+ } catch (err) {
+ result = null;
+ }
+
+ if (result === null && typeof defaultValue !== 'undefined') {
+ result = defaultValue;
+ }
+
+ return result;
}
setItem(name, value) {
- this.setGlobalItem(getPrefix() + name, value);
+ sessionStorage.setItem(getPrefix() + name, JSON.stringify(value));
}
removeItem(name) {
- localStorage.removeItem(getPrefix() + name);
+ sessionStorage.removeItem(getPrefix() + name);
}
setGlobalItem(name, value) {
try {
- localStorage.setItem(name, JSON.stringify(value));
+ if (this.isLocalStorageSupported()) {
+ localStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ } else {
+ sessionStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ }
} catch (err) {
console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console
localStorage.clear();
+ sessionStorage.clear();
window.location.href = window.location.href;
}
}
@@ -53,7 +73,11 @@ class BrowserStoreClass {
getGlobalItem(name, defaultValue) {
var result = null;
try {
- result = JSON.parse(localStorage.getItem(name));
+ if (this.isLocalStorageSupported()) {
+ result = JSON.parse(localStorage.getItem(getPrefix() + name));
+ } else {
+ result = JSON.parse(sessionStorage.getItem(getPrefix() + name));
+ }
} catch (err) {
result = null;
}
@@ -66,22 +90,46 @@ class BrowserStoreClass {
}
removeGlobalItem(name) {
- localStorage.removeItem(name);
+ if (this.isLocalStorageSupported()) {
+ localStorage.removeItem(getPrefix() + name);
+ } else {
+ sessionStorage.removeItem(getPrefix() + name);
+ }
}
- clear() {
- localStorage.clear();
- sessionStorage.clear();
+ getLastServerVersion() {
+ return sessionStorage.getItem('last_server_version');
+ }
+
+ setLastServerVersion(version) {
+ sessionStorage.setItem('last_server_version', version);
}
/**
* Preforms the given action on each item that has the given prefix
* Signature for action is action(key, value)
*/
+ actionOnGlobalItemsWithPrefix(prefix, action) {
+ var globalPrefix = getPrefix();
+ var globalPrefixiLen = globalPrefix.length;
+
+ var storage = sessionStorage;
+ if (this.isLocalStorageSupported()) {
+ storage = localStorage;
+ }
+
+ for (var key in storage) {
+ if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) {
+ var userkey = key.substring(globalPrefixiLen);
+ action(userkey, this.getGlobalItem(key));
+ }
+ }
+ }
+
actionOnItemsWithPrefix(prefix, action) {
var globalPrefix = getPrefix();
var globalPrefixiLen = globalPrefix.length;
- for (var key in localStorage) {
+ for (var key in sessionStorage) {
if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) {
var userkey = key.substring(globalPrefixiLen);
action(userkey, this.getGlobalItem(key));
@@ -89,6 +137,15 @@ class BrowserStoreClass {
}
}
+ clear() {
+ sessionStorage.clear();
+ }
+
+ clearAll() {
+ sessionStorage.clear();
+ localStorage.clear();
+ }
+
isLocalStorageSupported() {
try {
sessionStorage.setItem('testSession', '1');
diff --git a/web/react/stores/error_store.jsx b/web/react/stores/error_store.jsx
index a4c42dcb7..775b8e006 100644
--- a/web/react/stores/error_store.jsx
+++ b/web/react/stores/error_store.jsx
@@ -34,9 +34,11 @@ class ErrorStoreClass extends EventEmitter {
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
+
handledError() {
BrowserStore.removeItem('last_error');
}
+
getLastError() {
return BrowserStore.getItem('last_error');
}
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx
index 8609d8bbf..4a9314b31 100644
--- a/web/react/stores/post_store.jsx
+++ b/web/react/stores/post_store.jsx
@@ -230,7 +230,7 @@ class PostStoreClass extends EventEmitter {
getPosts(channelId) {
return BrowserStore.getItem('posts_' + channelId);
}
- getCurrentUsersLatestPost(channelId) {
+ getCurrentUsersLatestPost(channelId, rootId) {
const userId = UserStore.getCurrentId();
var postList = makePostListNonNull(this.getPosts(channelId));
var i = 0;
@@ -239,8 +239,15 @@ class PostStoreClass extends EventEmitter {
for (i; i < len; i++) {
if (postList.posts[postList.order[i]].user_id === userId) {
- lastPost = postList.posts[postList.order[i]];
- break;
+ if (rootId) {
+ if (postList.posts[postList.order[i]].root_id === rootId || postList.posts[postList.order[i]].id === rootId) {
+ lastPost = postList.posts[postList.order[i]];
+ break;
+ }
+ } else {
+ lastPost = postList.posts[postList.order[i]];
+ break;
+ }
}
}
@@ -317,10 +324,10 @@ class PostStoreClass extends EventEmitter {
return 0;
});
- BrowserStore.setItem('pending_posts_' + channelId, postList);
+ BrowserStore.setGlobalItem('pending_posts_' + channelId, postList);
}
getPendingPosts(channelId) {
- return BrowserStore.getItem('pending_posts_' + channelId);
+ return BrowserStore.getGlobalItem('pending_posts_' + channelId);
}
storeUnseenDeletedPost(post) {
var posts = this.getUnseenDeletedPosts(post.channel_id);
@@ -364,7 +371,7 @@ class PostStoreClass extends EventEmitter {
this.pStorePendingPosts(channelId, postList);
}
clearPendingPosts() {
- BrowserStore.actionOnItemsWithPrefix('pending_posts_', function clearPending(key) {
+ BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) {
BrowserStore.removeItem(key);
});
}
@@ -407,26 +414,26 @@ class PostStoreClass extends EventEmitter {
}
storeCurrentDraft(draft) {
var channelId = ChannelStore.getCurrentId();
- BrowserStore.setItem('draft_' + channelId, draft);
+ BrowserStore.setGlobalItem('draft_' + channelId, draft);
}
getCurrentDraft() {
var channelId = ChannelStore.getCurrentId();
return this.getDraft(channelId);
}
storeDraft(channelId, draft) {
- BrowserStore.setItem('draft_' + channelId, draft);
+ BrowserStore.setGlobalItem('draft_' + channelId, draft);
}
getDraft(channelId) {
- return BrowserStore.getItem('draft_' + channelId, this.getEmptyDraft());
+ return BrowserStore.getGlobalItem('draft_' + channelId, this.getEmptyDraft());
}
storeCommentDraft(parentPostId, draft) {
- BrowserStore.setItem('comment_draft_' + parentPostId, draft);
+ BrowserStore.setGlobalItem('comment_draft_' + parentPostId, draft);
}
getCommentDraft(parentPostId) {
- return BrowserStore.getItem('comment_draft_' + parentPostId, this.getEmptyDraft());
+ return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft());
}
clearDraftUploads() {
- BrowserStore.actionOnItemsWithPrefix('draft_', function clearUploads(key, value) {
+ BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setItem(key, value);
@@ -434,7 +441,7 @@ class PostStoreClass extends EventEmitter {
});
}
clearCommentDraftUploads() {
- BrowserStore.actionOnItemsWithPrefix('comment_draft_', function clearUploads(key, value) {
+ BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setItem(key, value);
diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx
index 77e7067ad..9410c1e9c 100644
--- a/web/react/stores/socket_store.jsx
+++ b/web/react/stores/socket_store.jsx
@@ -1,15 +1,22 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var UserStore = require('./user_store.jsx');
-var ErrorStore = require('./error_store.jsx');
-var EventEmitter = require('events').EventEmitter;
+const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+const UserStore = require('./user_store.jsx');
+const PostStore = require('./post_store.jsx');
+const ChannelStore = require('./channel_store.jsx');
+const BrowserStore = require('./browser_store.jsx');
+const ErrorStore = require('./error_store.jsx');
+const EventEmitter = require('events').EventEmitter;
-var Constants = require('../utils/constants.jsx');
-var ActionTypes = Constants.ActionTypes;
+const Utils = require('../utils/utils.jsx');
+const AsyncClient = require('../utils/async_client.jsx');
-var CHANGE_EVENT = 'change';
+const Constants = require('../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+const SocketEvents = Constants.SocketEvents;
+
+const CHANGE_EVENT = 'change';
var conn;
@@ -31,6 +38,10 @@ class SocketStoreClass extends EventEmitter {
return;
}
+ if (!global.window.hasOwnProperty('mm_session_token_index')) {
+ return;
+ }
+
this.setMaxListeners(0);
if (window.WebSocket && !conn) {
@@ -38,7 +49,9 @@ class SocketStoreClass extends EventEmitter {
if (window.location.protocol === 'https:') {
protocol = 'wss://';
}
- var connUrl = protocol + location.host + '/api/v1/websocket';
+
+ var connUrl = protocol + location.host + '/api/v1/websocket?' + Utils.getSessionIndex();
+
if (this.failCount === 0) {
console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
}
@@ -94,6 +107,39 @@ class SocketStoreClass extends EventEmitter {
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
+ handleMessage(msg) {
+ switch (msg.action) {
+ case SocketEvents.POSTED:
+ handleNewPostEvent(msg);
+ break;
+
+ case SocketEvents.POST_EDITED:
+ handlePostEditEvent(msg);
+ break;
+
+ case SocketEvents.POST_DELETED:
+ handlePostDeleteEvent(msg);
+ break;
+
+ case SocketEvents.NEW_USER:
+ handleNewUserEvent();
+ break;
+
+ case SocketEvents.USER_ADDED:
+ handleUserAddedEvent(msg);
+ break;
+
+ case SocketEvents.USER_REMOVED:
+ handleUserRemovedEvent(msg);
+ break;
+
+ case SocketEvents.CHANNEL_VIEWED:
+ handleChannelViewedEvent(msg);
+ break;
+
+ default:
+ }
+ }
sendMessage(msg) {
if (conn && conn.readyState === WebSocket.OPEN) {
conn.send(JSON.stringify(msg));
@@ -104,6 +150,138 @@ class SocketStoreClass extends EventEmitter {
}
}
+function handleNewPostEvent(msg) {
+ // Store post
+ const post = JSON.parse(msg.props.post);
+ PostStore.storePost(post);
+
+ // Update channel state
+ if (ChannelStore.getCurrentId() === msg.channel_id) {
+ if (window.isActive) {
+ AsyncClient.updateLastViewedAt(true);
+ }
+ } else {
+ AsyncClient.getChannel(msg.channel_id);
+ }
+
+ // Send desktop notification
+ if (UserStore.getCurrentId() !== msg.user_id) {
+ const msgProps = msg.props;
+
+ let mentions = [];
+ if (msgProps.mentions) {
+ mentions = JSON.parse(msg.props.mentions);
+ }
+
+ const channel = ChannelStore.get(msg.channel_id);
+ const user = UserStore.getCurrentUser();
+ const member = ChannelStore.getMember(msg.channel_id);
+
+ let notifyLevel = member && member.notify_props ? member.notify_props.desktop : 'default';
+ if (notifyLevel === 'default') {
+ notifyLevel = user.notify_props.desktop;
+ }
+
+ if (notifyLevel === 'none') {
+ return;
+ } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== 'D') {
+ return;
+ }
+
+ let username = 'Someone';
+ if (UserStore.hasProfile(msg.user_id)) {
+ username = UserStore.getProfile(msg.user_id).username;
+ }
+
+ let title = 'Posted';
+ if (channel) {
+ title = channel.display_name;
+ }
+
+ let notifyText = post.message.replace(/\n+/g, ' ');
+ if (notifyText.length > 50) {
+ notifyText = notifyText.substring(0, 49) + '...';
+ }
+
+ if (notifyText.length === 0) {
+ if (msgProps.image) {
+ Utils.notifyMe(title, username + ' uploaded an image', channel);
+ } else if (msgProps.otherFile) {
+ Utils.notifyMe(title, username + ' uploaded a file', channel);
+ } else {
+ Utils.notifyMe(title, username + ' did something new', channel);
+ }
+ } else {
+ Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel);
+ }
+ if (!user.notify_props || user.notify_props.desktop_sound === 'true') {
+ Utils.ding();
+ }
+ }
+}
+
+function handlePostEditEvent(msg) {
+ // Store post
+ const post = JSON.parse(msg.props.post);
+ PostStore.storePost(post);
+
+ // Update channel state
+ if (ChannelStore.getCurrentId() === msg.channel_id) {
+ if (window.isActive) {
+ AsyncClient.updateLastViewedAt();
+ }
+ }
+}
+
+function handlePostDeleteEvent(msg) {
+ const post = JSON.parse(msg.props.post);
+
+ PostStore.storeUnseenDeletedPost(post);
+ PostStore.removePost(post, true);
+ PostStore.emitChange();
+}
+
+function handleNewUserEvent() {
+ AsyncClient.getProfiles();
+ AsyncClient.getChannelExtraInfo(true);
+}
+
+function handleUserAddedEvent(msg) {
+ if (ChannelStore.getCurrentId() === msg.channel_id) {
+ AsyncClient.getChannelExtraInfo(true);
+ }
+
+ if (UserStore.getCurrentId() === msg.user_id) {
+ AsyncClient.getChannel(msg.channel_id);
+ }
+}
+
+function handleUserRemovedEvent(msg) {
+ if (UserStore.getCurrentId() === msg.user_id) {
+ AsyncClient.getChannels();
+
+ if (msg.props.remover_id !== msg.user_id &&
+ msg.channel_id === ChannelStore.getCurrentId() &&
+ $('#removed_from_channel').length > 0) {
+ var sentState = {};
+ sentState.channelName = ChannelStore.getCurrent().display_name;
+ sentState.remover = UserStore.getProfile(msg.props.remover_id).username;
+
+ BrowserStore.setItem('channel-removed-state', sentState);
+ $('#removed_from_channel').modal('show');
+ }
+ } else if (ChannelStore.getCurrentId() === msg.channel_id) {
+ AsyncClient.getChannelExtraInfo(true);
+ }
+}
+
+function handleChannelViewedEvent(msg) {
+ // Useful for when multiple devices have the app open to different channels
+ if (ChannelStore.getCurrentId() !== msg.channel_id && UserStore.getCurrentId() === msg.user_id) {
+ AsyncClient.getChannel(msg.channel_id);
+ }
+}
+
var SocketStore = new SocketStoreClass();
SocketStore.dispatchToken = AppDispatcher.register((payload) => {
@@ -111,6 +289,7 @@ SocketStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) {
case ActionTypes.RECIEVED_MSG:
+ SocketStore.handleMessage(action.msg);
SocketStore.emitChange(action.msg);
break;
diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx
index 7001acdb1..22114ae85 100644
--- a/web/react/stores/team_store.jsx
+++ b/web/react/stores/team_store.jsx
@@ -28,29 +28,31 @@ class TeamStoreClass extends EventEmitter {
this.get = this.get.bind(this);
this.getByName = this.getByName.bind(this);
this.getAll = this.getAll.bind(this);
- this.setCurrentId = this.setCurrentId.bind(this);
this.getCurrentId = this.getCurrentId.bind(this);
this.getCurrent = this.getCurrent.bind(this);
this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this);
- this.storeTeam = this.storeTeam.bind(this);
- this.pStoreTeams = this.pStoreTeams.bind(this);
- this.pGetTeams = this.pGetTeams.bind(this);
+ this.saveTeam = this.saveTeam.bind(this);
}
+
emitChange() {
this.emit(CHANGE_EVENT);
}
+
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
}
+
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
+
get(id) {
- var c = this.pGetTeams();
+ var c = this.getAll();
return c[id];
}
+
getByName(name) {
- var t = this.pGetTeams();
+ var t = this.getAll();
for (var id in t) {
if (t[id].name === name) {
@@ -60,59 +62,51 @@ class TeamStoreClass extends EventEmitter {
return null;
}
+
getAll() {
- return this.pGetTeams();
- }
- setCurrentId(id) {
- if (id === null) {
- BrowserStore.removeItem('current_team_id');
- } else {
- BrowserStore.setItem('current_team_id', id);
- }
+ return BrowserStore.getItem('user_teams', {});
}
+
getCurrentId() {
- return BrowserStore.getItem('current_team_id');
- }
- getCurrent() {
- var currentId = this.getCurrentId();
+ var team = global.window.mm_team;
- if (currentId !== null) {
- return this.get(currentId);
+ if (team) {
+ return team.id;
}
+
return null;
}
+
+ getCurrent() {
+ if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) {
+ this.saveTeam(global.window.mm_team);
+ }
+
+ return global.window.mm_team;
+ }
+
getCurrentTeamUrl() {
if (this.getCurrent()) {
return getWindowLocationOrigin() + '/' + this.getCurrent().name;
}
return null;
}
- storeTeam(team) {
- var teams = this.pGetTeams();
+
+ saveTeam(team) {
+ var teams = this.getAll();
teams[team.id] = team;
- this.pStoreTeams(teams);
- }
- pStoreTeams(teams) {
BrowserStore.setItem('user_teams', teams);
}
- pGetTeams() {
- return BrowserStore.getItem('user_teams', {});
- }
}
var TeamStore = new TeamStoreClass();
-TeamStore.dispatchToken = AppDispatcher.register(function registry(payload) {
+TeamStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
- case ActionTypes.CLICK_TEAM:
- TeamStore.setCurrentId(action.id);
- TeamStore.emitChange();
- break;
-
case ActionTypes.RECIEVED_TEAM:
- TeamStore.storeTeam(action.team);
+ TeamStore.saveTeam(action.team);
TeamStore.emitChange();
break;
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index fa74f812d..ce80c5ec9 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -3,7 +3,6 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
-var client = require('../utils/client.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -38,23 +37,19 @@ class UserStoreClass extends EventEmitter {
this.emitToggleImportModal = this.emitToggleImportModal.bind(this);
this.addImportModalListener = this.addImportModalListener.bind(this);
this.removeImportModalListener = this.removeImportModalListener.bind(this);
- this.setCurrentId = this.setCurrentId.bind(this);
this.getCurrentId = this.getCurrentId.bind(this);
this.getCurrentUser = this.getCurrentUser.bind(this);
this.setCurrentUser = this.setCurrentUser.bind(this);
this.getLastEmail = this.getLastEmail.bind(this);
this.setLastEmail = this.setLastEmail.bind(this);
- this.removeCurrentUser = this.removeCurrentUser.bind(this);
this.hasProfile = this.hasProfile.bind(this);
this.getProfile = this.getProfile.bind(this);
this.getProfileByUsername = this.getProfileByUsername.bind(this);
this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this);
this.getProfiles = this.getProfiles.bind(this);
this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this);
+ this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this);
this.saveProfile = this.saveProfile.bind(this);
- this.pStoreProfiles = this.pStoreProfiles.bind(this);
- this.pGetProfiles = this.pGetProfiles.bind(this);
- this.pGetProfilesUsernameMap = this.pGetProfilesUsernameMap.bind(this);
this.setSessions = this.setSessions.bind(this);
this.getSessions = this.getSessions.bind(this);
this.setAudits = this.setAudits.bind(this);
@@ -62,138 +57,155 @@ class UserStoreClass extends EventEmitter {
this.setTeams = this.setTeams.bind(this);
this.getTeams = this.getTeams.bind(this);
this.getCurrentMentionKeys = this.getCurrentMentionKeys.bind(this);
- this.getLastVersion = this.getLastVersion.bind(this);
- this.setLastVersion = this.setLastVersion.bind(this);
this.setStatuses = this.setStatuses.bind(this);
this.pSetStatuses = this.pSetStatuses.bind(this);
this.setStatus = this.setStatus.bind(this);
this.getStatuses = this.getStatuses.bind(this);
this.getStatus = this.getStatus.bind(this);
-
- this.gCurrentId = null;
}
emitChange(userId) {
this.emit(CHANGE_EVENT, userId);
}
+
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
}
+
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
+
emitSessionsChange() {
this.emit(CHANGE_EVENT_SESSIONS);
}
+
addSessionsChangeListener(callback) {
this.on(CHANGE_EVENT_SESSIONS, callback);
}
+
removeSessionsChangeListener(callback) {
this.removeListener(CHANGE_EVENT_SESSIONS, callback);
}
+
emitAuditsChange() {
this.emit(CHANGE_EVENT_AUDITS);
}
+
addAuditsChangeListener(callback) {
this.on(CHANGE_EVENT_AUDITS, callback);
}
+
removeAuditsChangeListener(callback) {
this.removeListener(CHANGE_EVENT_AUDITS, callback);
}
+
emitTeamsChange() {
this.emit(CHANGE_EVENT_TEAMS);
}
+
addTeamsChangeListener(callback) {
this.on(CHANGE_EVENT_TEAMS, callback);
}
+
removeTeamsChangeListener(callback) {
this.removeListener(CHANGE_EVENT_TEAMS, callback);
}
+
emitStatusesChange() {
this.emit(CHANGE_EVENT_STATUSES);
}
+
addStatusesChangeListener(callback) {
this.on(CHANGE_EVENT_STATUSES, callback);
}
+
removeStatusesChangeListener(callback) {
this.removeListener(CHANGE_EVENT_STATUSES, callback);
}
+
emitToggleImportModal(value) {
this.emit(TOGGLE_IMPORT_MODAL_EVENT, value);
}
+
addImportModalListener(callback) {
this.on(TOGGLE_IMPORT_MODAL_EVENT, callback);
}
+
removeImportModalListener(callback) {
this.removeListener(TOGGLE_IMPORT_MODAL_EVENT, callback);
}
- setCurrentId(id) {
- this.gCurrentId = id;
- if (id == null) {
- BrowserStore.removeGlobalItem('current_user_id');
- } else {
- BrowserStore.setGlobalItem('current_user_id', id);
+
+ getCurrentUser() {
+ if (this.getProfiles()[global.window.mm_user.id] == null) {
+ this.saveProfile(global.window.mm_user);
}
+
+ return global.window.mm_user;
}
- getCurrentId(skipFetch) {
- var currentId = this.gCurrentId;
- if (currentId == null) {
- currentId = BrowserStore.getGlobalItem('current_user_id');
- this.gCurrentId = currentId;
- }
+ setCurrentUser(user) {
+ var oldUser = global.window.mm_user;
- // this is a special case to force fetch the
- // current user if it's missing
- // it's synchronous to block rendering
- if (currentId == null && !skipFetch) {
- var me = client.getMeSynchronous();
- if (me != null) {
- this.setCurrentUser(me);
- currentId = me.id;
- }
+ if (oldUser.id === user.id) {
+ global.window.mm_user = user;
+ this.saveProfile(user);
+ } else {
+ throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id);
}
-
- return currentId;
}
- getCurrentUser() {
- if (this.getCurrentId() == null) {
- return null;
+
+ getCurrentId() {
+ var user = global.window.mm_user;
+
+ if (user) {
+ return user.id;
}
- return this.pGetProfiles()[this.getCurrentId()];
- }
- setCurrentUser(user) {
- this.setCurrentId(user.id);
- this.saveProfile(user);
+ return null;
}
+
getLastEmail() {
- return BrowserStore.getItem('last_email', '');
+ return BrowserStore.getGlobalItem('last_email', '');
}
+
setLastEmail(email) {
- BrowserStore.setItem('last_email', email);
- }
- removeCurrentUser() {
- this.setCurrentId(null);
+ BrowserStore.setGlobalItem('last_email', email);
}
+
hasProfile(userId) {
- return this.pGetProfiles()[userId] != null;
+ return this.getProfiles()[userId] != null;
}
+
getProfile(userId) {
- return this.pGetProfiles()[userId];
+ return this.getProfiles()[userId];
}
+
getProfileByUsername(username) {
- return this.pGetProfilesUsernameMap()[username];
+ return this.getProfilesUsernameMap()[username];
}
+
getProfilesUsernameMap() {
- return this.pGetProfilesUsernameMap();
+ var profileUsernameMap = {};
+
+ var profiles = this.getProfiles();
+ for (var key in profiles) {
+ if (profiles.hasOwnProperty(key)) {
+ var profile = profiles[key];
+ profileUsernameMap[profile.username] = profile;
+ }
+ }
+
+ return profileUsernameMap;
}
+
getProfiles() {
- return this.pGetProfiles();
+ return BrowserStore.getItem('profiles', {});
}
+
getActiveOnlyProfiles() {
var active = {};
- var current = this.pGetProfiles();
+ var current = this.getProfiles();
for (var key in current) {
if (current[key].delete_at === 0) {
@@ -203,45 +215,50 @@ class UserStoreClass extends EventEmitter {
return active;
}
- saveProfile(profile) {
- var ps = this.pGetProfiles();
- ps[profile.id] = profile;
- this.pStoreProfiles(ps);
- }
- pStoreProfiles(profiles) {
- BrowserStore.setItem('profiles', profiles);
- var profileUsernameMap = {};
- for (var id in profiles) {
- if (profiles.hasOwnProperty(id)) {
- profileUsernameMap[profiles[id].username] = profiles[id];
+
+ getActiveOnlyProfileList() {
+ const profileMap = this.getActiveOnlyProfiles();
+ const profiles = [];
+
+ for (const id in profileMap) {
+ if (profileMap.hasOwnProperty(id)) {
+ profiles.push(profileMap[id]);
}
}
- BrowserStore.setItem('profileUsernameMap', profileUsernameMap);
- }
- pGetProfiles() {
- return BrowserStore.getItem('profiles', {});
+
+ return profiles;
}
- pGetProfilesUsernameMap() {
- return BrowserStore.getItem('profileUsernameMap', {});
+
+ saveProfile(profile) {
+ var ps = this.getProfiles();
+ ps[profile.id] = profile;
+ BrowserStore.setItem('profiles', ps);
}
+
setSessions(sessions) {
BrowserStore.setItem('sessions', sessions);
}
+
getSessions() {
return BrowserStore.getItem('sessions', {loading: true});
}
+
setAudits(audits) {
BrowserStore.setItem('audits', audits);
}
+
getAudits() {
return BrowserStore.getItem('audits', {loading: true});
}
+
setTeams(teams) {
BrowserStore.setItem('teams', teams);
}
+
getTeams() {
return BrowserStore.getItem('teams', []);
}
+
getCurrentMentionKeys() {
var user = this.getCurrentUser();
@@ -269,28 +286,27 @@ class UserStoreClass extends EventEmitter {
return keys;
}
- getLastVersion() {
- return BrowserStore.getItem('last_version', '');
- }
- setLastVersion(version) {
- BrowserStore.setItem('last_version', version);
- }
+
setStatuses(statuses) {
this.pSetStatuses(statuses);
this.emitStatusesChange();
}
+
pSetStatuses(statuses) {
BrowserStore.setItem('statuses', statuses);
}
+
setStatus(userId, status) {
var statuses = this.getStatuses();
statuses[userId] = status;
this.pSetStatuses(statuses);
this.emitStatusesChange();
}
+
getStatuses() {
return BrowserStore.getItem('statuses', {});
}
+
getStatus(id) {
return this.getStatuses()[id];
}
@@ -299,7 +315,7 @@ class UserStoreClass extends EventEmitter {
var UserStore = new UserStoreClass();
UserStore.setMaxListeners(0);
-UserStore.dispatchToken = AppDispatcher.register(function registry(payload) {
+UserStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index b22d7237e..b1bc71d54 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -3,6 +3,7 @@
var client = require('./client.jsx');
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
+var BrowserStore = require('../stores/browser_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
@@ -50,18 +51,18 @@ export function getChannels(force, updateLastViewed, checkVersion) {
callTracker.getChannels = utils.getTimestamp();
client.getChannels(
- function getChannelsSuccess(data, textStatus, xhr) {
+ (data, textStatus, xhr) => {
callTracker.getChannels = 0;
if (checkVersion) {
var serverVersion = xhr.getResponseHeader('X-Version-ID');
- if (!UserStore.getLastVersion()) {
- UserStore.setLastVersion(serverVersion);
+ if (!BrowserStore.getLastServerVersion()) {
+ BrowserStore.setLastServerVersion(serverVersion);
}
- if (serverVersion !== UserStore.getLastVersion()) {
- UserStore.setLastVersion(serverVersion);
+ if (serverVersion !== BrowserStore.getLastServerVersion()) {
+ BrowserStore.setLastServerVersion(serverVersion);
window.location.href = window.location.href;
console.log('Detected version update refreshing the page'); //eslint-disable-line no-console
}
@@ -77,7 +78,7 @@ export function getChannels(force, updateLastViewed, checkVersion) {
members: data.members
});
},
- function getChannelsFailure(err) {
+ (err) => {
callTracker.getChannels = 0;
dispatchError(err, 'getChannels');
}
@@ -151,14 +152,14 @@ export function getChannel(id) {
);
}
-export function updateLastViewedAt() {
+export function updateLastViewedAt(force) {
const channelId = ChannelStore.getCurrentId();
if (channelId === null) {
return;
}
- if (isCallInProgress(`updateLastViewed${channelId}`)) {
+ if (isCallInProgress(`updateLastViewed${channelId}`) && !force) {
return;
}
@@ -566,8 +567,8 @@ export function getMe() {
}
callTracker.getMe = utils.getTimestamp();
- client.getMeSynchronous(
- function getMeSyncSuccess(data, textStatus, xhr) {
+ client.getMe(
+ (data, textStatus, xhr) => {
callTracker.getMe = 0;
if (xhr.status === 304 || !data) {
@@ -579,7 +580,7 @@ export function getMe() {
me: data
});
},
- function getMeSyncFailure(err) {
+ (err) => {
callTracker.getMe = 0;
dispatchError(err, 'getMe');
}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index f6aee362c..bc73f3c64 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -4,8 +4,8 @@ var BrowserStore = require('../stores/browser_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var ErrorStore = require('../stores/error_store.jsx');
-export function track(category, action, label, prop, val) {
- global.window.analytics.track(action, {category: category, label: label, property: prop, value: val});
+export function track(category, action, label, property, value) {
+ global.window.analytics.track(action, {category, label, property, value});
}
export function trackPage() {
@@ -232,6 +232,7 @@ export function logout() {
track('api', 'api_users_logout');
var currentTeamUrl = TeamStore.getCurrentTeamUrl();
BrowserStore.clear();
+ ErrorStore.storeLastError(null);
window.location.href = currentTeamUrl + '/logout';
}
@@ -385,10 +386,9 @@ export function getAllTeams(success, error) {
});
}
-export function getMeSynchronous(success, error) {
+export function getMe(success, error) {
var currentUser = null;
$.ajax({
- async: false,
cache: false,
url: '/api/v1/users/me',
dataType: 'json',
@@ -402,7 +402,7 @@ export function getMeSynchronous(success, error) {
},
error: function onError(xhr, status, err) {
if (error) {
- var e = handleError('getMeSynchronous', xhr, status, err);
+ var e = handleError('getMe', xhr, status, err);
error(e);
}
}
@@ -1182,3 +1182,61 @@ export function savePreferences(preferences, success, error) {
}
});
}
+
+export function addOutgoingHook(hook, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(hook),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addOutgoingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteOutgoingHook(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteOutgoingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listOutgoingHooks(success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/list',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listOutgoingHooks', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function regenOutgoingHookToken(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/regen_token',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('regenOutgoingHookToken', xhr, status, err);
+ error(e);
+ }
+ });
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index b7b8d3c60..72773bf05 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -33,7 +33,6 @@ module.exports = {
RECIEVED_MSG: null,
- CLICK_TEAM: null,
RECIEVED_TEAM: null,
RECIEVED_CONFIG: null,
@@ -47,6 +46,18 @@ module.exports = {
SERVER_ACTION: null,
VIEW_ACTION: null
}),
+
+ SocketEvents: {
+ POSTED: 'posted',
+ POST_EDITED: 'post_edited',
+ POST_DELETED: 'post_deleted',
+ CHANNEL_VIEWED: 'channel_viewed',
+ NEW_USER: 'new_user',
+ USER_ADDED: 'user_added',
+ USER_REMOVED: 'user_removed',
+ TYPING: 'typing'
+ },
+
SPECIAL_MENTIONS: ['all', 'channel'],
CHARACTER_LIMIT: 4000,
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
@@ -114,6 +125,7 @@ module.exports = {
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20,
DM_CHANNEL: 'D',
+ OPEN_CHANNEL: 'O',
MAX_POST_LEN: 4000,
EMOJI_SIZE: 16,
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
@@ -127,7 +139,7 @@ module.exports = {
sidebarText: '#333333',
sidebarUnreadText: '#333333',
sidebarTextHoverBg: '#e6f2fa',
- sidebarTextActiveBg: '#e1e1e1',
+ sidebarTextActiveBorder: '#378FD2',
sidebarTextActiveColor: '#111111',
sidebarHeaderBg: '#2389d7',
sidebarHeaderTextColor: '#ffffff',
@@ -149,7 +161,7 @@ module.exports = {
sidebarText: '#fff',
sidebarUnreadText: '#fff',
sidebarTextHoverBg: '#136197',
- sidebarTextActiveBg: '#136197',
+ sidebarTextActiveBorder: '#7AB0D6',
sidebarTextActiveColor: '#FFFFFF',
sidebarHeaderBg: '#2f81b7',
sidebarHeaderTextColor: '#FFFFFF',
@@ -171,7 +183,7 @@ module.exports = {
sidebarText: '#fff',
sidebarUnreadText: '#fff',
sidebarTextHoverBg: '#4A5664',
- sidebarTextActiveBg: '#39769C',
+ sidebarTextActiveBorder: '#39769C',
sidebarTextActiveColor: '#FFFFFF',
sidebarHeaderBg: '#1B2C3E',
sidebarHeaderTextColor: '#FFFFFF',
@@ -193,7 +205,7 @@ module.exports = {
sidebarText: '#fff',
sidebarUnreadText: '#fff',
sidebarTextHoverBg: '#302e30',
- sidebarTextActiveBg: '#484748',
+ sidebarTextActiveBorder: '#196CAF',
sidebarTextActiveColor: '#FFFFFF',
sidebarHeaderBg: '#1f1f1f',
sidebarHeaderTextColor: '#FFFFFF',
@@ -236,8 +248,8 @@ module.exports = {
uiName: 'Sidebar Text Hover BG'
},
{
- id: 'sidebarTextActiveBg',
- uiName: 'Sidebar Text Active BG'
+ id: 'sidebarTextActiveBorder',
+ uiName: 'Sidebar Text Active Border'
},
{
id: 'sidebarTextActiveColor',
@@ -299,6 +311,7 @@ module.exports = {
RIGHT: 39,
BACKSPACE: 8,
ENTER: 13,
- ESCAPE: 27
+ ESCAPE: 27,
+ SPACE: 32
}
};
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
index 94bb91503..bb948b6dc 100644
--- a/web/react/utils/emoticons.jsx
+++ b/web/react/utils/emoticons.jsx
@@ -2,26 +2,27 @@
// See License.txt for license information.
const emoticonPatterns = {
- smile: /(^|\s)(:-?\))($|\s)/g, // :)
- open_mouth: /(^|\s)(:o)($|\s)/gi, // :o
- scream: /(^|\s)(:-o)($|\s)/gi, // :-o
- smirk: /(^|\s)(:-?])($|\s)/g, // :]
- grinning: /(^|\s)(:-?d)($|\s)/gi, // :D
- stuck_out_tongue_closed_eyes: /(^|\s)(x-d)($|\s)/gi, // x-d
- stuck_out_tongue: /(^|\s)(:-?p)($|\s)/gi, // :p
- rage: /(^|\s)(:-?[\[@])($|\s)/g, // :@
- frowning: /(^|\s)(:-?\()($|\s)/g, // :(
- sob: /(^|\s)(:['’]-?\(|:&#x27;\(|:&#39;\()($|\s)/g, // :`(
- kissing_heart: /(^|\s)(:-?\*)($|\s)/g, // :*
- pensive: /(^|\s)(:-?\/)($|\s)/g, // :/
- confounded: /(^|\s)(:-?s)($|\s)/gi, // :s
- flushed: /(^|\s)(:-?\|)($|\s)/g, // :|
- relaxed: /(^|\s)(:-?\$)($|\s)/g, // :$
- mask: /(^|\s)(:-x)($|\s)/gi, // :-x
- heart: /(^|\s)(<3|&lt;3)($|\s)/g, // <3
- broken_heart: /(^|\s)(<\/3|&lt;&#x2F;3)($|\s)/g, // </3
- thumbsup: /(^|\s)(:\+1:)($|\s)/g, // :+1:
- thumbsdown: /(^|\s)(:\-1:)($|\s)/g // :-1:
+ smile: /(^|\s)(:-?\))(?=$|\s)/g, // :)
+ wink: /(^|\s)(;-?\))(?=$|\s)/g, // ;)
+ open_mouth: /(^|\s)(:o)(?=$|\s)/gi, // :o
+ scream: /(^|\s)(:-o)(?=$|\s)/gi, // :-o
+ smirk: /(^|\s)(:-?])(?=$|\s)/g, // :]
+ grinning: /(^|\s)(:-?d)(?=$|\s)/gi, // :D
+ stuck_out_tongue_closed_eyes: /(^|\s)(x-d)(?=$|\s)/gi, // x-d
+ stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p
+ rage: /(^|\s)(:-?[\[@])(?=$|\s)/g, // :@
+ frowning: /(^|\s)(:-?\()(?=$|\s)/g, // :(
+ sob: /(^|\s)(:['’]-?\(|:&#x27;\(|:&#39;\()(?=$|\s)/g, // :`(
+ kissing_heart: /(^|\s)(:-?\*)(?=$|\s)/g, // :*
+ pensive: /(^|\s)(:-?\/)(?=$|\s)/g, // :/
+ confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s
+ flushed: /(^|\s)(:-?\|)(?=$|\s)/g, // :|
+ relaxed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$
+ mask: /(^|\s)(:-x)(?=$|\s)/gi, // :-x
+ heart: /(^|\s)(<3|&lt;3)(?=$|\s)/g, // <3
+ broken_heart: /(^|\s)(<\/3|&lt;&#x2F;3)(?=$|\s)/g, // </3
+ thumbsup: /(^|\s)(:\+1:)(?=$|\s)/g, // :+1:
+ thumbsdown: /(^|\s)(:\-1:)(?=$|\s)/g // :-1:
};
function initializeEmoticonMap() {
@@ -126,28 +127,28 @@ const emoticonMap = initializeEmoticonMap();
export function handleEmoticons(text, tokens) {
let output = text;
- function replaceEmoticonWithToken(match, prefix, name, suffix) {
+ function replaceEmoticonWithToken(fullMatch, prefix, matchText, name) {
if (emoticonMap[name]) {
const index = tokens.size;
const alias = `MM_EMOTICON${index}`;
tokens.set(alias, {
- value: `<img align="absmiddle" alt=${match} class="emoji" src=${getImagePathForEmoticon(name)} title=${match} />`,
- originalText: match
+ value: `<img align="absmiddle" alt="${matchText}" class="emoji" src="${getImagePathForEmoticon(name)}" title="${matchText}" />`,
+ originalText: fullMatch
});
- return prefix + alias + suffix;
+ return prefix + alias;
}
- return match;
+ return fullMatch;
}
- output = output.replace(/(^|\s):([a-zA-Z0-9_-]+):($|\s)/g, replaceEmoticonWithToken);
+ output = output.replace(/(^|\s)(:([a-zA-Z0-9_-]+):)(?=$|\s)/g, (fullMatch, prefix, matchText, name) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
$.each(emoticonPatterns, (name, pattern) => {
// this might look a bit funny, but since the name isn't contained in the actual match
// like with the named emoticons, we need to add it in manually
- output = output.replace(pattern, (match, prefix, emoticon, suffix) => replaceEmoticonWithToken(match, prefix, name, suffix));
+ output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
});
return output;
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 2813798d2..7a4e70054 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -11,6 +11,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
super(options);
this.heading = this.heading.bind(this);
+ this.paragraph = this.paragraph.bind(this);
this.text = this.text.bind(this);
this.formattingOptions = formattingOptions;
@@ -53,11 +54,17 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
}
paragraph(text) {
+ let outText = text;
+
+ if (!('emoticons' in this.options) || this.options.emoticon) {
+ outText = TextFormatting.doFormatEmoticons(text);
+ }
+
if (this.formattingOptions.singleline) {
- return `<p class="markdown__paragraph-inline">${text}</p>`;
+ return `<p class="markdown__paragraph-inline">${outText}</p>`;
}
- return super.paragraph(text);
+ return super.paragraph(outText);
}
table(header, body) {
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index d79aeed68..204c37364 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -69,6 +69,15 @@ export function doFormatText(text, options) {
return output;
}
+export function doFormatEmoticons(text) {
+ const tokens = new Map();
+
+ let output = Emoticons.handleEmoticons(text, tokens);
+ output = replaceTokens(output, tokens);
+
+ return output;
+}
+
export function sanitizeHtml(text) {
let output = text;
@@ -237,7 +246,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
var newTokens = new Map();
for (const [alias, token] of tokens) {
- if (token.originalText === searchTerm) {
+ if (token.originalText.indexOf(searchTerm.replace(/\*$/, '')) > -1) {
const index = tokens.size + newTokens.size;
const newAlias = `MM_SEARCHTERM${index}`;
@@ -267,7 +276,7 @@ function highlightSearchTerm(text, tokens, searchTerm) {
return prefix + alias;
}
- return output.replace(new RegExp(`(^|\\W)(${searchTerm})\\b`, 'gi'), replaceSearchTermWithToken);
+ return output.replace(new RegExp(`()(${searchTerm})`, 'gi'), replaceSearchTermWithToken);
}
function replaceTokens(text, tokens) {
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 561c2c4c4..67a9d6983 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -13,7 +13,8 @@ var client = require('./client.jsx');
var Autolinker = require('autolinker');
export function isEmail(email) {
- var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+ //var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+ var regex = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i;
return regex.test(email);
}
@@ -117,7 +118,7 @@ export function notifyMe(title, body, channel) {
}
if (permission === 'granted') {
- var notification = new Notification(title, {body: body, tag: body, icon: '/static/images/icon50x50.gif'});
+ var notification = new Notification(title, {body: body, tag: body, icon: '/static/images/icon50x50.png'});
notification.onclick = function onClick() {
window.focus();
if (channel) {
@@ -424,16 +425,13 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1);
}
- if (theme.sidebarTextActiveBg) {
- changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'background:' + theme.sidebarTextActiveBg, 1);
+ if (theme.sidebarTextActiveBorder) {
+ changeCss('.sidebar--left .nav li.active a:before, .settings-modal .nav-pills>li.active a:before', 'background:' + theme.sidebarTextActiveBorder, 1);
}
if (theme.sidebarTextActiveColor) {
changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
- }
-
- if (theme.sidebarTextActiveBg === theme.onlineIndicator) {
- changeCss('.sidebar--left .nav-pills__container li.active a .status .online--icon', 'fill:' + theme.sidebarTextActiveColor, 1);
+ changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1);
}
if (theme.sidebarHeaderBg) {
@@ -497,7 +495,7 @@ export function applyTheme(theme) {
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
- changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
changeCss('.popover.left>.arrow', 'border-left-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
@@ -512,19 +510,19 @@ export function applyTheme(theme) {
changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
- changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
+ changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-channel-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
+ changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
- changeCss('.post:hover, .modal .more-channel-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
+ changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
+ changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
@@ -543,6 +541,7 @@ export function applyTheme(theme) {
if (theme.buttonBg) {
changeCss('.btn.btn-primary', 'background:' + theme.buttonBg, 1);
changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background:' + changeColor(theme.buttonBg, -0.25), 1);
+ changeCss('.file-playback-controls', 'color:' + changeColor(theme.buttonBg, -0.25), 1);
}
if (theme.buttonColor) {
@@ -871,7 +870,7 @@ export function getFileUrl(filename) {
if (url.indexOf('/api/v1/files/get') !== -1) {
url = filename.split('/api/v1/files/get')[1];
}
- url = getWindowLocationOrigin() + '/api/v1/files/get' + url;
+ url = getWindowLocationOrigin() + '/api/v1/files/get' + url + '?' + getSessionIndex();
return url;
}
@@ -882,6 +881,14 @@ export function getFileName(path) {
return split[split.length - 1];
}
+export function getSessionIndex() {
+ if (global.window.mm_session_token_index >= 0) {
+ return 'session_token_index=' + global.window.mm_session_token_index;
+ }
+
+ return '';
+}
+
// Generates a RFC-4122 version 4 compliant globally unique identifier.
export function generateId() {
// implementation taken from http://stackoverflow.com/a/2117523
@@ -967,3 +974,11 @@ export function getShortenedTeamURL() {
}
return teamURL + '/';
}
+
+export function windowWidth() {
+ return $(window).width();
+}
+
+export function windowHeight() {
+ return $(window).height();
+}