Commit 80f85dcc authored by Harrison Healey's avatar Harrison Healey
Browse files

Merge branch 'master' into post-metadata

parents b63b9748 0d2442e3
......@@ -1984,6 +1984,50 @@ SOFTWARE.
---
## rebound-js
This product contains 'rebound-js' by Facebook.
Rebound is a simple library that models Spring dynamics for the purpose of driving physical animations.
* HOMEPAGE:
* https://github.com/facebook/rebound-js
* LICENSE: BSD
BSD License
For the rebound-js software
Copyright (c) 2014, Facebook, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## redux
This product contains 'redux' by Redux.
......
......@@ -5,33 +5,26 @@ import {TeamTypes} from 'mattermost-redux/action_types';
import {viewChannel, getChannelStats} from 'mattermost-redux/actions/channels';
import * as TeamActions from 'mattermost-redux/actions/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getUser} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {browserHistory} from 'utils/browser_history';
import store from 'stores/redux_store.jsx';
const dispatch = store.dispatch;
const getState = store.getState;
export async function removeUserFromTeam(teamId, userId, success, error) {
const {data, error: err} = await dispatch(TeamActions.removeUserFromTeam(teamId, userId));
dispatch(getUser(userId));
dispatch(TeamActions.getTeamStats(teamId));
dispatch(getChannelStats(getCurrentChannelId(getState())));
if (data && success) {
success();
} else if (err && error) {
error({id: err.server_error_id, ...err});
}
export function removeUserFromTeam(teamId, userId) {
return async (dispatch, getState) => {
const response = await dispatch(TeamActions.removeUserFromTeam(teamId, userId));
dispatch(getUser(userId));
dispatch(TeamActions.getTeamStats(teamId));
dispatch(getChannelStats(getCurrentChannelId(getState())));
return response;
};
}
export function addUserToTeamFromInvite(token, inviteId, success, error) {
Client4.addToTeamFromInvite(token, inviteId).then(
async (member) => {
const {data: team} = await TeamActions.getTeam(member.team_id)(dispatch, getState);
export function addUserToTeamFromInvite(token, inviteId) {
return async (dispatch) => {
const {data: member, error} = await dispatch(TeamActions.addUserToTeamFromInvite(token, inviteId));
if (member) {
const {data} = await dispatch(TeamActions.getTeam(member.team_id));
dispatch({
type: TeamTypes.RECEIVED_MY_TEAM_MEMBER,
data: {
......@@ -42,66 +35,29 @@ export function addUserToTeamFromInvite(token, inviteId, success, error) {
},
});
if (success) {
success(team);
}
return {data};
}
).catch(
(err) => {
if (error) {
error(err);
}
}
);
return {error};
};
}
export function addUsersToTeam(teamId, userIds) {
return async (doDispatch, doGetState) => {
const {data, error} = await doDispatch(TeamActions.addUsersToTeam(teamId, userIds));
return async (dispatch, getState) => {
const {data, error} = await dispatch(TeamActions.addUsersToTeam(teamId, userIds));
if (error) {
return {error};
}
doDispatch(getChannelStats(doGetState().entities.channels.currentChannelId));
dispatch(getChannelStats(getCurrentChannelId(getState())));
return {data};
};
}
export function getInviteInfo(inviteId, success, error) {
Client4.getTeamInviteInfo(inviteId).then(
(inviteData) => {
if (success) {
success(inviteData);
}
}
).catch(
(err) => {
if (error) {
error(err);
}
}
);
}
export async function inviteMembers(data, success, error) {
if (!data.invites) {
success();
}
const emails = [];
data.invites.forEach((i) => {
emails.push(i.email);
});
const {data: result, error: err} = await dispatch(TeamActions.sendEmailInvitesToTeam(getCurrentTeamId(getState()), emails));
if (result && success) {
success();
} else if (result == null && error) {
error({id: err.server_error_id, ...err});
}
}
export function switchTeams(url) {
dispatch(viewChannel(getCurrentChannelId(getState())));
browserHistory.push(url);
export function switchTeam(url) {
return (dispatch, getState) => {
dispatch(viewChannel(getCurrentChannelId(getState())));
browserHistory.push(url);
};
}
......@@ -373,7 +373,62 @@ function handleChannelMemberUpdatedEvent(msg) {
dispatch({type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: channelMember});
}
function handleNewPostEvent(msg) {
export function debouncePostEvent(func, wait) {
let timeout;
let queue = [];
let count = 0;
// Called when timeout triggered
const triggered = () => {
timeout = null;
if (queue.length > 0) {
const posts = {};
for (const queuedMsg of queue) {
const post = JSON.parse(queuedMsg.data.post);
if (!posts[post.channel_id]) {
posts[post.channel_id] = {};
}
posts[post.channel_id][post.id] = post;
}
for (const channelId in posts) {
if (!posts.hasOwnProperty(channelId)) {
continue;
}
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {posts: posts[channelId]},
channelId,
});
getProfilesAndStatusesForPosts(posts[channelId], dispatch, getState);
}
}
queue = [];
count = 0;
};
return function fx(msg) {
if (timeout && count > 2) {
// If the timeout is going this is the second or further event so queue them up.
if (queue.push(msg) > 200) {
// Don't run us out of memory, give up if the queue gets insane
queue = [];
console.log('channel broken because of too many incoming messages'); //eslint-disable-line no-console
}
clearTimeout(timeout);
timeout = setTimeout(triggered, wait);
} else {
// Apply immediately for events up until count reaches limit
count += 1;
func(msg);
clearTimeout(timeout);
timeout = setTimeout(triggered, wait);
}
};
}
const handleNewPostEvent = debouncePostEvent(handleNewPostEventWrapped, 100);
function handleNewPostEventWrapped(msg) {
const post = JSON.parse(msg.data.post);
dispatch(handleNewPost(post, msg));
......
......@@ -320,7 +320,7 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
},
Object {
"order": 8,
"text": "Română (Beta)",
"text": "Română",
"value": "ro",
},
Object {
......@@ -429,7 +429,7 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
},
Object {
"order": 8,
"text": "Română (Beta)",
"text": "Română",
"value": "ro",
},
Object {
......
......@@ -169,9 +169,9 @@ exports[`ManageTeamsModal should save data in state from api calls 1`] = `
className="manage-teams__team-actions"
>
<ManageTeamsDropdown
handleRemoveUserFromTeam={[Function]}
onError={[Function]}
onMemberChange={[Function]}
onMemberRemove={[Function]}
team={
Object {
"delete_at": 0,
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RemoveFromTeamButton should match snapshot init 1`] = `
<button
className="btn btn-danger"
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Remove from Team"
id="team_members_dropdown.leave_team"
values={Object {}}
/>
</button>
`;
......@@ -6,6 +6,7 @@ import {bindActionCreators} from 'redux';
import {updateTeamMemberSchemeRoles, getTeamMembersForUser, getTeamsForUser} from 'mattermost-redux/actions/teams';
import {removeUserFromTeam} from 'actions/team_actions.jsx';
import {getCurrentLocale} from 'selectors/i18n';
import ManageTeamsModal from './manage_teams_modal';
......@@ -22,6 +23,7 @@ function mapDispatchToProps(dispatch) {
getTeamMembersForUser,
getTeamsForUser,
updateTeamMemberSchemeRoles,
removeUserFromTeam,
}, dispatch),
};
}
......
......@@ -6,7 +6,6 @@ import React from 'react';
import {Dropdown, MenuItem} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {removeUserFromTeam} from 'actions/team_actions.jsx';
import * as Utils from 'utils/utils.jsx';
export default class ManageTeamsDropdown extends React.Component {
......@@ -15,8 +14,8 @@ export default class ManageTeamsDropdown extends React.Component {
teamMember: PropTypes.object.isRequired,
onError: PropTypes.func.isRequired,
onMemberChange: PropTypes.func.isRequired,
onMemberRemove: PropTypes.func.isRequired,
updateTeamMemberSchemeRoles: PropTypes.func.isRequired,
handleRemoveUserFromTeam: PropTypes.func.isRequired,
};
constructor(props) {
......@@ -24,10 +23,7 @@ export default class ManageTeamsDropdown extends React.Component {
this.toggleDropdown = this.toggleDropdown.bind(this);
this.removeFromTeam = this.removeFromTeam.bind(this);
this.handleMemberChange = this.handleMemberChange.bind(this);
this.handleMemberRemove = this.handleMemberRemove.bind(this);
this.state = {
show: false,
......@@ -58,23 +54,14 @@ export default class ManageTeamsDropdown extends React.Component {
}
};
removeFromTeam() {
removeUserFromTeam(
this.props.teamMember.team_id,
this.props.user.id,
this.handleMemberRemove,
this.props.onError
);
removeFromTeam = () => {
this.props.handleRemoveUserFromTeam(this.props.teamMember.team_id);
}
handleMemberChange() {
this.props.onMemberChange(this.props.teamMember.team_id);
}
handleMemberRemove() {
this.props.onMemberRemove(this.props.teamMember.team_id);
}
render() {
const isTeamAdmin = Utils.isAdmin(this.props.teamMember.roles) || this.props.teamMember.scheme_admin;
......
......@@ -24,6 +24,7 @@ export default class ManageTeamsModal extends React.Component {
getTeamMembersForUser: PropTypes.func.isRequired,
getTeamsForUser: PropTypes.func.isRequired,
updateTeamMemberSchemeRoles: PropTypes.func.isRequired,
removeUserFromTeam: PropTypes.func.isRequired,
}).isRequired,
};
......@@ -89,6 +90,17 @@ export default class ManageTeamsModal extends React.Component {
});
}
handleRemoveUserFromTeam = async (teamId) => {
const {actions, user} = this.props;
const {data, error} = await actions.removeUserFromTeam(teamId, user.id);
if (data) {
this.handleMemberRemove(teamId);
} else if (error) {
this.handleError(error.message);
}
}
renderContents = () => {
const {user} = this.props;
const {teams, teamMembers} = this.state;
......@@ -118,10 +130,8 @@ export default class ManageTeamsModal extends React.Component {
if (isSystemAdmin) {
action = (
<RemoveFromTeamButton
user={user}
team={team}
onError={this.handleError}
onMemberRemove={this.handleMemberRemove}
teamId={team.id}
handleRemoveUserFromTeam={this.handleRemoveUserFromTeam}
/>
);
} else {
......@@ -132,8 +142,8 @@ export default class ManageTeamsModal extends React.Component {
teamMember={teamMember}
onError={this.handleError}
onMemberChange={this.getTeamMembers}
onMemberRemove={this.handleMemberRemove}
updateTeamMemberSchemeRoles={this.props.actions.updateTeamMemberSchemeRoles}
handleRemoveUserFromTeam={this.handleRemoveUserFromTeam}
/>
);
}
......
......@@ -23,6 +23,7 @@ describe('ManageTeamsModal', () => {
getTeamMembersForUser: jest.fn().mockReturnValue(Promise.resolve({data: []})),
getTeamsForUser: jest.fn().mockReturnValue(Promise.resolve({data: []})),
updateTeamMemberSchemeRoles: jest.fn(),
removeUserFromTeam: jest.fn(),
},
};
......
......@@ -5,36 +5,15 @@ import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {removeUserFromTeam} from 'actions/team_actions.jsx';
export default class RemoveFromTeamButton extends React.PureComponent {
static propTypes = {
onError: PropTypes.func.isRequired,
onMemberRemove: PropTypes.func.isRequired,
team: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
teamId: PropTypes.string.isRequired,
handleRemoveUserFromTeam: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleMemberRemove = this.handleMemberRemove.bind(this);
}
handleClick(e) {
handleClick = (e) => {
e.preventDefault();
removeUserFromTeam(
this.props.team.id,
this.props.user.id,
this.handleMemberRemove,
this.props.onError
);
}
handleMemberRemove() {
this.props.onMemberRemove(this.props.team.id);
this.props.handleRemoveUserFromTeam(this.props.teamId);
}
render() {
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import RemoveFromTeamButton from 'components/admin_console/manage_teams_modal/remove_from_team_button.jsx';
describe('RemoveFromTeamButton', () => {
const baseProps = {
teamId: '1234',
handleRemoveUserFromTeam: jest.fn(),
};
test('should match snapshot init', () => {
const wrapper = shallow(
<RemoveFromTeamButton {...baseProps}/>
);
expect(wrapper).toMatchSnapshot();
});
test('should call handleRemoveUserFromTeam on button click', () => {
const wrapper = shallow(
<RemoveFromTeamButton {...baseProps}/>
);
wrapper.find('button').prop('onClick')({preventDefault: jest.fn()});
expect(baseProps.handleRemoveUserFromTeam).toHaveBeenCalledTimes(1);
});
});
......@@ -122,9 +122,9 @@ export default class PushSettings extends AdminSettings {
const pushNotificationServerTypes = [];
pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_OFF, text: Utils.localizeMessage('admin.email.pushOff', 'Do not send push notifications')});
if (this.props.license.IsLicensed === 'true' && this.props.license.MHPNS === 'true') {
pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MHPNS, text: Utils.localizeMessage('admin.email.mhpns', 'Use HPNS connection with uptime SLA to iOS and Android apps')});
pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MHPNS, text: Utils.localizeMessage('admin.email.mhpns', 'Use HPNS connection with uptime SLA to send notifications to iOS and Android apps')});
}
pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MTPNS, text: Utils.localizeMessage('admin.email.mtpns', 'Use TPNS connection to iOS and Android apps')});
pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_MTPNS, text: Utils.localizeMessage('admin.email.mtpns', 'Use TPNS connection to send notifications to iOS and Android apps')});
pushNotificationServerTypes.push({value: PUSH_NOTIFICATIONS_CUSTOM, text: Utils.localizeMessage('admin.email.selfPush', 'Manually enter Push Notification Service location')});
let sendHelpText = null;
......
......@@ -28,6 +28,7 @@ describe('components/admin_console/system_users/list', () => {
updateTeamMemberSchemeRoles: jest.fn(),
getTeamMembersForUser: jest.fn(),
getTeamsForUser: jest.fn(),
removeUserFromTeam: jest.fn(),
},
};
......
......@@ -6,7 +6,6 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {Constants} from 'utils/constants.jsx';
import {localizeMessage} from 'utils/utils.jsx';
export default class BackstageNavbar extends React.Component {
......@@ -26,7 +25,7 @@ export default class BackstageNavbar extends React.Component {
<div className='backstage-navbar'>
<Link
className='backstage-navbar__back'
to={`/${this.props.team.name}/channels/${Constants.DEFAULT_CHANNEL}`}
to={`/${this.props.team.name}`}
>
<i
className='fa fa-angle-left'
......
......@@ -10,7 +10,7 @@ import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
import * as GlobalActions from 'actions/global_actions.jsx';
import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants.jsx';
import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox} from 'utils/post_utils.jsx';
import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox, isErrorInvalidSlashCommand} from 'utils/post_utils.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as Utils from 'utils/utils.jsx';
......@@ -27,6 +27,7 @@ import Textbox from 'components/textbox.jsx';
import TutorialTip from 'components/tutorial/tutorial_tip';
import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx';
import MessageSubmitError from 'components/message_submit_error';
const KeyCodes = Constants.KeyCodes;
......@@ -311,9 +312,18 @@ export default class CreatePost extends React.Component {
return;
}
let message = this.state.message;
let ignoreSlash = false;
const serverError = this.state.serverError;
if (serverError && isErrorInvalidSlashCommand(serverError) && serverError.submittedMessage === message) {
message = serverError.submittedMessage;
ignoreSlash = true;
}
const post = {};
post.file_ids = [];
post.message = this.state.message;
post.message = message;
if (post.message.trim().length === 0 && this.props.draft.fileInfos.length === 0) {
return;
......@@ -332,7 +342,7 @@ export default class CreatePost extends React.Component {
this.setState({submitting: true, serverError: null});
const isReaction = Utils.REACTION_PATTERN.exec(post.message);
if (post.message.indexOf('/') === 0) {
if (post.message.indexOf('/') === 0 && !ignoreSlash) {
this.setState({message: '', postError: null, enableSendButton: false});
const args = {};
args.channel_id = channelId;
......@@ -345,7 +355,10 @@ export default class CreatePost extends React.Component {
this.sendMessage(post);
} else {
this.setState({
serverError: error.message,
serverError: {
...error,
submittedMessage: post.message,
},
message: post.message,
});
}
......@@ -540,9 +553,16 @@ export default class CreatePost extends React.Component {
const message = e.target.value;
const channelId = this.props.currentChannel.id;
const enableSendButton = this.handleEnableSendButton(message, this.props.draft.fileInfos);
let serverError = this.state.serverError;
if (isErrorInvalidSlashCommand(serverError)) {
serverError = null;
}