Commit 746679ca authored by Joram Wilander's avatar Joram Wilander Committed by Derrick Anderson

Merge 'release-4.6' into 'master' (#589)

* PLT-8496 Fix shift+Up to open reply RHS (#554)

* Fix shift+Up to open reply RHS

* Feedback review

* Remove unsued vars

* Fix FF in mobile view (#559)

* Fix FF in mobile view

* show error in console when changeCss fails

* Fix hover and unmounting issues with post_info (#562)

* Release 4.6 (#567)

* PLT-8507 - Reply icon not inline in firefox

* PLT-8504 - Scroll bar cutting of File thumbnails

* Fix many places where components error when missing props (#571)

* Based on release-4.6 (#566)

This PR redirects a user to the town square channel before unsubscribing them from the current channel.

* RN-8502 Show errors on Join Another Team page (#563)

* fix incorrect channel notification settings when switching teams and reset active section whenever channel notification modal is closed or hidden (#560)

* [PLT-8424] Do not show add-user-to-channel ephemeral message at center (#533)

* do not show add-user-to-channel ephemeral message at center

* send add to channel ephemeral post via client app

* PLT-8510: Feature checks for older webkit support of Performance API. (#557)

* PLT-8510: Feature checks for older webkit support of Performance API.

* PLT-8510: Added global performance object for tests.

* Fixed autoFocus issue with delete modal after edit (#572)

* Fixed autoFocus issue with delete modal after edit

* check style fixes

* conditional check

* update copyTheme string on handleColorChange (#580)

* PLT-8335 fixing issue with iOS classic (#579)

* PLT-8335 fixing issue with iOS classic

* Fixing eslint errors

* PLT-8522 Added clientside rendering for removed from team messages (#576)

* PLT-8522 Added clientside rendering for removed from team messages

* Fixed extra quotation mark

* PLT-8521 - Fixing overlay on sidebar (#581)

* Fix merge
parent 88c17207
......@@ -373,14 +373,14 @@ export async function getChannelMembersForUserIds(channelId, userIds, success, e
}
export async function leaveChannel(channelId, success) {
const townsquare = ChannelStore.getByName('town-square');
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
await ChannelActions.leaveChannel(channelId)(dispatch, getState);
if (ChannelUtils.isFavoriteChannelId(channelId)) {
unmarkFavorite(channelId);
}
const townsquare = ChannelStore.getByName(Constants.DEFAULT_CHANNEL);
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
if (success) {
success();
}
......
......@@ -3,6 +3,15 @@
import UserStore from 'stores/user_store.jsx';
const SUPPORTS_CLEAR_MARKS = isSupported([performance.clearMarks]);
const SUPPORTS_MARK = isSupported([performance.mark]);
const SUPPORTS_MEASURE_METHODS = isSupported([
performance.measure,
performance.getEntries,
performance.getEntriesByName,
performance.clearMeasures
]);
export function trackEvent(category, event, props) {
if (global.window && global.window.analytics) {
const properties = Object.assign({category, type: event, user_actual_id: UserStore.getCurrentId()}, props);
......@@ -30,14 +39,14 @@ export function trackEvent(category, event, props) {
*
*/
export function clearMarks(names) {
if (!global.mm_config.EnableDeveloper === 'true') {
if (!isDevMode() || !SUPPORTS_CLEAR_MARKS) {
return;
}
names.forEach((name) => performance.clearMarks(name));
}
export function mark(name) {
if (!global.mm_config.EnableDeveloper === 'true') {
if (!isDevMode() || !SUPPORTS_MARK) {
return;
}
performance.mark(name);
......@@ -57,7 +66,7 @@ export function mark(name) {
*
*/
export function measure(name1, name2) {
if (!global.mm_config.EnableDeveloper === 'true') {
if (!isDevMode() || !SUPPORTS_MEASURE_METHODS) {
return [-1, ''];
}
......@@ -77,7 +86,37 @@ export function measure(name1, name2) {
return [lastDuration, measurementName];
}
export function trackLoadTime() {
if (!isSupported([performance.timing.loadEventEnd, performance.timing.navigationStart])) {
return;
}
// Must be wrapped in setTimeout because loadEventEnd property is 0
// until onload is complete, also time added because analytics
// code isn't loaded until a subsequent window event has fired.
const tenSeconds = 10000;
setTimeout(() => {
const {loadEventEnd, navigationStart} = window.performance.timing;
const pageLoadTime = loadEventEnd - navigationStart;
trackEvent('performance', 'page_load', {duration: pageLoadTime});
}, tenSeconds);
}
function mostRecentDurationByEntryName(entryName) {
const entriesWithName = performance.getEntriesByName(entryName);
return entriesWithName.map((item) => item.duration)[entriesWithName.length - 1];
}
function isSupported(checks) {
for (let i = 0, len = checks.length; i < len; i++) {
const item = checks[i];
if (typeof item === 'undefined') {
return false;
}
}
return true;
}
function isDevMode() {
return global.mm_config.EnableDeveloper === 'true';
}
\ No newline at end of file
......@@ -24,7 +24,7 @@ import UserStore from 'stores/user_store.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
import {ActionTypes, Constants, ErrorPageTypes} from 'utils/constants.jsx';
import {ActionTypes, Constants, ErrorPageTypes, PostTypes} from 'utils/constants.jsx';
import EventTypes from 'utils/event_types.jsx';
import {sortTeamsByDisplayName} from 'utils/team_utils.jsx';
import * as Utils from 'utils/utils.jsx';
......@@ -330,7 +330,7 @@ export function sendEphemeralPost(message, channelId, parentId) {
user_id: '0',
channel_id: channelId || ChannelStore.getCurrentId(),
message,
type: Constants.PostTypes.EPHEMERAL,
type: PostTypes.EPHEMERAL,
create_at: timestamp,
update_at: timestamp,
root_id: parentId,
......@@ -341,6 +341,27 @@ export function sendEphemeralPost(message, channelId, parentId) {
handleNewPost(post);
}
export function sendAddToChannelEphemeralPost(user, addedUsername, channelId, postRootId = '') {
const timestamp = Utils.getTimestamp();
const post = {
id: Utils.generateId(),
user_id: user.id,
channel_id: channelId || ChannelStore.getCurrentId(),
message: '',
type: PostTypes.EPHEMERAL_ADD_TO_CHANNEL,
create_at: timestamp,
update_at: timestamp,
root_id: postRootId,
parent_id: postRootId,
props: {
username: user.username,
addedUsername
}
};
handleNewPost(post);
}
export function newLocalizationSelected(locale) {
const localeInfo = I18n.getLanguageInfo(locale);
......
......@@ -11,6 +11,7 @@ import {FormattedMessage} from 'react-intl';
import {updateChannelNotifyProps} from 'actions/channel_actions.jsx';
import {NotificationLevels} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import SettingItemMax from 'components/setting_item_max.jsx';
import SettingItemMin from 'components/setting_item_min.jsx';
......@@ -45,6 +46,24 @@ export default class ChannelNotificationsModal extends React.Component {
};
}
componentWillReceiveProps(nextProps) {
if (!Utils.areObjectsEqual(this.props.channelMember.notify_props, nextProps.channelMember.notify_props)) {
this.setState({
notifyLevel: nextProps.channelMember.notify_props.desktop,
unreadLevel: nextProps.channelMember.notify_props.mark_unread,
pushLevel: nextProps.channelMember.notify_props.push || NotificationLevels.DEFAULT
});
}
}
handleOnHide = () => {
this.setState({
activeSection: ''
});
this.props.onHide();
}
updateSection(section) {
if ($('.section-max').length) {
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
......@@ -614,8 +633,8 @@ export default class ChannelNotificationsModal extends React.Component {
<Modal
show={this.props.show}
dialogClassName='settings-modal settings-modal--tabless'
onHide={this.props.onHide}
onExited={this.props.onHide}
onHide={this.handleOnHide}
onExited={this.handleOnHide}
>
<Modal.Header closeButton={true}>
<Modal.Title>
......
......@@ -7,22 +7,30 @@ import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router';
export default class BackButton extends React.PureComponent {
static defaultProps = {
url: '/'
};
static propTypes = {
/**
* URL to return to
*/
url: PropTypes.string
}
url: PropTypes.string,
/**
* An optional click handler that will trigger when the user clicks on the back button
*/
onClick: PropTypes.func
};
static defaultProps = {
url: '/'
};
render() {
return (
<div className='signup-header'>
<Link to={this.props.url}>
<Link
onClick={this.props.onClick}
to={this.props.url}
>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
......
......@@ -8,7 +8,7 @@ import {getCurrentChannel, getCurrentChannelStats} from 'mattermost-redux/select
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {get, getBool} from 'mattermost-redux/selectors/entities/preferences';
import {makeGetMessageInHistoryItem, getPost, getMostRecentPostIdInChannel, makeGetCommentCountForPost, getLatestReplyablePostId, getCurrentUsersLatestPost} from 'mattermost-redux/selectors/entities/posts';
import {addMessageIntoHistory, moveHistoryIndexBack, moveHistoryIndexForward, createPost, addReaction, removeReaction} from 'mattermost-redux/actions/posts';
import {addMessageIntoHistory, moveHistoryIndexBack, moveHistoryIndexForward, createPost, createPostImmediately, addReaction, removeReaction} from 'mattermost-redux/actions/posts';
import {Posts} from 'mattermost-redux/constants';
import {setEditingPost} from 'actions/post_actions.jsx';
......@@ -16,12 +16,13 @@ import {selectPostFromRightHandSideSearchByPostId} from 'actions/views/rhs';
import {makeGetGlobalItem} from 'selectors/storage';
import {setGlobalItem, actionOnGlobalItemsWithPrefix} from 'actions/storage';
import {Preferences, StoragePrefixes} from 'utils/constants.jsx';
import * as UserAgent from 'utils/user_agent';
import CreatePost from './create_post.jsx';
function mapStateToProps() {
return (state, ownProps) => {
const currentChannel = getCurrentChannel(state);
const currentChannel = getCurrentChannel(state) || {};
const getDraft = makeGetGlobalItem(StoragePrefixes.DRAFT + currentChannel.id, {
message: '',
uploadsInProgress: [],
......@@ -52,12 +53,17 @@ function mapStateToProps() {
}
function mapDispatchToProps(dispatch) {
var createPostTemp = createPost;
if (UserAgent.isIosClassic()) {
createPostTemp = createPostImmediately;
}
return {
actions: bindActionCreators({
addMessageIntoHistory,
moveHistoryIndexBack,
moveHistoryIndexForward,
createPost,
createPost: createPostTemp,
addReaction,
removeReaction,
setDraft: setGlobalItem,
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import React from 'react';
import {Modal} from 'react-bootstrap';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
import {deletePost} from 'actions/post_actions.jsx';
......@@ -42,8 +39,10 @@ export default class DeletePostModal extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (this.state.show && !prevState.show) {
setTimeout(() => {
$(ReactDOM.findDOMNode(this.refs.deletePostBtn)).focus();
}, 0);
if (this.deletePostBtn) {
this.deletePostBtn.focus();
}
}, 200);
}
}
......@@ -149,8 +148,11 @@ export default class DeletePostModal extends React.Component {
/>
</button>
<button
ref='deletePostBtn'
ref={(deletePostBtn) => {
this.deletePostBtn = deletePostBtn;
}}
type='button'
autoFocus={true}
className='btn btn-danger'
onClick={this.handleDelete}
>
......
......@@ -10,7 +10,7 @@ import {getSiteURL} from 'utils/url.jsx';
import GetPostLinkModal from './get_post_link_modal';
function mapStateToProps(state, ownProps) {
const currentTeam = getCurrentTeam(state);
const currentTeam = getCurrentTeam(state) || {};
const currentTeamUrl = `${getSiteURL()}/${currentTeam.name}`;
return {
...ownProps,
......
......@@ -26,6 +26,10 @@ export default class GetTeamInviteLinkModal extends React.PureComponent {
config: PropTypes.object.isRequired
}
static defaultProps = {
currentTeam: {}
}
constructor(props) {
super(props);
this.state = {
......
......@@ -199,13 +199,15 @@ export default class NeedsTeam extends React.Component {
);
}
const teamType = this.state.team ? this.state.team.type : '';
return (
<div className='channel-view'>
<AnnouncementBar/>
<WebrtcNotification/>
<div className='container-fluid'>
<SidebarRight/>
<SidebarRightMenu teamType={this.state.team.type}/>
<SidebarRightMenu teamType={teamType}/>
<WebrtcSidebar/>
{content}
......
......@@ -35,18 +35,6 @@ function renderJoinChannelMessage(post, options) {
);
}
function renderJoinTeamMessage(post, options) {
const username = renderUsername(post.props.username, options);
return (
<FormattedMessage
id='api.team.join_team.post_and_forget'
defaultMessage='{username} joined the team.'
values={{username}}
/>
);
}
function renderLeaveChannelMessage(post, options) {
const username = renderUsername(post.props.username, options);
......@@ -75,6 +63,44 @@ function renderAddToChannelMessage(post, options) {
);
}
function renderRemoveFromChannelMessage(post, options) {
const removedUsername = renderUsername(post.props.removedUsername, options);
return (
<FormattedMessage
id='api.channel.remove_member.removed'
defaultMessage='{removedUsername} was removed from the channel'
values={{
removedUsername
}}
/>
);
}
function renderJoinTeamMessage(post, options) {
const username = renderUsername(post.props.username, options);
return (
<FormattedMessage
id='api.team.join_team.post_and_forget'
defaultMessage='{username} joined the team.'
values={{username}}
/>
);
}
function renderLeaveTeamMessage(post, options) {
const username = renderUsername(post.props.username, options);
return (
<FormattedMessage
id='api.team.leave.left'
defaultMessage='{username} left the team.'
values={{username}}
/>
);
}
function renderAddToTeamMessage(post, options) {
const username = renderUsername(post.props.username, options);
const addedUsername = renderUsername(post.props.addedUsername, options);
......@@ -91,13 +117,13 @@ function renderAddToTeamMessage(post, options) {
);
}
function renderRemoveFromChannelMessage(post, options) {
function renderRemoveFromTeamMessage(post, options) {
const removedUsername = renderUsername(post.props.removedUsername, options);
return (
<FormattedMessage
id='api.channel.remove_member.removed'
defaultMessage='{removedUsername} was removed from the channel'
id='api.team.remove_user_from_team.removed'
defaultMessage='{removedUsername} was removed from the team.'
values={{
removedUsername
}}
......@@ -250,11 +276,13 @@ function renderChannelDeletedMessage(post, options) {
const systemMessageRenderers = {
[PostTypes.JOIN_CHANNEL]: renderJoinChannelMessage,
[PostTypes.JOIN_TEAM]: renderJoinTeamMessage,
[PostTypes.LEAVE_CHANNEL]: renderLeaveChannelMessage,
[PostTypes.ADD_TO_CHANNEL]: renderAddToChannelMessage,
[PostTypes.ADD_TO_TEAM]: renderAddToTeamMessage,
[PostTypes.REMOVE_FROM_CHANNEL]: renderRemoveFromChannelMessage,
[PostTypes.JOIN_TEAM]: renderJoinTeamMessage,
[PostTypes.LEAVE_TEAM]: renderLeaveTeamMessage,
[PostTypes.ADD_TO_TEAM]: renderAddToTeamMessage,
[PostTypes.REMOVE_FROM_TEAM]: renderRemoveFromTeamMessage,
[PostTypes.HEADER_CHANGE]: renderHeaderChangeMessage,
[PostTypes.DISPLAYNAME_CHANGE]: renderDisplayNameChangeMessage,
[PostTypes.PURPOSE_CHANGE]: renderPurposeChangeMessage,
......@@ -287,6 +315,8 @@ export function renderSystemMessage(post, options) {
return null;
} else if (systemMessageRenderers[post.type]) {
return systemMessageRenderers[post.type](post, options);
} else if (post.type === PostTypes.EPHEMERAL_ADD_TO_CHANNEL) {
return renderAddToChannelMessage(post, options);
}
return null;
......
......@@ -9,6 +9,7 @@ import {removePost} from 'mattermost-redux/actions/posts';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getChannel, getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import PostAddChannelMember from './post_add_channel_member.jsx';
......@@ -18,7 +19,8 @@ function mapStateToProps(state, ownProps) {
return {
...ownProps,
team: getCurrentTeam(state),
channel: getChannel(state, currentChannelId)
channel: getChannel(state, currentChannelId),
currentUser: getCurrentUser(state)
};
}
......
......@@ -8,6 +8,8 @@ import {browserHistory} from 'react-router';
import store from 'stores/redux_store.jsx';
import {sendAddToChannelEphemeralPost} from 'actions/global_actions.jsx';
import {Constants} from 'utils/constants.jsx';
import AtMention from 'components/at_mention';
......@@ -17,6 +19,11 @@ const getState = store.getState;
export default class PostAddChannelMember extends React.PureComponent {
static propTypes = {
/*
* Current user
*/
currentUser: PropTypes.object.isRequired,
/*
* Current team
*/
......@@ -62,12 +69,13 @@ export default class PostAddChannelMember extends React.PureComponent {
}
handleAddChannelMember = () => {
const {team, channel, postId, userIds} = this.props;
const {currentUser, team, channel, postId, userIds, usernames} = this.props;
const post = this.props.actions.getPost(getState(), postId) || {};
if (post.channel_id === channel.id) {
userIds.forEach((userId) => {
this.props.actions.addChannelMember(channel.id, userId, post.root_id);
userIds.forEach((userId, index) => {
this.props.actions.addChannelMember(channel.id, userId);
sendAddToChannelEphemeralPost(currentUser, usernames[index], channel.id, post.root_id);
});
this.props.actions.removePost(post);
......
......@@ -7,7 +7,7 @@ import ReactDOM from 'react-dom';
import {FormattedDate, FormattedMessage} from 'react-intl';
import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
import Constants from 'utils/constants.jsx';
import Constants, {PostTypes} from 'utils/constants.jsx';
import DelayedAction from 'utils/delayed_action.jsx';
import EventTypes from 'utils/event_types.jsx';
import GlobalEventEmitter from 'utils/global_event_emitter.jsx';
......@@ -426,7 +426,10 @@ export default class PostList extends React.PureComponent {
for (let i = posts.length - 1; i >= 0; i--) {
const post = posts[i];
if (post == null) {
if (
post == null ||
post.type === PostTypes.EPHEMERAL_ADD_TO_CHANNEL
) {
continue;
}
......
......@@ -4,14 +4,11 @@
import PropTypes from 'prop-types';
import React from 'react';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import {browserHistory, Link} from 'react-router';
import {addUserToTeamFromInvite} from 'actions/team_actions.jsx';
import {Link} from 'react-router';
import TeamInfoIcon from 'components/svg/team_info_icon';
import * as Utils from 'utils/utils.jsx';
import {Constants} from 'utils/constants.jsx';
export default class SelectTeamItem extends React.PureComponent {
static propTypes = {
......@@ -21,11 +18,6 @@ export default class SelectTeamItem extends React.PureComponent {
};
handleTeamClick = () => {
addUserToTeamFromInvite('', '', this.props.team.invite_id,
() => {
browserHistory.push(`/${this.props.team.name}/channels/${Constants.DEFAULT_CHANNEL}`);
}
);
this.props.onTeamClick(this.props.team);
}
......
......@@ -4,9 +4,11 @@
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router';
import {browserHistory, Link} from 'react-router';
import * as GlobalActions from 'actions/global_actions.jsx';
import {addUserToTeamFromInvite} from 'actions/team_actions.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
......@@ -30,12 +32,10 @@ export default class SelectTeam extends React.Component {
constructor(props) {
super(props);
this.onTeamChange = this.onTeamChange.bind(this);
this.handleTeamClick = this.handleTeamClick.bind(this);
this.teamContentsCompare = this.teamContentsCompare.bind(this);
const state = this.getStateFromStores(false);
state.loadingTeamId = '';
state.error = null;
this.state = state;
}
......@@ -48,9 +48,9 @@ export default class SelectTeam extends React.Component {
TeamStore.removeChangeListener(this.onTeamChange);
}
onTeamChange() {
onTeamChange = () => {
this.setState(this.getStateFromStores(true));
}
};
getStateFromStores(loaded) {
return {
......@@ -61,88 +61,115 @@ export default class SelectTeam extends React.Component {
};
}
handleTeamClick(team) {
handleTeamClick = (team) => {
this.setState({loadingTeamId: team.id});
}
addUserToTeamFromInvite('', '', team.invite_id,
() => {
browserHistory.push(`/${team.name}/channels/town-square`);
},
(error) => {
this.setState({
error,
loadingTeamId: ''
});
}
);
};
clearError = (e) => {
e.preventDefault();
this.setState({
error: null
});
};
teamContentsCompare(teamItemA, teamItemB) {
return teamItemA.props.team.display_name.localeCompare(teamItemB.props.team.display_name);
}
handleLoggedOutEvent = () => {
GlobalActions.emitUserLoggedOutEvent();
}
render() {
let openTeamContents = [];
const isAlreadyMember = new Map();
const isSystemAdmin = Utils.isSystemAdmin(UserStore.getCurrentUser().roles);
for (const teamMember of this.state.teamMembers) {
const teamId = teamMember.team_id;
isAlreadyMember[teamId] = true;
}
let openContent;
for (const id in this.state.teamListings) {
if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
const openTeam = this.state.teamListings[id];
openTeamContents.push(
<SelectTeamItem
key={'team_' + openTeam.name}
team={openTeam}
onTeamClick={this.handleTeamClick}
loading={this.state.loadingTeamId === openTeam.id}
/>
);
if (!this.state.loaded || this.state.loadingTeamId) {
openContent = <LoadingScreen/>;
} else if (this.state.error) {
openContent = (
<div className='signup__content'>
<div className={'form-group has-error'}>
<label className='control-label'>{this.state.error.message}</label>
</div>
</div>
);
} else {
let openTeamContents = [];
const isAlreadyMember = new Map();
for (const teamMember of this.state.teamMembers) {
const teamId = teamMember.team_id;
isAlreadyMember[teamId] = true;
}
}
if (openTeamContents.length === 0 && (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin)) {
openTeamContents = (
<div className='signup-team-dir-err'>
<div>
<FormattedMessage
id='signup_team.no_open_teams_canCreate'
defaultMessage='No teams are available to join. Please create a new team or ask your administrator for an invite.'
for (const id in this.state.teamListings) {
if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
const openTeam = this.state.teamListings[id];
openTeamContents.push(
<SelectTeamItem
key={'team_' + openTeam.name}
team={openTeam}
onTeamClick={this.handleTeamClick}
loading={this.state.loadingTeamId === openTeam.id}
/>
);
}
}
if (openTeamContents.length === 0 && (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin)) {
openTeamContents = (
<div className='signup-team-dir-err'>
<div>
<FormattedMessage
id='signup_team.no_open_teams_canCreate'
defaultMessage='No teams are available to join. Please create a new team or ask your administrator for an invite.'
/>
</div>
</div>