summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/action_creators/global_actions.jsx34
-rw-r--r--webapp/action_creators/websocket_actions.jsx227
-rw-r--r--webapp/components/about_build_modal.jsx6
-rw-r--r--webapp/components/access_history_modal.jsx8
-rw-r--r--webapp/components/activity_log_modal.jsx8
-rw-r--r--webapp/components/admin_console/license_settings.jsx64
-rw-r--r--webapp/components/channel_header.jsx1
-rw-r--r--webapp/components/channel_invite_button.jsx79
-rw-r--r--webapp/components/channel_invite_modal.jsx51
-rw-r--r--webapp/components/create_comment.jsx8
-rw-r--r--webapp/components/create_post.jsx7
-rw-r--r--webapp/components/filtered_user_list.jsx5
-rw-r--r--webapp/components/invite_member_modal.jsx7
-rw-r--r--webapp/components/logged_in.jsx18
-rw-r--r--webapp/components/more_direct_channels.jsx18
-rw-r--r--webapp/components/msg_typing.jsx78
-rw-r--r--webapp/components/navbar.jsx11
-rw-r--r--webapp/components/popover_list_members.jsx6
-rw-r--r--webapp/components/rhs_comment.jsx12
-rw-r--r--webapp/components/rhs_root_post.jsx5
-rw-r--r--webapp/components/rhs_thread.jsx42
-rw-r--r--webapp/components/search_results.jsx3
-rw-r--r--webapp/components/sidebar.jsx18
-rw-r--r--webapp/components/sidebar_right.jsx65
-rw-r--r--webapp/components/spinner_button.jsx48
-rw-r--r--webapp/components/team_settings_modal.jsx8
-rw-r--r--webapp/components/tutorial/tutorial_intro_screens.jsx19
-rw-r--r--webapp/components/tutorial/tutorial_tip.jsx11
-rw-r--r--webapp/components/user_list.jsx22
-rw-r--r--webapp/components/user_list_row.jsx9
-rw-r--r--webapp/components/user_settings/user_settings_advanced.jsx46
-rw-r--r--webapp/components/user_settings/user_settings_display.jsx40
-rw-r--r--webapp/components/user_settings/user_settings_modal.jsx22
-rw-r--r--webapp/i18n/en.json16
-rw-r--r--webapp/i18n/es.json8
-rw-r--r--webapp/i18n/pt.json56
-rw-r--r--webapp/package.json7
-rw-r--r--webapp/root.jsx42
-rw-r--r--webapp/sass/components/_modal.scss1
-rw-r--r--webapp/sass/components/_spinner-button.scss8
-rw-r--r--webapp/sass/layout/_headers.scss4
-rw-r--r--webapp/sass/layout/_navigation.scss2
-rw-r--r--webapp/sass/layout/_sidebar-left.scss1
-rw-r--r--webapp/sass/routes/_admin-console.scss17
-rw-r--r--webapp/stores/browser_store.jsx47
-rw-r--r--webapp/stores/error_store.jsx12
-rw-r--r--webapp/stores/notificaiton_store.jsx98
-rw-r--r--webapp/stores/preference_store.jsx113
-rw-r--r--webapp/stores/socket_store.jsx343
-rw-r--r--webapp/stores/user_store.jsx50
-rw-r--r--webapp/stores/user_typing_store.jsx108
-rw-r--r--webapp/utils/async_client.jsx17
-rw-r--r--webapp/utils/constants.jsx32
-rw-r--r--webapp/utils/utils.jsx12
54 files changed, 1108 insertions, 892 deletions
diff --git a/webapp/action_creators/global_actions.jsx b/webapp/action_creators/global_actions.jsx
index 0280d5974..ab38532a6 100644
--- a/webapp/action_creators/global_actions.jsx
+++ b/webapp/action_creators/global_actions.jsx
@@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes;
import * as AsyncClient from 'utils/async_client.jsx';
import * as Client from 'utils/client.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as Websockets from './websocket_actions.jsx';
import * as I18n from 'i18n/i18n.jsx';
import en from 'i18n/en.json';
@@ -97,10 +98,21 @@ export function emitLoadMorePostsFocusedBottomEvent() {
AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE);
}
-export function emitPostRecievedEvent(post) {
+export function emitPostRecievedEvent(post, websocketMessageProps) {
+ if (ChannelStore.getCurrentId() === post.channel_id) {
+ if (window.isActive) {
+ AsyncClient.updateLastViewedAt();
+ } else {
+ AsyncClient.getChannel(post.channel_id);
+ }
+ } else {
+ AsyncClient.getChannel(post.channel_id);
+ }
+
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST,
- post
+ post,
+ websocketMessageProps
});
}
@@ -261,3 +273,21 @@ export function viewLoggedIn() {
// Clear pending posts (shouldn't have pending posts if we are loading)
PostStore.clearPendingPosts();
}
+
+var lastTimeTypingSent = 0;
+export function emitLocalUserTypingEvent(channelId, parentId) {
+ const t = Date.now();
+ if ((t - lastTimeTypingSent) > Constants.UPDATE_TYPING_MS) {
+ Websockets.sendMessage({channel_id: channelId, action: 'typing', props: {parent_id: parentId}, state: {}});
+ lastTimeTypingSent = t;
+ }
+}
+
+export function emitRemoteUserTypingEvent(channelId, userId, postParentId) {
+ AppDispatcher.handleViewAction({
+ type: Constants.ActionTypes.USER_TYPING,
+ channelId,
+ userId,
+ postParentId
+ });
+}
diff --git a/webapp/action_creators/websocket_actions.jsx b/webapp/action_creators/websocket_actions.jsx
new file mode 100644
index 000000000..55a76dbea
--- /dev/null
+++ b/webapp/action_creators/websocket_actions.jsx
@@ -0,0 +1,227 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+import UserStore from 'stores/user_store.jsx';
+import PostStore from 'stores/post_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
+import ErrorStore from 'stores/error_store.jsx';
+
+import * as Utils from 'utils/utils.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
+
+import Constants from 'utils/constants.jsx';
+const SocketEvents = Constants.SocketEvents;
+
+const MAX_WEBSOCKET_FAILS = 7;
+const WEBSOCKET_RETRY_TIME = 3000;
+
+var conn = null;
+var connectFailCount = 0;
+var pastFirstInit = false;
+
+export function initialize() {
+ if (window.WebSocket && !conn) {
+ let protocol = 'ws://';
+ if (window.location.protocol === 'https:') {
+ protocol = 'wss://';
+ }
+
+ const connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket';
+
+ if (connectFailCount === 0) {
+ console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
+ }
+
+ conn = new WebSocket(connUrl);
+
+ conn.onopen = () => {
+ if (connectFailCount > 0) {
+ console.log('websocket re-established connection'); //eslint-disable-line no-console
+ AsyncClient.getChannels();
+ AsyncClient.getPosts(ChannelStore.getCurrentId());
+ }
+
+ if (pastFirstInit) {
+ ErrorStore.clearLastError();
+ ErrorStore.emitChange();
+ }
+
+ pastFirstInit = true;
+ connectFailCount = 0;
+ };
+
+ conn.onclose = () => {
+ conn = null;
+
+ if (connectFailCount === 0) {
+ console.log('websocket closed'); //eslint-disable-line no-console
+ }
+
+ connectFailCount = connectFailCount + 1;
+
+ if (connectFailCount > MAX_WEBSOCKET_FAILS) {
+ ErrorStore.storeLastError(Utils.localizeMessage('channel_loader.socketError', 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'));
+ }
+
+ ErrorStore.setConnectionErrorCount(connectFailCount);
+ ErrorStore.emitChange();
+
+ setTimeout(
+ () => {
+ initialize();
+ },
+ WEBSOCKET_RETRY_TIME
+ );
+ };
+
+ conn.onerror = (evt) => {
+ if (connectFailCount <= 1) {
+ console.log('websocket error'); //eslint-disable-line no-console
+ console.log(evt); //eslint-disable-line no-console
+ }
+ };
+
+ conn.onmessage = (evt) => {
+ const msg = JSON.parse(evt.data);
+ handleMessage(msg);
+ };
+ }
+}
+
+function handleMessage(msg) {
+ // Let the store know we are online. This probably shouldn't be here.
+ UserStore.setStatus(msg.user_id, 'online');
+
+ switch (msg.action) {
+ case SocketEvents.POSTED:
+ case SocketEvents.EPHEMERAL_MESSAGE:
+ 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;
+
+ case SocketEvents.PREFERENCE_CHANGED:
+ handlePreferenceChangedEvent(msg);
+ break;
+
+ case SocketEvents.TYPING:
+ handleUserTypingEvent(msg);
+ break;
+
+ default:
+ }
+}
+
+export function sendMessage(msg) {
+ if (conn && conn.readyState === WebSocket.OPEN) {
+ conn.send(JSON.stringify(msg));
+ } else if (!conn || conn.readyState === WebSocket.Closed) {
+ conn = null;
+ this.initialize();
+ }
+}
+
+export function close() {
+ if (conn && conn.readyState === WebSocket.OPEN) {
+ conn.close();
+ }
+}
+
+function handleNewPostEvent(msg) {
+ const post = JSON.parse(msg.props.post);
+ GlobalActions.emitPostRecievedEvent(post, msg.props);
+}
+
+function handlePostEditEvent(msg) {
+ // Store post
+ const post = JSON.parse(msg.props.post);
+ PostStore.storePost(post);
+ PostStore.emitChange();
+
+ // Update channel state
+ if (ChannelStore.getCurrentId() === msg.channel_id) {
+ if (window.isActive) {
+ AsyncClient.updateLastViewedAt();
+ }
+ }
+}
+
+function handlePostDeleteEvent(msg) {
+ const post = JSON.parse(msg.props.post);
+ GlobalActions.emitPostDeletedEvent(post);
+}
+
+function handleNewUserEvent() {
+ AsyncClient.getProfiles();
+ AsyncClient.getChannelExtraInfo();
+}
+
+function handleUserAddedEvent(msg) {
+ if (ChannelStore.getCurrentId() === msg.channel_id) {
+ AsyncClient.getChannelExtraInfo();
+ }
+
+ 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();
+ }
+}
+
+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);
+ }
+}
+
+function handlePreferenceChangedEvent(msg) {
+ const preference = JSON.parse(msg.props.preference);
+ GlobalActions.emitPreferenceChangedEvent(preference);
+}
+
+function handleUserTypingEvent(msg) {
+ GlobalActions.emitRemoteUserTypingEvent(msg.channel_id, msg.user_id, msg.props.parent_id);
+}
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx
index e2fefc44e..e73d842d0 100644
--- a/webapp/components/about_build_modal.jsx
+++ b/webapp/components/about_build_modal.jsx
@@ -24,7 +24,7 @@ export default class AboutBuildModal extends React.Component {
let title = (
<FormattedMessage
id='about.teamEditiont0'
- defaultMessage='Team Edition T0'
+ defaultMessage='Team Edition'
/>
);
@@ -33,14 +33,14 @@ export default class AboutBuildModal extends React.Component {
title = (
<FormattedMessage
id='about.teamEditiont1'
- defaultMessage='Team Edition T1'
+ defaultMessage='Enterprise Edition'
/>
);
if (license.IsLicensed === 'true') {
title = (
<FormattedMessage
id='about.enterpriseEditione1'
- defaultMessage='Enterprise Edition E1'
+ defaultMessage='Enterprise Edition'
/>
);
licensee = (
diff --git a/webapp/components/access_history_modal.jsx b/webapp/components/access_history_modal.jsx
index 94a10c97f..9c49c3879 100644
--- a/webapp/components/access_history_modal.jsx
+++ b/webapp/components/access_history_modal.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
import {Modal} from 'react-bootstrap';
import LoadingScreen from './loading_screen.jsx';
import AuditTable from './audit_table.jsx';
@@ -36,11 +35,8 @@ class AccessHistoryModal extends React.Component {
}
onShow() {
AsyncClient.getAudits();
-
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
+ if (!Utils.isMobile()) {
+ $('.modal-body').perfectScrollbar();
}
}
onHide() {
diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx
index 9a4ff3ef2..f1dd4a26a 100644
--- a/webapp/components/activity_log_modal.jsx
+++ b/webapp/components/activity_log_modal.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
import UserStore from 'stores/user_store.jsx';
import * as Client from 'utils/client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
@@ -56,11 +55,8 @@ export default class ActivityLogModal extends React.Component {
}
onShow() {
AsyncClient.getSessions();
-
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
+ if (!Utils.isMobile()) {
+ $('.modal-body').perfectScrollbar();
}
}
onHide() {
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
index 5aa0dba7e..ad310d8e0 100644
--- a/webapp/components/admin_console/license_settings.jsx
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -105,36 +105,27 @@ class LicenseSettings extends React.Component {
let licenseType;
let licenseKey;
+ const issued = Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true);
+ const startsAt = Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10));
+ const expiresAt = Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10));
+
if (global.window.mm_license.IsLicensed === 'true') {
- edition = (
- <FormattedMessage
- id='admin.license.enterpriseEdition'
- defaultMessage='Mattermost Enterprise Edition. Designed for enterprise-scale communication.'
- />
- );
+ // Note: DO NOT LOCALISE THESE STRINGS. Legally we can not since the license is in English.
+ edition = 'Mattermost Enterprise Edition. Enterprise features on this server have been unlocked with a license key and a valid subscription.';
licenseType = (
- <FormattedHTMLMessage
- id='admin.license.enterpriseType'
- values={{
- terms: global.window.mm_config.TermsOfServiceLink,
- name: global.window.mm_license.Name,
- company: global.window.mm_license.Company,
- users: global.window.mm_license.Users,
- issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true),
- start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)),
- expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)),
- ldap: global.window.mm_license.LDAP
- }}
- defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p>
- <p>Your subscription details are as follows:</p>
- Name: {name}<br />
- Company or organization name: {company}<br/>
- Number of users: {users}<br/>
- License issued: {issued}<br/>
- Start date of license: {start}<br/>
- Expiry date of license: {expires}<br/>
- LDAP: {ldap}<br/></div>'
- />
+ <div>
+ <p>
+ {'This software is offered under a commercial license.\n\nSee ENTERPRISE-EDITION-LICENSE.txt in your root install directory for details. See NOTICE.txt for information about open source software used in this system.\n\nYour subscription details are as follows:'}
+ </p>
+ {`Name: ${global.window.mm_license.Name}`}<br/>
+ {`Company or organization name: ${global.window.mm_license.Company}`}<br/>
+ {`Number of users: ${global.window.mm_license.Users}`}<br/>
+ {`License issued: ${issued}`}<br/>
+ {`Start date of license: ${startsAt}`}<br/>
+ {`Expiry date of license: ${expiresAt}`}<br/>
+ <br/>
+ {'See also '}<a href='https://about.mattermost.com/enterprise-edition-terms/'>{'Enterprise Edition Terms of Service'}</a>{' and '}<a href='https://about.mattermost.com/privacy/'>{'Privacy Policy.'}</a>
+ </div>
);
licenseKey = (
@@ -162,20 +153,15 @@ class LicenseSettings extends React.Component {
</div>
);
} else {
+ // Note: DO NOT LOCALISE THESE STRINGS. Legally we can not since the license is in English.
edition = (
- <FormattedMessage
- id='admin.license.teamEdition'
- defaultMessage='Mattermost Team Edition. Designed for teams from 5 to 50 users.'
- />
+ <p>
+ {'Mattermost Enterprise Edition. Unlock enterprise features in this software through the purchase of a subscription from '}
+ <a href='https://mattermost.com/'>{'https://mattermost.com/'}</a>
+ </p>
);
- licenseType = (
- <FormattedHTMLMessage
- id='admin.license.teamType'
- defaultMessage='<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p>
- <p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>'
- />
- );
+ licenseType = 'This software is offered under a commercial license.\n\nSee ENTERPRISE-EDITION-LICENSE.txt in your root install directory for details. See NOTICE.txt for information about open source software used in this system.';
let fileName;
if (this.state.fileName) {
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 7cd713942..369fa2dbb 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -80,6 +80,7 @@ export default class ChannelHeader extends React.Component {
SearchStore.addSearchChangeListener(this.onListenerChange);
PreferenceStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
+ $('.sidebar--left .dropdown-menu').perfectScrollbar();
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx
new file mode 100644
index 000000000..e4af9f9ce
--- /dev/null
+++ b/webapp/components/channel_invite_button.jsx
@@ -0,0 +1,79 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import * as Client from 'utils/client.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+export default class ChannelInviteButton extends React.Component {
+ static get propTypes() {
+ return {
+ user: React.PropTypes.object.isRequired,
+ channel: React.PropTypes.object.isRequired,
+ onInviteError: React.PropTypes.func.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+
+ this.state = {
+ addingUser: false
+ };
+ }
+
+ handleClick() {
+ if (this.state.addingUser) {
+ return;
+ }
+
+ this.setState({
+ addingUser: true
+ });
+
+ const data = {
+ user_id: this.props.user.id
+ };
+
+ Client.addChannelMember(
+ this.props.channel.id,
+ data,
+ () => {
+ this.setState({
+ addingUser: false
+ });
+
+ this.props.onInviteError(null);
+ AsyncClient.getChannelExtraInfo();
+ },
+ (err) => {
+ this.setState({
+ addingUser: false
+ });
+
+ this.props.onInviteError(err);
+ }
+ );
+ }
+
+ render() {
+ return (
+ <SpinnerButton
+ onClick={this.handleClick}
+ spinning={this.state.addingUser}
+ >
+ <i className='glyphicon glyphicon-envelope'/>
+ <FormattedMessage
+ id='channel_invite.add'
+ defaultMessage=' Add'
+ />
+ </SpinnerButton>
+ );
+ }
+}
diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx
index dfb0d4187..c7c1906a5 100644
--- a/webapp/components/channel_invite_modal.jsx
+++ b/webapp/components/channel_invite_modal.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import $ from 'jquery';
+import ChannelInviteButton from './channel_invite_button.jsx';
import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
@@ -9,7 +10,6 @@ import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import {FormattedMessage} from 'react-intl';
@@ -23,9 +23,8 @@ export default class ChannelInviteModal extends React.Component {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
- this.handleInvite = this.handleInvite.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
- this.createInviteButton = this.createInviteButton.bind(this);
+ this.handleInviteError = this.handleInviteError.bind(this);
this.state = this.getStateFromStores();
}
@@ -120,36 +119,16 @@ export default class ChannelInviteModal extends React.Component {
this.setState(newState);
}
}
- handleInvite(user) {
- const data = {
- user_id: user.id
- };
-
- Client.addChannelMember(
- this.props.channel.id,
- data,
- () => {
- this.setState({inviteError: null});
- AsyncClient.getChannelExtraInfo();
- },
- (err) => {
- this.setState({inviteError: err.message});
- }
- );
- }
- createInviteButton({user}) {
- return (
- <a
- onClick={this.handleInvite.bind(this, user)}
- className='btn btn-sm btn-primary'
- >
- <i className='glyphicon glyphicon-envelope'/>
- <FormattedMessage
- id='channel_invite.add'
- defaultMessage=' Add'
- />
- </a>
- );
+ handleInviteError(err) {
+ if (err) {
+ this.setState({
+ inviteError: err.message
+ });
+ } else {
+ this.setState({
+ inviteError: null
+ });
+ }
}
render() {
var inviteError = null;
@@ -169,7 +148,11 @@ export default class ChannelInviteModal extends React.Component {
<FilteredUserList
style={{maxHeight}}
users={this.state.nonmembers}
- actions={[this.createInviteButton]}
+ actions={[ChannelInviteButton]}
+ actionProps={{
+ channel: this.props.channel,
+ onInviteError: this.handleInviteError
+ }}
/>
);
}
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 0aeb70c57..177f282d3 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -6,7 +6,6 @@ import ReactDOM from 'react-dom';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Client from 'utils/client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PostDeletedModal from './post_deleted_modal.jsx';
@@ -17,6 +16,7 @@ import MsgTyping from './msg_typing.jsx';
import FileUpload from './file_upload.jsx';
import FilePreview from './file_preview.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -196,11 +196,7 @@ class CreateComment extends React.Component {
}
}
- const t = Date.now();
- if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
- SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}});
- this.lastTime = t;
- }
+ GlobalActions.emitLocalUserTypingEvent(this.props.channelId, this.props.rootId);
}
handleUserInput(messageText) {
let draft = PostStore.getCommentDraft(this.props.rootId);
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index 36bfbf22d..e5e99debd 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -19,7 +19,6 @@ import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import Constants from 'utils/constants.jsx';
@@ -213,11 +212,7 @@ class CreatePost extends React.Component {
}
}
- const t = Date.now();
- if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
- SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}});
- this.lastTime = t;
- }
+ GlobalActions.emitLocalUserTypingEvent(this.state.channelId, '');
}
handleUserInput(messageText) {
this.setState({messageText});
diff --git a/webapp/components/filtered_user_list.jsx b/webapp/components/filtered_user_list.jsx
index 1e4afd2be..bd6c49714 100644
--- a/webapp/components/filtered_user_list.jsx
+++ b/webapp/components/filtered_user_list.jsx
@@ -114,6 +114,7 @@ class FilteredUserList extends React.Component {
<UserList
users={users}
actions={this.props.actions}
+ actionProps={this.props.actionProps}
/>
</div>
</div>
@@ -123,13 +124,15 @@ class FilteredUserList extends React.Component {
FilteredUserList.defaultProps = {
users: [],
- actions: []
+ actions: [],
+ actionProps: {}
};
FilteredUserList.propTypes = {
intl: intlShape.isRequired,
users: React.PropTypes.arrayOf(React.PropTypes.object),
actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object,
style: React.PropTypes.object
};
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index d567183ac..1f8fd6133 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -176,12 +175,6 @@ class InviteMemberModal extends React.Component {
});
}
- componentDidUpdate(prevProps, prevState) {
- if (!prevState.show && this.state.show) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- }
- }
-
addInviteFields() {
var count = this.state.idCount + 1;
var inviteIds = this.state.inviteIds;
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 6d35ff8c2..c6f7b50b1 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -5,12 +5,13 @@ import $ from 'jquery';
import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'action_creators/global_actions.jsx';
import UserStore from 'stores/user_store.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import ErrorBar from 'components/error_bar.jsx';
+import * as Websockets from 'action_creators/websocket_actions.jsx';
import {browserHistory} from 'react-router';
@@ -66,11 +67,6 @@ export default class LoggedIn extends React.Component {
}
}
}
- onSocketChange(msg) {
- if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
- UserStore.setStatus(msg.user_id, 'online');
- }
- }
componentWillMount() {
// Emit view action
GlobalActions.viewLoggedIn();
@@ -78,8 +74,8 @@ export default class LoggedIn extends React.Component {
// Listen for user
UserStore.addChangeListener(this.onUserChanged);
- // Add listner for socker store
- SocketStore.addChangeListener(this.onSocketChange);
+ // Initalize websockets
+ Websockets.initialize();
// Get all statuses regularally. (Soon to be switched to websocket)
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
@@ -89,7 +85,7 @@ export default class LoggedIn extends React.Component {
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
+ if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
return;
}
@@ -99,7 +95,7 @@ export default class LoggedIn extends React.Component {
if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
+ if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
return;
}
@@ -178,7 +174,7 @@ export default class LoggedIn extends React.Component {
$(window).off('focus');
$(window).off('blur');
- SocketStore.removeChangeListener(this.onSocketChange);
+ Websockets.close();
UserStore.removeChangeListener(this.onUserChanged);
$('body').off('click.userpopover');
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx
index 57cac7229..d1446059d 100644
--- a/webapp/components/more_direct_channels.jsx
+++ b/webapp/components/more_direct_channels.jsx
@@ -5,9 +5,9 @@ import {Modal} from 'react-bootstrap';
import FilteredUserList from './filtered_user_list.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import loadingGif from 'images/load.gif';
import {FormattedMessage} from 'react-intl';
+import SpinnerButton from 'components/spinner_button.jsx';
import React from 'react';
@@ -83,26 +83,16 @@ export default class MoreDirectChannels extends React.Component {
}
createJoinDirectChannelButton({user}) {
- if (this.state.loadingDMChannel === user.id) {
- return (
- <img
- className='channel-loading-gif'
- src={loadingGif}
- />
- );
- }
-
return (
- <button
- type='button'
- className='btn btn-primary btn-message'
+ <SpinnerButton
+ spinning={this.state.loadingDMChannel === user.id}
onClick={this.handleShowDirectChannel.bind(this, user)}
>
<FormattedMessage
id='more_direct_channels.message'
defaultMessage='Message'
/>
- </button>
+ </SpinnerButton>
);
}
diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx
index b1781623c..b2d414287 100644
--- a/webapp/components/msg_typing.jsx
+++ b/webapp/components/msg_typing.jsx
@@ -1,21 +1,9 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import SocketStore from 'stores/socket_store.jsx';
-import UserStore from 'stores/user_store.jsx';
+import UserTypingStore from 'stores/user_typing_store.jsx';
-import Constants from 'utils/constants.jsx';
-
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
-
-const SocketEvents = Constants.SocketEvents;
-
-const holders = defineMessages({
- someone: {
- id: 'msg_typing.someone',
- defaultMessage: 'Someone'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
@@ -23,69 +11,40 @@ class MsgTyping extends React.Component {
constructor(props) {
super(props);
- this.onChange = this.onChange.bind(this);
+ this.onTypingChange = this.onTypingChange.bind(this);
this.updateTypingText = this.updateTypingText.bind(this);
this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
- this.typingUsers = {};
this.state = {
text: ''
};
}
- componentDidMount() {
- SocketStore.addChangeListener(this.onChange);
+ componentWillMount() {
+ UserTypingStore.addChangeListener(this.onTypingChange);
+ this.onTypingChange();
+ }
+
+ componentWillUnmount() {
+ UserTypingStore.removeChangeListener(this.onTypingChange);
}
componentWillReceiveProps(nextProps) {
if (this.props.channelId !== nextProps.channelId) {
- for (const u in this.typingUsers) {
- if (!this.typingUsers.hasOwnProperty(u)) {
- continue;
- }
-
- clearTimeout(this.typingUsers[u]);
- }
- this.typingUsers = {};
- this.setState({text: ''});
+ this.updateTypingText(UserTypingStore.getUsersTyping(nextProps.channelId, nextProps.parentId));
}
}
- componentWillUnmount() {
- SocketStore.removeChangeListener(this.onChange);
+ onTypingChange() {
+ this.updateTypingText(UserTypingStore.getUsersTyping(this.props.channelId, this.props.parentId));
}
- onChange(msg) {
- let username = this.props.intl.formatMessage(holders.someone);
- if (msg.action === SocketEvents.TYPING &&
- this.props.channelId === msg.channel_id &&
- this.props.parentId === msg.props.parent_id) {
- if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
-
- if (this.typingUsers[username]) {
- clearTimeout(this.typingUsers[username]);
- }
-
- this.typingUsers[username] = setTimeout(function myTimer(user) {
- delete this.typingUsers[user];
- this.updateTypingText();
- }.bind(this, username), Constants.UPDATE_TYPING_MS);
-
- this.updateTypingText();
- } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
- if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
- clearTimeout(this.typingUsers[username]);
- delete this.typingUsers[username];
- this.updateTypingText();
+ updateTypingText(typingUsers) {
+ if (!typingUsers) {
+ return;
}
- }
- updateTypingText() {
- const users = Object.keys(this.typingUsers);
+ const users = Object.keys(typingUsers);
let text = '';
switch (users.length) {
case 0:
@@ -129,9 +88,8 @@ class MsgTyping extends React.Component {
}
MsgTyping.propTypes = {
- intl: intlShape.isRequired,
channelId: React.PropTypes.string,
parentId: React.PropTypes.string
};
-export default injectIntl(MsgTyping); \ No newline at end of file
+export default MsgTyping;
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index fb3b25957..520f05ed0 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -8,6 +8,7 @@ import MessageWrapper from './message_wrapper.jsx';
import NotifyCounts from './notify_counts.jsx';
import ChannelInfoModal from './channel_info_modal.jsx';
import ChannelInviteModal from './channel_invite_modal.jsx';
+import ChannelMembersModal from './channel_members_modal.jsx';
import ChannelNotificationsModal from './channel_notifications_modal.jsx';
import DeleteChannelModal from './delete_channel_modal.jsx';
import RenameChannelModal from './rename_channel_modal.jsx';
@@ -433,6 +434,7 @@ export default class Navbar extends React.Component {
var editChannelHeaderModal = null;
var editChannelPurposeModal = null;
let renameChannelModal = null;
+ let channelMembersModal = null;
if (channel) {
popoverContent = (
@@ -523,6 +525,14 @@ export default class Navbar extends React.Component {
channel={channel}
/>
);
+
+ channelMembersModal = (
+ <ChannelMembersModal
+ show={this.state.showMembersModal}
+ onModalDismissed={() => this.setState({showMembersModal: false})}
+ channel={channel}
+ />
+ );
}
var collapseButtons = this.createCollapseButtons(currentId);
@@ -556,6 +566,7 @@ export default class Navbar extends React.Component {
{editChannelHeaderModal}
{editChannelPurposeModal}
{renameChannelModal}
+ {channelMembersModal}
</div>
);
}
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index 7d048019c..819c7f590 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
+
import UserStore from 'stores/user_store.jsx';
import {Popover, Overlay} from 'react-bootstrap';
import * as Utils from 'utils/utils.jsx';
@@ -20,6 +22,10 @@ export default class PopoverListMembers extends React.Component {
this.closePopover = this.closePopover.bind(this);
}
+ componentDidUpdate() {
+ $('.member-list__popover .popover-content').perfectScrollbar();
+ }
+
componentWillMount() {
this.setState({showPopover: false});
}
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 29986d415..de99eb37d 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -5,7 +5,6 @@ import ReactDOM from 'react-dom';
import PostStore from 'stores/post_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserProfile from './user_profile.jsx';
-import UserStore from 'stores/user_store.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -97,8 +96,8 @@ class RhsComment extends React.Component {
return '';
}
- var isOwner = UserStore.getCurrentId() === post.user_id;
- var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles);
+ const isOwner = this.props.currentUser.id === post.user_id;
+ const isAdmin = Utils.isAdmin(this.props.currentUser.roles);
var dropdownContents = [];
@@ -193,11 +192,11 @@ class RhsComment extends React.Component {
var post = this.props.post;
var currentUserCss = '';
- if (UserStore.getCurrentId() === post.user_id) {
+ if (this.props.currentUser === post.user_id) {
currentUserCss = 'current--user';
}
- var timestamp = UserStore.getCurrentUser().update_at;
+ var timestamp = this.props.currentUser.update_at;
let loading;
let postClass = '';
@@ -305,7 +304,8 @@ RhsComment.defaultProps = {
RhsComment.propTypes = {
intl: intlShape.isRequired,
post: React.PropTypes.object,
- user: React.PropTypes.object
+ user: React.PropTypes.object.isRequired,
+ currentUser: React.PropTypes.object.isRequired
};
export default injectIntl(RhsComment);
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index a2c7ee7f8..1aa4a555f 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -55,8 +55,8 @@ export default class RhsRootPost extends React.Component {
render() {
const post = this.props.post;
const user = this.props.user;
- var isOwner = user.id === post.user_id;
- var isAdmin = Utils.isAdmin(user.roles);
+ var isOwner = this.props.currentUser.id === post.user_id;
+ var isAdmin = Utils.isAdmin(this.props.currentUser.roles);
var timestamp = UserStore.getProfile(post.user_id).update_at;
var channel = ChannelStore.get(post.channel_id);
@@ -286,5 +286,6 @@ RhsRootPost.defaultProps = {
RhsRootPost.propTypes = {
post: React.PropTypes.object.isRequired,
user: React.PropTypes.object.isRequired,
+ currentUser: React.PropTypes.object.isRequired,
commentCount: React.PropTypes.number
};
diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx
index cc900f8e7..f0324d7ce 100644
--- a/webapp/components/rhs_thread.jsx
+++ b/webapp/components/rhs_thread.jsx
@@ -46,6 +46,9 @@ export default class RhsThread extends React.Component {
window.addEventListener('resize', this.handleResize);
this.mounted = true;
+ if (!Utils.isMobile()) {
+ $('.sidebar--right .post-right__scroll').perfectScrollbar();
+ }
}
componentDidUpdate() {
if ($('.post-right__scroll')[0]) {
@@ -130,7 +133,7 @@ export default class RhsThread extends React.Component {
}
// sort failed posts to bottom, followed by pending, and then regular posts
- postsArray.sort(function postSort(a, b) {
+ postsArray.sort((a, b) => {
if ((a.state === Constants.POST_LOADING || a.state === Constants.POST_FAILED) && (b.state !== Constants.POST_LOADING && b.state !== Constants.POST_FAILED)) {
return 1;
}
@@ -182,24 +185,26 @@ export default class RhsThread extends React.Component {
post={selected}
commentCount={postsArray.length}
user={profile}
+ currentUser={this.props.currentUser}
/>
<div className='post-right-comments-container'>
- {postsArray.map(function mapPosts(comPost) {
- let p;
- if (UserStore.getCurrentId() === comPost.user_id) {
- p = UserStore.getCurrentUser();
- } else {
- p = profiles[comPost.user_id];
- }
- return (
- <Comment
- ref={comPost.id}
- key={comPost.id + 'commentKey'}
- post={comPost}
- user={p}
- />
- );
- })}
+ {postsArray.map((comPost) => {
+ let p;
+ if (UserStore.getCurrentId() === comPost.user_id) {
+ p = UserStore.getCurrentUser();
+ } else {
+ p = profiles[comPost.user_id];
+ }
+ return (
+ <Comment
+ ref={comPost.id}
+ key={comPost.id + 'commentKey'}
+ post={comPost}
+ user={p}
+ currentUser={this.props.currentUser}
+ />
+ );
+ })}
</div>
<div className='post-create__container'>
<CreateComment
@@ -221,5 +226,6 @@ RhsThread.defaultProps = {
RhsThread.propTypes = {
fromSearch: React.PropTypes.string,
- isMentionSearch: React.PropTypes.bool
+ isMentionSearch: React.PropTypes.bool,
+ currentUser: React.PropTypes.object.isRequired
};
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index 7619e41cd..c5baf50ef 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -61,6 +61,9 @@ export default class SearchResults extends React.Component {
UserStore.addChangeListener(this.onUserChange);
this.resize();
window.addEventListener('resize', this.handleResize);
+ if (!Utils.isMobile()) {
+ $('.sidebar--right .search-items-container').perfectScrollbar();
+ }
}
shouldComponentUpdate(nextProps, nextState) {
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index 49ae1bec6..0e1b7dd0e 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -93,12 +93,12 @@ export default class Sidebar extends React.Component {
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const directChannels = [];
- for (const preference of preferences) {
- if (preference.value !== 'true') {
+ for (const [name, value] of preferences) {
+ if (value !== 'true') {
continue;
}
- const teammateId = preference.name;
+ const teammateId = name;
let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId));
@@ -163,6 +163,9 @@ export default class Sidebar extends React.Component {
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
+ if (!Utils.isMobile()) {
+ $('.sidebar--left .nav-pills__container').perfectScrollbar();
+ }
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
@@ -239,11 +242,10 @@ export default class Sidebar extends React.Component {
if (!this.isLeaving.get(channel.id)) {
this.isLeaving.set(channel.id, true);
- const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, channel.teammate_id, 'false');
-
- // bypass AsyncClient since we've already saved the updated preferences
- Client.savePreferences(
- [preference],
+ AsyncClient.savePreference(
+ Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
+ channel.teammate_id,
+ 'false',
() => {
this.isLeaving.set(channel.id, false);
},
diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx
index 1b3286963..a2e3914f3 100644
--- a/webapp/components/sidebar_right.jsx
+++ b/webapp/components/sidebar_right.jsx
@@ -8,6 +8,7 @@ import SearchResults from './search_results.jsx';
import RhsThread from './rhs_thread.jsx';
import SearchStore from 'stores/search_store.jsx';
import PostStore from 'stores/post_store.jsx';
+import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
const SIDEBAR_SCROLL_DELAY = 500;
@@ -22,33 +23,38 @@ export default class SidebarRight extends React.Component {
this.onSelectedChange = this.onSelectedChange.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
+ this.onUserChange = this.onUserChange.bind(this);
this.onShowSearch = this.onShowSearch.bind(this);
this.doStrangeThings = this.doStrangeThings.bind(this);
- this.state = this.getStateFromStores();
- }
- getStateFromStores() {
- return {
- search_visible: SearchStore.getSearchResults() != null,
- post_right_visible: PostStore.getSelectedPost() != null,
- is_mention_search: SearchStore.getIsMentionSearch()
+ this.state = {
+ searchVisible: !!SearchStore.getSearchResults(),
+ isMentionSearch: SearchStore.getIsMentionSearch(),
+ postRightVisible: !!PostStore.getSelectedPost(),
+ fromSearch: false,
+ currentUser: UserStore.getCurrentUser()
};
}
componentDidMount() {
SearchStore.addSearchChangeListener(this.onSearchChange);
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
SearchStore.addShowSearchListener(this.onShowSearch);
+ UserStore.addChangeListener(this.onUserChange);
this.doStrangeThings();
}
componentWillUnmount() {
SearchStore.removeSearchChangeListener(this.onSearchChange);
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
SearchStore.removeShowSearchListener(this.onShowSearch);
+ UserStore.removeChangeListener(this.onUserChange);
+ }
+ shouldComponentUpdate(nextProps, nextState) {
+ return !Utils.areObjectsEqual(nextState, this.state);
}
componentWillUpdate(nextProps, nextState) {
- const isOpen = this.state.search_visible || this.state.post_right_visible;
- const willOpen = nextState.search_visible || nextState.post_right_visible;
+ const isOpen = this.state.searchVisible || this.state.postRightVisible;
+ const willOpen = nextState.searchVisible || nextState.postRightVisible;
if (!isOpen && willOpen) {
setTimeout(() => PostStore.jumpPostsViewSidebarOpen(), SIDEBAR_SCROLL_DELAY);
@@ -66,7 +72,7 @@ export default class SidebarRight extends React.Component {
$('.sidebar--right').addClass('move--left');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');
- if (this.state.search_visible || this.state.post_right_visible) {
+ if (this.state.searchVisible || this.state.postRightVisible) {
if (windowWidth > 960) {
velocity($('.inner-wrap'), {marginRight: sidebarRightWidth}, {duration: 500, easing: 'easeOutSine'});
velocity($('.sidebar--right'), {translateX: 0}, {duration: 500, easing: 'easeOutSine'});
@@ -98,35 +104,40 @@ export default class SidebarRight extends React.Component {
this.doStrangeThings();
}
onSelectedChange(fromSearch) {
- var newState = this.getStateFromStores(fromSearch);
- newState.from_search = fromSearch;
- if (!Utils.areObjectsEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.setState({
+ postRightVisible: !!PostStore.getSelectedPost(),
+ fromSearch
+ });
}
onSearchChange() {
- var newState = this.getStateFromStores();
- if (!Utils.areObjectsEqual(newState, this.state)) {
- this.setState(newState);
- }
+ this.setState({
+ searchVisible: !!SearchStore.getSearchResults(),
+ isMentionSearch: SearchStore.getIsMentionSearch()
+ });
+ }
+ onUserChange() {
+ this.setState({
+ currentUser: UserStore.getCurrentUser()
+ });
}
onShowSearch() {
- if (!this.state.search_visible) {
+ if (!this.state.searchVisible) {
this.setState({
- search_visible: true
+ searchVisible: true
});
}
}
render() {
- var content = '';
+ let content = null;
- if (this.state.search_visible) {
- content = <SearchResults isMentionSearch={this.state.is_mention_search}/>;
- } else if (this.state.post_right_visible) {
+ if (this.state.searchVisible) {
+ content = <SearchResults isMentionSearch={this.state.isMentionSearch}/>;
+ } else if (this.state.postRightVisible) {
content = (
<RhsThread
- fromSearch={this.state.from_search}
- isMentionSearch={this.state.is_mention_search}
+ fromSearch={this.state.fromSearch}
+ isMentionSearch={this.state.isMentionSearch}
+ currentUser={this.state.currentUser}
/>
);
}
diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx
new file mode 100644
index 000000000..fcc9af8cd
--- /dev/null
+++ b/webapp/components/spinner_button.jsx
@@ -0,0 +1,48 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import loadingGif from 'images/load.gif';
+
+export default class SpinnerButton extends React.Component {
+ static get propTypes() {
+ return {
+ children: React.PropTypes.node,
+ spinning: React.PropTypes.bool.isRequired,
+ onClick: React.PropTypes.func
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick(e) {
+ if (this.props.onClick) {
+ this.props.onClick(e);
+ }
+ }
+
+ render() {
+ if (this.props.spinning) {
+ return (
+ <img
+ className='spinner-button__gif'
+ src={loadingGif}
+ />
+ );
+ }
+
+ return (
+ <button
+ onClick={this.handleClick}
+ className='btn btn-sm btn-primary'
+ >
+ {this.props.children}
+ </button>
+ );
+ }
+}
diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx
index 7dbbd680a..c19787993 100644
--- a/webapp/components/team_settings_modal.jsx
+++ b/webapp/components/team_settings_modal.jsx
@@ -5,6 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import SettingsSidebar from './settings_sidebar.jsx';
import TeamSettings from './team_settings.jsx';
+import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
@@ -49,9 +50,16 @@ class TeamSettingsModal extends React.Component {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
+
+ if (!Utils.isMobile()) {
+ $('.settings-modal .settings-content').perfectScrollbar();
+ }
}
updateTab(tab) {
this.setState({activeTab: tab, activeSection: ''});
+ if (!Utils.isMobile()) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
+ }
}
updateSection(section) {
this.setState({activeSection: section});
diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx
index 5db45523e..734842cad 100644
--- a/webapp/components/tutorial/tutorial_intro_screens.jsx
+++ b/webapp/components/tutorial/tutorial_intro_screens.jsx
@@ -36,17 +36,22 @@ export default class TutorialIntroScreens extends React.Component {
Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
- let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0);
- const newValue = (parseInt(preference.value, 10) + 1).toString();
-
- preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
- AsyncClient.savePreferences([preference]);
+ AsyncClient.savePreference(
+ Preferences.TUTORIAL_STEP,
+ UserStore.getCurrentId(),
+ step + 1
+ );
}
skipTutorial(e) {
e.preventDefault();
- const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999');
- AsyncClient.savePreferences([preference]);
+
+ AsyncClient.savePreference(
+ Preferences.TUTORIAL_STEP,
+ UserStore.getCurrentId(),
+ 999
+ );
}
createScreen() {
switch (this.state.currentScreen) {
diff --git a/webapp/components/tutorial/tutorial_tip.jsx b/webapp/components/tutorial/tutorial_tip.jsx
index ab49d4b04..d93fff1b1 100644
--- a/webapp/components/tutorial/tutorial_tip.jsx
+++ b/webapp/components/tutorial/tutorial_tip.jsx
@@ -29,12 +29,13 @@ export default class TutorialTip extends React.Component {
this.setState({show});
if (!show && this.state.currentScreen >= this.props.screens.length - 1) {
- let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'});
+ let step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0);
- const newValue = (parseInt(preference.value, 10) + 1).toString();
-
- preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue);
- AsyncClient.savePreferences([preference]);
+ AsyncClient.savePreference(
+ Preferences.TUTORIAL_STEP,
+ UserStore.getCurrentId(),
+ step + 1
+ );
}
}
handleNext() {
diff --git a/webapp/components/user_list.jsx b/webapp/components/user_list.jsx
index 2140158e6..3652723be 100644
--- a/webapp/components/user_list.jsx
+++ b/webapp/components/user_list.jsx
@@ -18,16 +18,22 @@ export default class UserList extends React.Component {
key={user.id}
user={user}
actions={this.props.actions}
+ actionProps={this.props.actionProps}
/>
);
});
} else {
content = (
- <div key='no-users-found'>
- <FormattedMessage
- id='user_list.notFound'
- defaultMessage='No users found :('
- />
+ <div
+ key='no-users-found'
+ className='no-channel-message'
+ >
+ <p className='primary-message'>
+ <FormattedMessage
+ id='user_list.notFound'
+ defaultMessage='No users found :('
+ />
+ </p>
</div>
);
}
@@ -42,10 +48,12 @@ export default class UserList extends React.Component {
UserList.defaultProps = {
users: [],
- actions: []
+ actions: [],
+ actionProps: {}
};
UserList.propTypes = {
users: React.PropTypes.arrayOf(React.PropTypes.object),
- actions: React.PropTypes.arrayOf(React.PropTypes.func)
+ actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object
};
diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx
index ed3a29a61..f6fd91688 100644
--- a/webapp/components/user_list_row.jsx
+++ b/webapp/components/user_list_row.jsx
@@ -6,7 +6,7 @@ import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
-export default function UserListRow({user, actions}) {
+export default function UserListRow({user, actions, actionProps}) {
const nameFormat = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', '');
let name = user.username;
@@ -21,6 +21,7 @@ export default function UserListRow({user, actions}) {
<Action
key={index.toString()}
user={user}
+ {...actionProps}
/>
);
});
@@ -56,10 +57,12 @@ export default function UserListRow({user, actions}) {
}
UserListRow.defaultProps = {
- actions: []
+ actions: [],
+ actionProps: {}
};
UserListRow.propTypes = {
user: React.PropTypes.object.isRequired,
- actions: React.PropTypes.arrayOf(React.PropTypes.func)
+ actions: React.PropTypes.arrayOf(React.PropTypes.func),
+ actionProps: React.PropTypes.object
};
diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx
index 7c496f57b..4fcdc9a41 100644
--- a/webapp/components/user_settings/user_settings_advanced.jsx
+++ b/webapp/components/user_settings/user_settings_advanced.jsx
@@ -1,11 +1,12 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as Client from 'utils/client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import Constants from 'utils/constants.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import UserStore from 'stores/user_store.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
@@ -68,25 +69,27 @@ class AdvancedSettingsDisplay extends React.Component {
const preReleaseFeaturesKeys = Object.keys(PreReleaseFeatures);
const advancedSettings = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS);
const settings = {
- send_on_ctrl_enter: PreferenceStore.getPreference(
+ send_on_ctrl_enter: PreferenceStore.get(
Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
'send_on_ctrl_enter',
- {value: 'false'}
- ).value
+ 'false'
+ )
};
let enabledFeatures = 0;
- advancedSettings.forEach((setting) => {
- preReleaseFeaturesKeys.forEach((key) => {
+ for (const [name, value] of advancedSettings) {
+ for (const key of preReleaseFeaturesKeys) {
const feature = PreReleaseFeatures[key];
- if (setting.name === Constants.FeatureTogglePrefix + feature.label) {
- settings[setting.name] = setting.value;
- if (setting.value === 'true') {
- enabledFeatures++;
+
+ if (name === Constants.FeatureTogglePrefix + feature.label) {
+ settings[name] = value;
+
+ if (value === 'true') {
+ enabledFeatures += 1;
}
}
- });
- });
+ }
+ }
this.state = {preReleaseFeatures: PreReleaseFeatures, settings, preReleaseFeaturesKeys, enabledFeatures};
}
@@ -124,20 +127,21 @@ class AdvancedSettingsDisplay extends React.Component {
handleSubmit(settings) {
const preferences = [];
+ const userId = UserStore.getCurrentId();
+ // this should be refactored so we can actually be certain about what type everything is
(Array.isArray(settings) ? settings : [settings]).forEach((setting) => {
- preferences.push(
- PreferenceStore.setPreference(
- Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
- setting,
- String(this.state.settings[setting])
- )
- );
+ preferences.push({
+ user_id: userId,
+ category: Constants.Preferences.CATEGORY_ADVANCED_SETTINGS,
+ name: setting,
+ value: this.state.settings[setting]
+ });
});
- Client.savePreferences(preferences,
+ AsyncClient.savePreferences(
+ preferences,
() => {
- PreferenceStore.emitChange();
this.updateSection('');
},
(err) => {
diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx
index 3299588f7..e56156049 100644
--- a/webapp/components/user_settings/user_settings_display.jsx
+++ b/webapp/components/user_settings/user_settings_display.jsx
@@ -6,24 +6,22 @@ import SettingItemMax from '../setting_item_max.jsx';
import ManageLanguages from './manage_languages.jsx';
import ThemeSetting from './user_settings_theme.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as I18n from 'i18n/i18n.jsx';
import Constants from 'utils/constants.jsx';
+const Preferences = Constants.Preferences;
-import {savePreferences} from 'utils/client.jsx';
import {FormattedMessage} from 'react-intl';
function getDisplayStateFromStores() {
- const militaryTime = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', {value: 'false'});
- const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'username'});
- const selectedFont = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', {value: Constants.DEFAULT_FONT});
-
return {
- militaryTime: militaryTime.value,
- nameFormat: nameFormat.value,
- selectedFont: selectedFont.value
+ militaryTime: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', 'false'),
+ nameFormat: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'username'),
+ selectedFont: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT)
};
}
@@ -44,13 +42,29 @@ export default class UserSettingsDisplay extends React.Component {
this.state = getDisplayStateFromStores();
}
handleSubmit() {
- const timePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', this.state.militaryTime);
- const namePreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', this.state.nameFormat);
- const fontPreference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', this.state.selectedFont);
+ const userId = UserStore.getCurrentId();
+
+ const timePreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: 'use_military_time',
+ value: this.state.militaryTime
+ };
+ const namePreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: 'name_format',
+ value: this.state.nameFormat
+ };
+ const fontPreference = {
+ user_id: userId,
+ category: Preferences.CATEGORY_DISPLAY_SETTINGS,
+ name: 'selected_font',
+ value: this.state.selectedFont
+ };
- savePreferences([timePreference, namePreference, fontPreference],
+ AsyncClient.savePreferences([timePreference, namePreference, fontPreference],
() => {
- PreferenceStore.emitChange();
this.updateSection('');
},
(err) => {
diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx
index bd1df6ea5..d1c1f0fe2 100644
--- a/webapp/components/user_settings/user_settings_modal.jsx
+++ b/webapp/components/user_settings/user_settings_modal.jsx
@@ -64,7 +64,6 @@ class UserSettingsModal extends React.Component {
constructor(props) {
super(props);
- this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
this.handleHidden = this.handleHidden.bind(this);
this.handleCollapse = this.handleCollapse.bind(this);
@@ -95,24 +94,13 @@ class UserSettingsModal extends React.Component {
}
componentDidMount() {
- if (this.props.show) {
- this.handleShow();
- }
UserStore.addChangeListener(this.onUserChanged);
}
- componentDidUpdate(prevProps) {
- if (this.props.show && !prevProps.show) {
- this.handleShow();
- }
+ componentDidUpdate() {
UserStore.removeChangeListener(this.onUserChanged);
- }
-
- handleShow() {
- if ($(window).width() > 768) {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
- } else {
- $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 50);
+ if (!Utils.isMobile()) {
+ $('.settings-modal .modal-body').perfectScrollbar();
}
}
@@ -222,6 +210,10 @@ class UserSettingsModal extends React.Component {
active_section: ''
});
}
+
+ if (!Utils.isMobile()) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
+ }
}
updateSection(section, skipConfirm) {
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 9b44f9abd..d9411df07 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1,12 +1,12 @@
{
"about.close": "Close",
"about.date": "Build Date:",
- "about.enterpriseEditione1": "Enterprise Edition E1",
+ "about.enterpriseEditione1": "Enterprise Edition",
"about.hash": "Build Hash:",
"about.licensed": "Licensed by:",
"about.number": "Build Number:",
- "about.teamEditiont0": "Team Edition T0",
- "about.teamEditiont1": "Team Edition T1",
+ "about.teamEditiont0": "Team Edition",
+ "about.teamEditiont1": "Enterprise Edition",
"about.title": "About Mattermost",
"about.version": "Version:",
"access_history.title": "Access History",
@@ -230,15 +230,9 @@
"admin.ldap.usernameAttrTitle": "Username Attribute:",
"admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, <a href=\"http://mattermost.com\" target=\"_blank\">disable all Enterprise Edition features on this server</a>. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.",
"admin.license.chooseFile": "Choose File",
- "admin.license.edition": "Edition: ",
- "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Designed for enterprise-scale communication.",
- "admin.license.enterpriseType": "<div><p>This compiled release of Mattermost platform is provided under a <a href=\"http://mattermost.com\" target=\"_blank\">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href=\"{terms}\" target=\"_blank\">Terms of Service.</a></p><p>Your subscription details are as follows:</p>Name: {name}<br />Company or organization name: {company}<br/>Number of users: {users}<br/>License issued: {issued}<br/>Start date of license: {start}<br/>Expiry date of license: {expires}<br/>LDAP: {ldap}<br/></div>",
- "admin.license.key": "License Key: ",
"admin.license.keyRemove": "Remove Enterprise License and Downgrade Server",
"admin.license.noFile": "No file uploaded",
"admin.license.removing": "Removing License...",
- "admin.license.teamEdition": "Mattermost Team Edition. Designed for teams from 5 to 50 users.",
- "admin.license.teamType": "<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p><p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>",
"admin.license.title": "Edition and License",
"admin.license.type": "License: ",
"admin.license.upload": "Upload",
@@ -416,6 +410,8 @@
"admin.support.emailTitle": "Support email:",
"admin.support.helpDesc": "Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.",
"admin.support.helpTitle": "Help link:",
+ "admin.support.noteDescription": "If linking to an external site, URLs should begin with http:// or https://.",
+ "admin.support.noteTitle": "Note:",
"admin.support.privacyDesc": "Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.",
"admin.support.privacyTitle": "Privacy Policy link:",
"admin.support.problemDesc": "Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.",
@@ -425,8 +421,6 @@
"admin.support.termsDesc": "Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.",
"admin.support.termsTitle": "Terms of Service link:",
"admin.support.title": "Legal and Support Settings",
- "admin.support.noteTitle": "Note:",
- "admin.support.noteDescription": "If linking to an external site, URLs should begin with http:// or https://.",
"admin.system_analytics.activeUsers": "Active Users With Posts",
"admin.system_analytics.title": "the System",
"admin.system_analytics.totalPosts": "Total Posts",
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index 606b4376d..8852dd3c6 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -230,15 +230,9 @@
"admin.ldap.usernameAttrTitle": "Atributo Usuario:",
"admin.licence.keyMigration": "Si estás migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar, <a href=\"http://mattermost.com\" target=\"_blank\">deshabilita todas las características de la Edición Enterprise de este servidor</a>. Esta operación habilitará la opción para remover la licencia y degradar este servidor de la Edición Enterprise a la Edición Team.",
"admin.license.chooseFile": "Escoger Archivo",
- "admin.license.edition": "Edición: ",
- "admin.license.enterpriseEdition": "Mattermost Edición Enterprise. Diseñada para comunicación de escala empresarial.",
- "admin.license.enterpriseType": "<div><p>Esta versión compilada de la plataforma de Mattermost es provista bajo una <a href=\"http://mattermost.com\" target=\"_blank\">licencia comercial</a> de Mattermost, Inc. en función en su nivel de subscripción y bajo los <a href=\"{terms}\" target=\"_blank\">Términos del Servicio.</a></p><p>Los detalles de tu subscripción son los siguientes:</p>Nombre: {name}<br />Nombre compañía u organización: {company}<br/>Cantidad de usuarios: {users}<br/>Licencia emitida: {issued}<br/>Fecha de inicio: {start}<br/>Fecha de expiración: {expires}<br/>LDAP: {ldap}<br/></div>",
- "admin.license.key": "Llave de la Licencia: ",
"admin.license.keyRemove": "Remover la Licencia Enterprise y Degradar el Servidor",
"admin.license.noFile": "No se subió ningún archivo",
"admin.license.removing": "Removiendo Licencia...",
- "admin.license.teamEdition": "Mattermost Edición Team. Diseñado para equipos desde 5 hasta 50 usuarios.",
- "admin.license.teamType": "<span><p>Esta versión compilada de la plataforma de Mattermost es proporcionada bajo la licencia MIT.</p><p>Lea MIT-COMPILED-LICENSE.txt en el directorio raíz de la instalación para más detalles. Lea NOTICES.txt para información sobre software libre utilizado en este sistema.</p></span>",
"admin.license.title": "Edición y Licencia",
"admin.license.type": "Licencia: ",
"admin.license.upload": "Subir",
@@ -416,6 +410,8 @@
"admin.support.emailTitle": "Correo electrónico de Soporte:",
"admin.support.helpDesc": "Enlace con la documentación de ayuda para el equipo desde el menú principal. Normalmente no cambia a menos que tu organización decida crear una documentación personalizada.",
"admin.support.helpTitle": "Enlace de Ayuda:",
+ "admin.support.noteDescription": "Si se enlaza a un sitio externo, las URLs deben comenzar con http:// o https://.",
+ "admin.support.noteTitle": "Nota:",
"admin.support.privacyDesc": "Enlace para las políticas de Privacidad disponible para los usuarios en versión de escritorio y movil. Al dejarlo en blanco esconderá la opción que muestra el aviso.",
"admin.support.privacyTitle": "Enlace de políticas de Privacidad:",
"admin.support.problemDesc": "Enlace con la documentación de ayuda para el equipo desde el menú principal. Como predeterminado esto apunta a un foro de ayuda donde los usuarios pueden buscar, encontrar y solicitar ayuda sobre temas técnicos.",
diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json
index 17ffe1b16..41d3bbc1c 100644
--- a/webapp/i18n/pt.json
+++ b/webapp/i18n/pt.json
@@ -1,12 +1,12 @@
{
"about.close": "Fechar",
"about.date": "Data De Criação:",
- "about.enterpriseEditione1": "Enterprise Edition E1",
+ "about.enterpriseEditione1": "Enterprise Edition",
"about.hash": "Hash de Compilação:",
"about.licensed": "Licenciado pela:",
"about.number": "O Número De Compilação:",
- "about.teamEditiont0": "Team Edition T0",
- "about.teamEditiont1": "Team Edition T1",
+ "about.teamEditiont0": "Team Edition",
+ "about.teamEditiont1": "Enterprise Edition",
"about.title": "Sobre o Mattermost",
"about.version": "Versão:",
"access_history.title": "Histórico de Acesso",
@@ -24,6 +24,39 @@
"activity_log_modal.iphoneNativeApp": "App Nativo para iPhone",
"admin.audits.reload": "Recarregar",
"admin.audits.title": "Atividade de Usuário",
+ "admin.compliance.directoryDescription": "Diretório o qual os relatórios compliance são gravados, Se estiver em branco, será usado ./data/.",
+ "admin.compliance.directoryExample": "Ex \"./data/\"",
+ "admin.compliance.directoryTitle": "Localização do Diretório de Compliance:",
+ "admin.compliance.enableDailyTitle": "Ativar Relatório Diário:",
+ "admin.compliance.enableDesc": "Quando verdadeiro, Mattermost irá gerar um relatório diário de compliance.",
+ "admin.compliance.enableTitle": "Ativar Compliance:",
+ "admin.compliance.false": "falso",
+ "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Nota:</h4><p>Compliance é um recurso empresarial. Sua licença atual não suporta Compliance. Clique <a href=\"http://mattermost.com\" target=\"_blank\">aqui</a> para informações e preços da licença empresarial.</p>",
+ "admin.compliance.save": "Salvar",
+ "admin.compliance.saving": "Salvando Config...",
+ "admin.compliance.title": "Configurações Compliance",
+ "admin.compliance.true": "verdadeiro",
+ "admin.compliance_reports.desc": "Nome da Tarefa:",
+ "admin.compliance_reports.desc_placeholder": "Ex \"Audit 445 for HR\"",
+ "admin.compliance_reports.emails": "Emails:",
+ "admin.compliance_reports.emails_placeholder": "Ex \"bill@example.com, bob@example.com\"",
+ "admin.compliance_reports.from": "De:",
+ "admin.compliance_reports.from_placeholder": "Ex \"2016-03-11\"",
+ "admin.compliance_reports.keywords": "Palavras-chave:",
+ "admin.compliance_reports.keywords_placeholder": "Ex \"diminuir estoque\"",
+ "admin.compliance_reports.reload": "Recarregar",
+ "admin.compliance_reports.run": "Executar",
+ "admin.compliance_reports.title": "Relatórios Compliance",
+ "admin.compliance_reports.to": "Para:",
+ "admin.compliance_reports.to_placeholder": "Ex \"2016-03-15\"",
+ "admin.compliance_table.desc": "Descrição",
+ "admin.compliance_table.download": "Download",
+ "admin.compliance_table.params": "Parâmetros",
+ "admin.compliance_table.records": "Registros",
+ "admin.compliance_table.status": "Status",
+ "admin.compliance_table.timestamp": "Timestamp",
+ "admin.compliance_table.type": "Tipo",
+ "admin.compliance_table.userId": "Solicitado Por",
"admin.email.allowEmailSignInDescription": "Quando verdadeiro, Mattermost permite aos usuários fazer login usando o e-mail e senha.",
"admin.email.allowEmailSignInTitle": "Permitir Login Com E-mail: ",
"admin.email.allowSignupDescription": "Quando verdadeiro, Mattermost permite a criação de equipe e conta de inscrição através de e-mail e senha. Este valor deve ser falso somente quando você deseja limitar a entrada para o single-sign-on service como OAuth ou LDAP.",
@@ -42,6 +75,8 @@
"admin.email.emailSettings": "Configuração do e-mail",
"admin.email.emailSuccess": "Nenhum erro foram relatados durante o envio de um e-mail. Por favor verifique a sua caixa de entrada para se certificar.",
"admin.email.false": "falso",
+ "admin.email.fullPushNotification": "Enviar trecho de mensagem",
+ "admin.email.genericPushNotification": "Enviar descrição genérica com nomes do usuário e canal",
"admin.email.inviteSaltDescription": "32-caracteres salt adicionados a assinatura de convites por e-mail. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.",
"admin.email.inviteSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.inviteSaltTitle": "Salt Convite:",
@@ -56,6 +91,8 @@
"admin.email.passwordSaltDescription": "32-caracteres de salt adicionado para assinar o redefinição de senha de e-mails. Gerada aleatoriamente na instalação. Clique em \"Re-Gerar\" para criar novos salt.",
"admin.email.passwordSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.passwordSaltTitle": "Salt Reset Senha:",
+ "admin.email.pushContentDesc": "Selecionar \"Enviar descrição genérica com nomes de usuário e canal\" fornece envio de notificações com mensagens genéricas, inclui nomes de usuários e canais mas não específica detalhes do texto da mensagem.<br /><br />Selecionar \"Enviar trecho da mensagem\" envia trechos da mensagem que desencadearam as notificações e pode incluir informações confidenciais enviadas na mensagem. Se o seu serviço de Envio de Notificação está fora de um firewall, é ALTAMENTE RECOMENDADO que está opção somente seja usada com o protocolo \"https\" para encriptar a conexão.",
+ "admin.email.pushContentTitle": "Enviar Notificação Contendo:",
"admin.email.pushDesc": "Normalmente definida como verdadeiro na produção. Quando verdadeiro, Mattermost tenta enviar notificações no iOS e Android, através do servidor de notificação.",
"admin.email.pushServerDesc": "Localização do serviço de notificação push Mattermost você pode configurar por trás do firewall usando https://github.com/mattermost/push-proxy. Para testar, você pode usar http://push-test.mattermost.com, que liga à amostra do app Mattermost iOS na Apple AppStore pública. Por favor, não use o serviço de teste para implantações de produção.",
"admin.email.pushServerEx": "Ex: \"http://push-test.mattermost.com\"",
@@ -326,6 +363,7 @@
"admin.service.webhooksTitle": "Ativar Webhooks Entrada: ",
"admin.sidebar.addTeamSidebar": "Adicionar equipe do menu lateral",
"admin.sidebar.audits": "Conformidade e Auditoria",
+ "admin.sidebar.compliance": "Configurações Compliance",
"admin.sidebar.email": "Configuração do e-mail",
"admin.sidebar.file": "Configurações do arquivo",
"admin.sidebar.gitlab": "Configurações GitLab",
@@ -387,6 +425,8 @@
"admin.support.termsDesc": "Link para os Termos de Serviço para os usuários no desktop ou móvel. Deixando este espaço em branco irá esconder a opção de exibir um aviso.",
"admin.support.termsTitle": "Link Termos do Serviço:",
"admin.support.title": "Configurações jurídico e apoio",
+ "admin.support.noteTitle": "Note:",
+ "admin.support.noteDescription": "If linking to an external site, URLs should begin with http:// or https://.",
"admin.system_analytics.activeUsers": "Usuários Ativos com Postagens",
"admin.system_analytics.title": "o Sistema",
"admin.system_analytics.totalPosts": "Total Posts",
@@ -660,13 +700,13 @@
"email_signup.emailError": "Por favor introduza um endereço de e-mail válido",
"email_signup.find": "Encontrar minhas equipes",
"email_verify.almost": "{siteName}: Você está quase pronto",
+ "email_verify.failed": " Falha ao enviar verificação por email.",
"email_verify.notVerifiedBody": "Por favor verifique seu endereço de email. Verifique por um email em sua caixa de entrada.",
- "email_verify.verifyFailed": "Falha ao verificar seu email.",
"email_verify.resend": "Re-enviar Email",
"email_verify.sent": " Verificação de email enviado.",
- "email_verify.failed": " Falha ao enviar verificação por email.",
"email_verify.verified": "{siteName} Email Verificado",
"email_verify.verifiedBody": "<p>Seu email foi verificado! Clique <a href={url}>aqui</a> para login.</p>",
+ "email_verify.verifyFailed": "Falha ao verificar seu email.",
"error_bar.preview_mode": "Modo de visualização: Notificações por E-mail não foram configuradas",
"file_attachment.download": "Download",
"file_info_preview.size": "Tamanho ",
@@ -758,9 +798,9 @@
"login.noAccount": "Não tem uma conta? ",
"login.on": "no {siteName}",
"login.or": "ou",
+ "login.session_expired": " Sua sessão expirou. Por favor faça login novamente.",
"login.signTo": "Login em:",
"login.verified": " Email Verificado",
- "login.session_expired": " Sua sessão expirou. Por favor faça login novamente.",
"login_email.badTeam": "Nome ruim de equipe",
"login_email.email": "E-mail",
"login_email.emailReq": "Um email é necessário",
@@ -1102,6 +1142,7 @@
"user.settings.advance.preReleaseTitle": "Visualizar recursos de pré-lançamento",
"user.settings.advance.sendDesc": "Se habilitado 'Enter' insere uma nova linha e 'Ctrl + Enter' envia a mensagem.",
"user.settings.advance.sendTitle": "Enviar mensagens Ctrl + Enter",
+ "user.settings.advance.slashCmd_autocmp": "Ativar aplicação externa para autocompletar comandos slash",
"user.settings.advance.title": "Configurações Avançadas",
"user.settings.cmds.add": "Adicionar",
"user.settings.cmds.add_desc": "Criar comandos slash para enviar eventos para integrações externas e receber uma resposta. Por exemplo digitando `/patient Joe Smith` poderia trazer de volta os resultados de pesquisa a partir do seu sistema de gestão de registos internos de saúde para o nome “Joe Smith”. Por favor veja <a href=\"http://docs.mattermost.com/developer/slash-commands.html\">Documentação comandos Slash</a> para detalhes e instruções. Ver todos os comandos slash configurados nesta equipe abaixo.",
@@ -1130,6 +1171,7 @@
"user.settings.cmds.request_type_desc": "O tipo de solicitação do comando emitido para a URL requisitada.",
"user.settings.cmds.request_type_get": "GET",
"user.settings.cmds.request_type_post": "POST",
+ "user.settings.cmds.slashCmd_autocmp": "Ativar aplicação externa para autocompletar",
"user.settings.cmds.token": "Token: ",
"user.settings.cmds.trigger": "Comando Palavra Gatilho: ",
"user.settings.cmds.trigger_desc": "Exemplos: /patient, /client, /employee Reserved: /echo, /join, /logout, /me, /shrug",
@@ -1313,4 +1355,4 @@
"web.footer.terms": "Termos",
"web.header.back": "Voltar",
"web.root.singup_info": "Toda comunicação em um só lugar, pesquisável e acessível em qualquer lugar"
-}
+} \ No newline at end of file
diff --git a/webapp/package.json b/webapp/package.json
index 464083bd4..6f50962a4 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -11,6 +11,7 @@
"fastclick": "1.0.6",
"flux": "2.1.1",
"highlight.js": "9.2.0",
+ "intl": "1.1.0",
"jasny-bootstrap": "3.1.3",
"jquery": "2.2.1",
"keymirror": "0.1.1",
@@ -53,8 +54,8 @@
"webpack": "webpack/webpack#master"
},
"scripts": {
- "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .",
- "build": "webpack",
- "run": "webpack --progress --watch"
+ "check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .",
+ "build": "webpack",
+ "run": "webpack --progress --watch"
}
}
diff --git a/webapp/root.jsx b/webapp/root.jsx
index 2ce220f1d..63fbb4422 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import $ from 'jquery';
+require('perfect-scrollbar/jquery')($);
import 'bootstrap/dist/css/bootstrap.css';
import 'jasny-bootstrap/dist/css/jasny-bootstrap.css';
@@ -24,11 +25,11 @@ import Sidebar from 'components/sidebar.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
-import SocketStore from 'stores/socket_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import SignupTeam from 'components/signup_team.jsx';
import * as Client from 'utils/client.jsx';
+import * as Websockets from 'action_creators/websocket_actions.jsx';
import * as GlobalActions from 'action_creators/global_actions.jsx';
import SignupTeamConfirm from 'components/signup_team_confirm.jsx';
import SignupUserComplete from 'components/signup_user_complete.jsx';
@@ -101,29 +102,43 @@ function preRenderSetup(callwhendone) {
// Do Nothing
};
+ // Make sure the websockets close
$(window).on('beforeunload',
() => {
- if (window.SocketStore) {
- SocketStore.close();
- }
+ Websockets.close();
}
);
- addLocaleData(enLocaleData);
- addLocaleData(esLocaleData);
- addLocaleData(ptLocaleData);
+ function afterIntl() {
+ addLocaleData(enLocaleData);
+ addLocaleData(esLocaleData);
+ addLocaleData(ptLocaleData);
- $.when(d1, d2).done(callwhendone);
+ $.when(d1, d2).done(callwhendone);
+ }
+
+ if (global.Intl) {
+ afterIntl();
+ } else {
+ require.ensure([
+ 'intl',
+ 'intl/locale-data/jsonp/en.js',
+ 'intl/locale-data/jsonp/es.js',
+ 'intl/locale-data/jsonp/pt.js'
+ ], (require) => {
+ require('intl');
+ require('intl/locale-data/jsonp/en.js');
+ require('intl/locale-data/jsonp/es.js');
+ require('intl/locale-data/jsonp/pt.js');
+ afterIntl();
+ });
+ }
}
function preLoggedIn(nextState, replace, callback) {
const d1 = Client.getAllPreferences(
(data) => {
- if (!data) {
- return;
- }
-
- PreferenceStore.setPreferences(data);
+ PreferenceStore.setPreferencesFromServer(data);
},
(err) => {
AsyncClient.dispatchError(err, 'getAllPreferences');
@@ -179,6 +194,7 @@ function onLoggedOut(nextState) {
BrowserStore.signalLogout();
BrowserStore.clear();
ErrorStore.clearLastError();
+ PreferenceStore.clear();
},
() => {
browserHistory.push('/' + teamName + '/login');
diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss
index 94378aabe..4e2049857 100644
--- a/webapp/sass/components/_modal.scss
+++ b/webapp/sass/components/_modal.scss
@@ -9,6 +9,7 @@
}
.modal-body {
+ max-height: calc(90vh - 62px);
overflow: auto;
padding: 20px 15px;
diff --git a/webapp/sass/components/_spinner-button.scss b/webapp/sass/components/_spinner-button.scss
new file mode 100644
index 000000000..e683ad4f4
--- /dev/null
+++ b/webapp/sass/components/_spinner-button.scss
@@ -0,0 +1,8 @@
+@charset 'UTF-8';
+
+.spinner-button__gif {
+ height: 15px;
+ margin-top: 2px;
+ width: 15px;
+}
+
diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss
index 9b631d00f..e75d2556b 100644
--- a/webapp/sass/layout/_headers.scss
+++ b/webapp/sass/layout/_headers.scss
@@ -134,6 +134,10 @@
@include alpha-property(background, $black, .1);
}
+ a {
+ text-decoration: none;
+ }
+
.user__name {
color: $white;
}
diff --git a/webapp/sass/layout/_navigation.scss b/webapp/sass/layout/_navigation.scss
index 87e4b4d27..3daf6e56b 100644
--- a/webapp/sass/layout/_navigation.scss
+++ b/webapp/sass/layout/_navigation.scss
@@ -89,7 +89,7 @@
&.info-popover {
@include background-size(100% 100%);
- background: url('../images/info__icon.png');
+ background-image: url('../images/info__icon.png');
cursor: pointer;
height: 19px;
position: relative;
diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss
index 4c65d0a65..3a5e74570 100644
--- a/webapp/sass/layout/_sidebar-left.scss
+++ b/webapp/sass/layout/_sidebar-left.scss
@@ -1,7 +1,6 @@
@charset 'UTF-8';
.sidebar--left {
- background: #fafafa;
border-right: $border-gray;
height: 100%;
left: 0;
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index 0b47e5ab6..7e53713c0 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -10,12 +10,8 @@
width: 100%;
}
- .row {
- margin: 0;
- }
-
h3 {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid alpha-color($black, .1);
font-weight: 600;
margin: 1em 0;
padding-bottom: .5em;
@@ -75,11 +71,18 @@
width: 17px;
}
+ &.divider {
+ @include alpha-property(background, $black, .1);
+ }
+
> a {
&:hover,
- &.active,
&:focus {
- background-color: #eaeaea;
+ @include alpha-property(background, $black, .1);
+ }
+
+ &.active {
+ background-color: transparent;
}
}
diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx
index 66190f6a2..bba146e38 100644
--- a/webapp/stores/browser_store.jsx
+++ b/webapp/stores/browser_store.jsx
@@ -8,6 +8,8 @@ function getPrefix() {
return global.window.mm_current_user_id + '_';
}
+ console.log('BrowserStore tried to operate without user present'); //eslint-disable-line no-console
+
return 'unknown_';
}
@@ -34,46 +36,35 @@ class BrowserStoreClass {
}
checkVersion() {
- var currentVersion = sessionStorage.getItem('storage_version');
+ var currentVersion = this.getGlobalItem('storage_version');
if (currentVersion !== global.window.mm_config.Version) {
- sessionStorage.clear();
+ this.clearAll();
try {
- sessionStorage.setItem('storage_version', global.window.mm_config.Version);
+ this.setGlobalItem('storage_version', global.window.mm_config.Version);
} catch (e) {
// Do nothing
}
}
}
- getItem(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);
}
- setItem(name, value) {
- sessionStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ getItem(name, defaultValue) {
+ return this.getGlobalItem(getPrefix() + name, defaultValue);
}
removeItem(name) {
- sessionStorage.removeItem(getPrefix() + name);
+ this.removeGlobalItem(getPrefix() + name);
}
setGlobalItem(name, value) {
try {
if (this.isLocalStorageSupported()) {
- localStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ localStorage.setItem(name, JSON.stringify(value));
} else {
- sessionStorage.setItem(getPrefix() + name, JSON.stringify(value));
+ sessionStorage.setItem(name, JSON.stringify(value));
}
} catch (err) {
console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console
@@ -87,9 +78,9 @@ class BrowserStoreClass {
var result = null;
try {
if (this.isLocalStorageSupported()) {
- result = JSON.parse(localStorage.getItem(getPrefix() + name));
+ result = JSON.parse(localStorage.getItem(name));
} else {
- result = JSON.parse(sessionStorage.getItem(getPrefix() + name));
+ result = JSON.parse(sessionStorage.getItem(name));
}
} catch (err) {
result = null;
@@ -104,18 +95,18 @@ class BrowserStoreClass {
removeGlobalItem(name) {
if (this.isLocalStorageSupported()) {
- localStorage.removeItem(getPrefix() + name);
+ localStorage.removeItem(name);
} else {
- sessionStorage.removeItem(getPrefix() + name);
+ sessionStorage.removeItem(name);
}
}
getLastServerVersion() {
- return sessionStorage.getItem('last_server_version');
+ return this.getGlobalItem('last_server_version');
}
setLastServerVersion(version) {
- sessionStorage.setItem('last_server_version', version);
+ this.setGlobalItem('last_server_version', version);
}
signalLogout() {
@@ -185,6 +176,7 @@ class BrowserStoreClass {
const logoutId = sessionStorage.getItem('__logout__');
sessionStorage.clear();
+ localStorage.clear();
if (logoutId) {
sessionStorage.setItem('__logout__', logoutId);
@@ -222,4 +214,3 @@ class BrowserStoreClass {
var BrowserStore = new BrowserStoreClass();
export default BrowserStore;
-window.BrowserStore = BrowserStore;
diff --git a/webapp/stores/error_store.jsx b/webapp/stores/error_store.jsx
index 776375a82..7c695a335 100644
--- a/webapp/stores/error_store.jsx
+++ b/webapp/stores/error_store.jsx
@@ -35,15 +35,15 @@ class ErrorStoreClass extends EventEmitter {
}
getLastError() {
- return BrowserStore.getItem('last_error');
+ return BrowserStore.getGlobalItem('last_error');
}
storeLastError(error) {
- BrowserStore.setItem('last_error', error);
+ BrowserStore.setGlobalItem('last_error', error);
}
getConnectionErrorCount() {
- var count = BrowserStore.getItem('last_error_conn');
+ var count = BrowserStore.getGlobalItem('last_error_conn');
if (count == null) {
return 0;
@@ -53,12 +53,12 @@ class ErrorStoreClass extends EventEmitter {
}
setConnectionErrorCount(count) {
- BrowserStore.setItem('last_error_conn', count);
+ BrowserStore.setGlobalItem('last_error_conn', count);
}
clearLastError() {
- BrowserStore.removeItem('last_error');
- BrowserStore.removeItem('last_error_conn');
+ BrowserStore.removeGlobalItem('last_error');
+ BrowserStore.removeGlobalItem('last_error_conn');
}
}
diff --git a/webapp/stores/notificaiton_store.jsx b/webapp/stores/notificaiton_store.jsx
new file mode 100644
index 000000000..70caffeb6
--- /dev/null
+++ b/webapp/stores/notificaiton_store.jsx
@@ -0,0 +1,98 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import EventEmitter from 'events';
+import Constants from 'utils/constants.jsx';
+import UserStore from './user_store.jsx';
+import ChannelStore from './channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class NotificationStoreClass extends EventEmitter {
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ handleRecievedPost(post, msgProps) {
+ // Send desktop notification
+ if ((UserStore.getCurrentId() !== post.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) {
+ let mentions = [];
+ if (msgProps.mentions) {
+ mentions = JSON.parse(msgProps.mentions);
+ }
+
+ const channel = ChannelStore.get(post.channel_id);
+ const user = UserStore.getCurrentUser();
+ const member = ChannelStore.getMember(post.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 !== Constants.DM_CHANNEL) {
+ return;
+ }
+
+ let username = Utils.localizeMessage('channel_loader.someone', 'Someone');
+ if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
+ username = post.props.override_username;
+ } else if (UserStore.hasProfile(post.user_id)) {
+ username = UserStore.getProfile(post.user_id).username;
+ }
+
+ let title = Utils.localizeMessage('channel_loader.posted', '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 + Utils.localizeMessage('channel_loader.uploadedImage', ' uploaded an image'), channel);
+ } else if (msgProps.otherFile) {
+ Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.uploadedFile', ' uploaded a file'), channel);
+ } else {
+ Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.something', ' did something new'), channel);
+ }
+ } else {
+ Utils.notifyMe(title, username + Utils.localizeMessage('channel_loader.wrote', ' wrote: ') + notifyText, channel);
+ }
+ if (!user.notify_props || user.notify_props.desktop_sound === 'true') {
+ Utils.ding();
+ }
+ }
+ }
+}
+
+var NotificationStore = new NotificationStoreClass();
+
+NotificationStore.dispatchToken = AppDispatcher.register((payload) => {
+ const action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_POST:
+ NotificationStore.handleRecievedPost(action.post, action.webspcketMessageProps);
+ NotificationStore.emitChange();
+ break;
+ }
+});
+
+export default NotificationStore;
diff --git a/webapp/stores/preference_store.jsx b/webapp/stores/preference_store.jsx
index df77f0d51..fcfd1c426 100644
--- a/webapp/stores/preference_store.jsx
+++ b/webapp/stores/preference_store.jsx
@@ -4,143 +4,80 @@
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import BrowserStore from './browser_store.jsx';
import EventEmitter from 'events';
-import UserStore from 'stores/user_store.jsx';
const CHANGE_EVENT = 'change';
-function getPreferenceKey(category, name) {
- return `${category}-${name}`;
-}
-
-function getPreferenceKeyForModel(preference) {
- return `${preference.category}-${preference.name}`;
-}
-
class PreferenceStoreClass extends EventEmitter {
constructor() {
super();
- this.getAllPreferences = this.getAllPreferences.bind(this);
- this.get = this.get.bind(this);
- this.getBool = this.getBool.bind(this);
- this.getInt = this.getInt.bind(this);
- this.getPreference = this.getPreference.bind(this);
- this.getCategory = this.getCategory.bind(this);
- this.getPreferencesWhere = this.getPreferencesWhere.bind(this);
- this.setAllPreferences = this.setAllPreferences.bind(this);
- this.setPreference = this.setPreference.bind(this);
-
- this.emitChange = this.emitChange.bind(this);
- this.addChangeListener = this.addChangeListener.bind(this);
- this.removeChangeListener = this.removeChangeListener.bind(this);
-
this.handleEventPayload = this.handleEventPayload.bind(this);
this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
+
+ this.preferences = new Map();
}
- getAllPreferences() {
- return new Map(BrowserStore.getItem('preferences', []));
+ getKey(category, name) {
+ return `${category}--${name}`;
}
get(category, name, defaultValue = '') {
- const preference = this.getAllPreferences().get(getPreferenceKey(category, name));
+ const key = this.getKey(category, name);
- if (!preference) {
+ if (!this.preferences.has(key)) {
return defaultValue;
}
- return preference.value || defaultValue;
+ return this.preferences.get(key);
}
getBool(category, name, defaultValue = false) {
- const preference = this.getAllPreferences().get(getPreferenceKey(category, name));
+ const key = this.getKey(category, name);
- if (!preference) {
+ if (!this.preferences.has(key)) {
return defaultValue;
}
- // prevent a non-false default value from being returned instead of an actual false value
- if (preference.value === 'false') {
- return false;
- }
-
- return (preference.value !== 'false') || defaultValue;
+ return this.preferences.get(key) !== 'false';
}
getInt(category, name, defaultValue = 0) {
- const preference = this.getAllPreferences().get(getPreferenceKey(category, name));
+ const key = this.getKey(category, name);
- if (!preference) {
+ if (!this.preferences.has(key)) {
return defaultValue;
}
- // prevent a non-zero default value from being returned instead of an actual 0 value
- if (preference.value === '0') {
- return 0;
- }
-
- return parseInt(preference.value, 10) || defaultValue;
- }
-
- getPreference(category, name, defaultValue = {}) {
- return this.getAllPreferences().get(getPreferenceKey(category, name)) || defaultValue;
+ return parseInt(this.preferences.get(key), 10);
}
getCategory(category) {
- return this.getPreferencesWhere((preference) => (preference.category === category));
- }
+ const prefix = category + '--';
- getPreferencesWhere(pred) {
- const all = this.getAllPreferences();
- const preferences = [];
+ const preferences = new Map();
- for (const [, preference] of all) {
- if (pred(preference)) {
- preferences.push(preference);
+ for (const [key, value] of this.preferences) {
+ if (key.startsWith(prefix)) {
+ preferences.set(key.substring(prefix.length), value);
}
}
return preferences;
}
- setAllPreferences(preferences) {
- // note that we store the preferences as an array of key-value pairs so that we can deserialize
- // it as a proper Map instead of an object
- BrowserStore.setItem('preferences', [...preferences]);
- }
-
setPreference(category, name, value) {
- const preferences = this.getAllPreferences();
-
- const key = getPreferenceKey(category, name);
- let preference = preferences.get(key);
-
- if (!preference) {
- preference = {
- user_id: UserStore.getCurrentId(),
- category,
- name
- };
- }
- preference.value = value;
-
- preferences.set(key, preference);
-
- this.setAllPreferences(preferences);
-
- return preference;
+ this.preferences.set(this.getKey(category, name), value);
}
- setPreferences(newPreferences) {
- const preferences = this.getAllPreferences();
-
+ setPreferencesFromServer(newPreferences) {
for (const preference of newPreferences) {
- preferences.set(getPreferenceKeyForModel(preference), preference);
+ this.setPreference(preference.category, preference.name, preference.value);
}
+ }
- this.setAllPreferences(preferences);
+ clear() {
+ this.preferences.clear();
}
emitChange() {
@@ -166,7 +103,7 @@ class PreferenceStoreClass extends EventEmitter {
break;
}
case ActionTypes.RECEIVED_PREFERENCES:
- this.setPreferences(action.preferences);
+ this.setPreferencesFromServer(action.preferences);
this.emitChange();
break;
}
diff --git a/webapp/stores/socket_store.jsx b/webapp/stores/socket_store.jsx
deleted file mode 100644
index 5d6302743..000000000
--- a/webapp/stores/socket_store.jsx
+++ /dev/null
@@ -1,343 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import UserStore from './user_store.jsx';
-import PostStore from './post_store.jsx';
-import ChannelStore from './channel_store.jsx';
-import BrowserStore from './browser_store.jsx';
-import ErrorStore from './error_store.jsx';
-import EventEmitter from 'events';
-
-import * as Utils from 'utils/utils.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import * as GlobalActions from 'action_creators/global_actions.jsx';
-
-import Constants from 'utils/constants.jsx';
-const SocketEvents = Constants.SocketEvents;
-
-const CHANGE_EVENT = 'change';
-
-var conn;
-
-class SocketStoreClass extends EventEmitter {
- constructor() {
- super();
-
- this.initialize = this.initialize.bind(this);
- this.emitChange = this.emitChange.bind(this);
- this.addChangeListener = this.addChangeListener.bind(this);
- this.removeChangeListener = this.removeChangeListener.bind(this);
- this.sendMessage = this.sendMessage.bind(this);
- this.close = this.close.bind(this);
-
- this.failCount = 0;
- this.isInitialize = false;
-
- this.translations = this.getDefaultTranslations();
-
- this.initialize();
- }
-
- initialize() {
- if (!UserStore.getCurrentId()) {
- return;
- }
-
- this.setMaxListeners(0);
-
- if (window.WebSocket && !conn) {
- var protocol = 'ws://';
- if (window.location.protocol === 'https:') {
- protocol = 'wss://';
- }
-
- var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket';
-
- if (this.failCount === 0) {
- console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console
- }
-
- conn = new WebSocket(connUrl);
-
- conn.onopen = () => {
- if (this.failCount > 0) {
- console.log('websocket re-established connection'); //eslint-disable-line no-console
- AsyncClient.getChannels();
- AsyncClient.getPosts(ChannelStore.getCurrentId());
- }
-
- if (this.isInitialize) {
- ErrorStore.clearLastError();
- ErrorStore.emitChange();
- }
-
- this.isInitialize = true;
- this.failCount = 0;
- };
-
- conn.onclose = () => {
- conn = null;
-
- if (this.failCount === 0) {
- console.log('websocket closed'); //eslint-disable-line no-console
- }
-
- this.failCount = this.failCount + 1;
-
- if (this.failCount > 7) {
- ErrorStore.storeLastError({message: this.translations.socketError});
- }
-
- ErrorStore.setConnectionErrorCount(this.failCount);
- ErrorStore.emitChange();
-
- setTimeout(
- () => {
- this.initialize();
- },
- 3000
- );
- };
-
- conn.onerror = (evt) => {
- if (this.failCount <= 1) {
- console.log('websocket error'); //eslint-disable-line no-console
- console.log(evt); //eslint-disable-line no-console
- }
- };
-
- conn.onmessage = (evt) => {
- const msg = JSON.parse(evt.data);
- this.handleMessage(msg);
- this.emitChange(msg);
- };
- }
- }
-
- emitChange(msg) {
- this.emit(CHANGE_EVENT, msg);
- }
-
- addChangeListener(callback) {
- this.on(CHANGE_EVENT, callback);
- }
-
- removeChangeListener(callback) {
- this.removeListener(CHANGE_EVENT, callback);
- }
-
- handleMessage(msg) {
- switch (msg.action) {
- case SocketEvents.POSTED:
- case SocketEvents.EPHEMERAL_MESSAGE:
- handleNewPostEvent(msg, this.translations);
- 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;
-
- case SocketEvents.PREFERENCE_CHANGED:
- handlePreferenceChangedEvent(msg);
- break;
-
- default:
- }
- }
-
- sendMessage(msg) {
- if (conn && conn.readyState === WebSocket.OPEN) {
- conn.send(JSON.stringify(msg));
- } else if (!conn || conn.readyState === WebSocket.Closed) {
- conn = null;
- this.initialize();
- }
- }
-
- setTranslations(messages) {
- this.translations = messages;
- }
-
- getDefaultTranslations() {
- return ({
- socketError: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.',
- someone: 'Someone',
- posted: 'Posted',
- uploadedImage: ' uploaded an image',
- uploadedFile: ' uploaded a file',
- something: ' did something new',
- wrote: ' wrote: '
- });
- }
-
- close() {
- if (conn && conn.readyState === WebSocket.OPEN) {
- conn.close();
- }
- }
-}
-
-function handleNewPostEvent(msg, translations) {
- // Store post
- const post = JSON.parse(msg.props.post);
- GlobalActions.emitPostRecievedEvent(post);
-
- // Update channel state
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- if (window.isActive) {
- AsyncClient.updateLastViewedAt();
- } else {
- AsyncClient.getChannel(msg.channel_id);
- }
- } else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
- AsyncClient.getChannel(msg.channel_id);
- }
-
- // Send desktop notification
- if ((UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') && !Utils.isSystemMessage(post)) {
- 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 !== Constants.DM_CHANNEL) {
- return;
- }
-
- let username = translations.someone;
- if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
- username = post.props.override_username;
- } else if (UserStore.hasProfile(msg.user_id)) {
- username = UserStore.getProfile(msg.user_id).username;
- }
-
- let title = translations.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 + translations.uploadedImage, channel);
- } else if (msgProps.otherFile) {
- Utils.notifyMe(title, username + translations.uploadedFile, channel);
- } else {
- Utils.notifyMe(title, username + translations.something, channel);
- }
- } else {
- Utils.notifyMe(title, username + translations.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);
- PostStore.emitChange();
-
- // Update channel state
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- if (window.isActive) {
- AsyncClient.updateLastViewedAt();
- }
- }
-}
-
-function handlePostDeleteEvent(msg) {
- const post = JSON.parse(msg.props.post);
- GlobalActions.emitPostDeletedEvent(post);
-}
-
-function handleNewUserEvent() {
- AsyncClient.getProfiles();
- AsyncClient.getChannelExtraInfo();
-}
-
-function handleUserAddedEvent(msg) {
- if (ChannelStore.getCurrentId() === msg.channel_id) {
- AsyncClient.getChannelExtraInfo();
- }
-
- 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();
- }
-}
-
-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);
- }
-}
-
-function handlePreferenceChangedEvent(msg) {
- const preference = JSON.parse(msg.props.preference);
- GlobalActions.emitPreferenceChangedEvent(preference);
-}
-
-var SocketStore = new SocketStoreClass();
-
-export default SocketStore;
-window.SocketStore = SocketStore;
diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx
index 98cc2f3f1..4213e6e8c 100644
--- a/webapp/stores/user_store.jsx
+++ b/webapp/stores/user_store.jsx
@@ -16,7 +16,10 @@ const CHANGE_EVENT_STATUSES = 'change_statuses';
class UserStoreClass extends EventEmitter {
constructor() {
super();
- this.profileCache = null;
+ this.profiles = {};
+ this.statuses = {};
+ this.sessions = {};
+ this.audits = {};
this.currentUserId = '';
}
@@ -135,11 +138,7 @@ class UserStoreClass extends EventEmitter {
}
getProfiles() {
- if (this.profileCache !== null) {
- return this.profileCache;
- }
-
- return BrowserStore.getItem('profiles', {});
+ return this.profiles;
}
getActiveOnlyProfiles(skipCurrent) {
@@ -171,47 +170,38 @@ class UserStoreClass extends EventEmitter {
}
saveProfile(profile) {
- var ps = this.getProfiles();
- ps[profile.id] = profile;
- this.profileCache = ps;
- BrowserStore.setItem('profiles', ps);
+ this.profiles[profile.id] = profile;
}
saveProfiles(profiles) {
const currentId = this.getCurrentId();
- if (this.profileCache) {
- const currentUser = this.profileCache[currentId];
- if (currentUser) {
- if (currentId in profiles) {
- delete profiles[currentId];
- }
-
- this.profileCache = profiles;
- this.profileCache[currentId] = currentUser;
- } else {
- this.profileCache = profiles;
+ const currentUser = this.profiles[currentId];
+ if (currentUser) {
+ if (currentId in this.profiles) {
+ delete this.profiles[currentId];
}
+
+ this.profiles = profiles;
+ this.profiles[currentId] = currentUser;
} else {
- this.profileCache = profiles;
+ this.profiles = profiles;
}
-
- BrowserStore.setItem('profiles', profiles);
}
setSessions(sessions) {
- BrowserStore.setItem('sessions', sessions);
+ this.sessions = sessions;
}
getSessions() {
- return BrowserStore.getItem('sessions', {loading: true});
+ return this.sessions;
}
setAudits(audits) {
- BrowserStore.setItem('audits', audits);
+ this.audits = audits;
}
getAudits() {
- return BrowserStore.getItem('audits', {loading: true});
+ return this.audits;
}
getCurrentMentionKeys() {
@@ -252,7 +242,7 @@ class UserStoreClass extends EventEmitter {
}
pSetStatuses(statuses) {
- BrowserStore.setItem('statuses', statuses);
+ this.statuses = statuses;
}
setStatus(userId, status) {
@@ -263,7 +253,7 @@ class UserStoreClass extends EventEmitter {
}
getStatuses() {
- return BrowserStore.getItem('statuses', {});
+ return this.statuses;
}
getStatus(id) {
diff --git a/webapp/stores/user_typing_store.jsx b/webapp/stores/user_typing_store.jsx
new file mode 100644
index 000000000..ab0a9af1d
--- /dev/null
+++ b/webapp/stores/user_typing_store.jsx
@@ -0,0 +1,108 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import UserStore from 'stores/user_store.jsx';
+import EventEmitter from 'events';
+import * as Utils from 'utils/utils.jsx';
+
+import Constants from 'utils/constants.jsx';
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+
+class UserTypingStoreClass extends EventEmitter {
+ constructor() {
+ super();
+
+ // All typeing users by channel
+ // this.typingUsers.[channelId+postParentId].user if present then user us typing
+ // Value is timeout to remove user
+ this.typingUsers = {};
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ usernameFromId(userId) {
+ let username = Utils.localizeMessage('msg_typing.someone', 'Someone');
+ if (UserStore.hasProfile(userId)) {
+ username = UserStore.getProfile(userId).username;
+ }
+ return username;
+ }
+
+ userTyping(channelId, userId, postParentId) {
+ const username = this.usernameFromId(userId);
+
+ // Key representing a location where users can type
+ const loc = channelId + postParentId;
+
+ // Create entry
+ if (!this.typingUsers[loc]) {
+ this.typingUsers[loc] = {};
+ }
+
+ // If we already have this user, clear it's timeout to be deleted
+ if (this.typingUsers[loc][username]) {
+ clearTimeout(this.typingUsers[loc][username].timeout);
+ }
+
+ // Set the user and a timeout to remove it
+ this.typingUsers[loc][username] = setTimeout(() => {
+ delete this.typingUsers[loc][username];
+ if (this.typingUsers[loc] === {}) {
+ delete this.typingUsers[loc];
+ }
+ this.emitChange();
+ }, Constants.UPDATE_TYPING_MS);
+ this.emitChange();
+ }
+
+ getUsersTyping(channelId, postParentId) {
+ // Key representing a location where users can type
+ const loc = channelId + postParentId;
+
+ return this.typingUsers[loc];
+ }
+
+ userPosted(userId, channelId, postParentId) {
+ const username = this.usernameFromId(userId);
+ const loc = channelId + postParentId;
+
+ if (this.typingUsers[loc]) {
+ clearTimeout(this.typingUsers[loc][username]);
+ delete this.typingUsers[loc][username];
+ if (this.typingUsers[loc] === {}) {
+ delete this.typingUsers[loc];
+ }
+ this.emitChange();
+ }
+ }
+}
+
+var UserTypingStore = new UserTypingStoreClass();
+
+UserTypingStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_POST:
+ UserTypingStore.userPosted(action.post.user_id, action.post.channel_id, action.post.parent_id);
+ break;
+ case ActionTypes.USER_TYPING:
+ UserTypingStore.userTyping(action.channelId, action.userId, action.postParentId);
+ break;
+ }
+});
+
+export default UserTypingStore;
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index d3f91bb0e..6140fd9e0 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -673,9 +673,9 @@ export function getStatuses() {
const preferences = PreferenceStore.getCategory(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const teammateIds = [];
- for (const preference of preferences) {
- if (preference.value === 'true') {
- teammateIds.push(preference.name);
+ for (const [name, value] of preferences) {
+ if (value === 'true') {
+ teammateIds.push(name);
}
}
@@ -756,6 +756,17 @@ export function getAllPreferences() {
);
}
+export function savePreference(category, name, value, success, error) {
+ const preference = {
+ user_id: UserStore.getCurrentId(),
+ category,
+ name,
+ value
+ };
+
+ savePreferences([preference], success, error);
+}
+
export function savePreferences(preferences, success, error) {
client.savePreferences(
preferences,
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index a4aa7604c..859348c73 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -83,6 +83,8 @@ export default {
SHOW_SEARCH: null,
+ USER_TYPING: null,
+
TOGGLE_IMPORT_THEME_MODAL: null,
TOGGLE_INVITE_MEMBER_MODAL: null,
TOGGLE_DELETE_POST_MODAL: null,
@@ -429,6 +431,21 @@ export default {
uiName: 'Mention Highlight Link'
},
{
+ group: 'linkAndButtonElements',
+ id: 'linkColor',
+ uiName: 'Link Color'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonBg',
+ uiName: 'Button BG'
+ },
+ {
+ group: 'linkAndButtonElements',
+ id: 'buttonColor',
+ uiName: 'Button Text'
+ },
+ {
group: 'centerChannelElements',
id: 'codeTheme',
uiName: 'Code Theme',
@@ -458,21 +475,6 @@ export default {
iconURL: monokaiIcon
}
]
- },
- {
- group: 'linkAndButtonElements',
- id: 'linkColor',
- uiName: 'Link Color'
- },
- {
- group: 'linkAndButtonElements',
- id: 'buttonBg',
- uiName: 'Button BG'
- },
- {
- group: 'linkAndButtonElements',
- id: 'buttonColor',
- uiName: 'Button Text'
}
],
DEFAULT_CODE_THEME: 'github',
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 95a0f99d5..1379455ca 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -314,7 +314,13 @@ export function getTimestamp() {
// extracts links not styled by Markdown
export function extractLinks(text) {
- const links = [];
+ text; // eslint-disable-line no-unused-expressions
+ Autolinker; // eslint-disable-line no-unused-expressions
+
+ // skip this operation because autolinker is having issues
+ return [];
+
+ /*const links = [];
let inText = text;
// strip out code blocks
@@ -348,7 +354,7 @@ export function extractLinks(text) {
}
);
- return links;
+ return links;*/
}
export function escapeRegExp(string) {
@@ -681,7 +687,7 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-list__content, .modal .modal-content, .modal .modal-back', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .suggestion-list__content, .modal .modal-content', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);