...
 
Commits (66)
This diff is collapsed.
......@@ -11,7 +11,7 @@ import {Client4} from 'mattermost-redux/client';
import store from 'stores/redux_store.jsx';
import * as Utils from 'utils/utils.jsx';
export function uploadFile(file, name, channelId, clientId, successCallback, errorCallback, progress) {
export function uploadFile(file, name, channelId, rootId, clientId, successCallback, errorCallback) {
const {dispatch, getState} = store;
function handleResponse(err, res) {
......@@ -31,14 +31,14 @@ export function uploadFile(file, name, channelId, clientId, successCallback, err
type: FileTypes.UPLOAD_FILES_FAILURE,
clientIds: [clientId],
channelId,
rootId: null,
rootId,
error: err,
};
dispatch(batchActions([failure, getLogErrorAction(err)]), getState);
if (errorCallback) {
errorCallback(e);
errorCallback(e, clientId, channelId, rootId);
}
} else if (res) {
const data = res.body.file_infos.map((fileInfo, index) => {
......@@ -53,7 +53,7 @@ export function uploadFile(file, name, channelId, clientId, successCallback, err
type: FileTypes.RECEIVED_UPLOAD_FILES,
data,
channelId,
rootId: null,
rootId,
},
{
type: FileTypes.UPLOAD_FILES_SUCCESS,
......@@ -61,7 +61,7 @@ export function uploadFile(file, name, channelId, clientId, successCallback, err
]), getState);
if (successCallback) {
successCallback(res.body);
successCallback(res.body, channelId, rootId);
}
}
}
......@@ -75,7 +75,6 @@ export function uploadFile(file, name, channelId, clientId, successCallback, err
field('channel_id', channelId).
field('client_ids', clientId).
accept('application/json').
on('progress', (e) => progress(e, name, clientId)).
end(handleResponse);
}
......
......@@ -446,11 +446,12 @@ export function emitRemoteUserTypingEvent(channelId, userId, postParentId) {
});
}
export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = true) {
// If the logout was intentional (as it should be if emitUserLoggedOutEvent is called),
// discard knowledge about having previously been logged in. This bit is otherwise used to
// detect session expirations on the login page.
LocalStorageStore.setWasLoggedIn(false);
export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = true, userAction = true) {
// If the logout was intentional, discard knowledge about having previously been logged in.
// This bit is otherwise used to detect session expirations on the login page.
if (userAction) {
LocalStorageStore.setWasLoggedIn(false);
}
dispatch(logout()).then(() => {
if (shouldSignalLogout) {
......
......@@ -289,6 +289,11 @@ export function doPostAction(postId, actionId) {
export function setEditingPost(postId = '', commentCount = 0, refocusId = '', title = '', isRHS = false) {
return async (doDispatch, doGetState) => {
const state = doGetState();
const post = Selectors.getPost(state, postId);
if (!post || post.pending_post_id === postId) {
return {data: false};
}
let canEditNow = true;
......@@ -299,8 +304,6 @@ export function setEditingPost(postId = '', commentCount = 0, refocusId = '', ti
if (config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) {
canEditNow = false;
} else if (config.AllowEditPost === Constants.ALLOW_EDIT_POST_TIME_LIMIT) {
const post = Selectors.getPost(state, postId);
if ((post.create_at + (config.PostEditTimeLimit * 1000)) < Date.now()) {
canEditNow = false;
}
......
......@@ -48,7 +48,7 @@ export function removeGlobalItem(name) {
};
}
export function clear(options) {
export function clear(options = {exclude: []}) {
return (dispatch, getState) => {
dispatch({
type: StorageTypes.CLEAR,
......
......@@ -556,8 +556,8 @@ export async function webLogin(loginId, password, token, success, error) {
}
}
export async function updateServiceTermsStatus(serviceTermsId, accepted, success, error) {
const {data, error: err} = await UserActions.updateServiceTermsStatus(serviceTermsId, accepted)(dispatch, getState);
export async function updateTermsOfServiceStatus(termsOfServiceId, accepted, success, error) {
const {data, error: err} = await UserActions.updateTermsOfServiceStatus(termsOfServiceId, accepted)(dispatch, getState);
if (data && success) {
success(data);
} else if (err && error) {
......@@ -565,8 +565,8 @@ export async function updateServiceTermsStatus(serviceTermsId, accepted, success
}
}
export async function getServiceTerms(success, error) {
const {data, error: err} = await UserActions.getServiceTerms()(dispatch, getState);
export async function getTermsOfService(success, error) {
const {data, error: err} = await UserActions.getTermsOfService()(dispatch, getState);
if (data && success) {
success(data);
} else if (err && error) {
......
......@@ -9,6 +9,7 @@ import * as PostActions from 'mattermost-redux/actions/posts';
import {Client4} from 'mattermost-redux/client';
import {getCurrentUserId, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
......@@ -66,13 +67,15 @@ export function updateSearchTerms(terms) {
export function performSearch(terms, isMentionSearch) {
return (dispatch, getState) => {
const teamId = getCurrentTeamId(getState());
const config = getConfig(getState());
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
// timezone offset in seconds
const userId = getCurrentUserId(getState());
const userTimezone = getUserTimezone(getState(), userId);
const userCurrentTimezone = getUserCurrentTimezone(userTimezone);
const timezoneOffset = (userCurrentTimezone.length > 0 ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60;
return dispatch(searchPostsWithParams(teamId, {terms, is_or_search: isMentionSearch, time_zone_offset: timezoneOffset}, true));
return dispatch(searchPostsWithParams(teamId, {terms, is_or_search: isMentionSearch, include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset}, true));
};
}
......
......@@ -258,6 +258,14 @@ export default class AdminConsole extends React.Component {
schema: AdminDefinition.settings.authentication.gitlab.schema,
}}
/>
<SCRoute
path={`${props.match.url}/phabricator`}
component={SchemaAdminSettings}
extraProps={{
...extraProps,
schema: AdminDefinition.settings.authentication.phabricator.schema,
}}
/>
<SCRoute
path={`${props.match.url}/oauth`}
component={SchemaAdminSettings}
......
......@@ -72,6 +72,8 @@ export default class AdminSidebar extends React.Component {
let oauthSettings = null;
let ldapSettings = null;
let samlSettings = null;
let gitlabSettings = null;
let phabricatorSettings = null;
let clusterSettings = null;
let metricsSettings = null;
let complianceSettings = null;
......@@ -219,7 +221,7 @@ export default class AdminSidebar extends React.Component {
/>
);
} else {
oauthSettings = (
gitlabSettings = (
<AdminSidebarSection
name='gitlab'
title={
......@@ -230,6 +232,18 @@ export default class AdminSidebar extends React.Component {
}
/>
);
phabricatorSettings = (
<AdminSidebarSection
name='phabricator'
title={
<FormattedMessage
id='admin.sidebar.phabricator'
defaultMessage='Phabricator'
/>
}
/>
);
}
if (this.props.license.IsLicensed === 'true') {
......@@ -530,6 +544,8 @@ export default class AdminSidebar extends React.Component {
}
/>
{oauthSettings}
{gitlabSettings}
{phabricatorSettings}
{ldapSettings}
{samlSettings}
{mfaSettings}
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
import * as Utils from 'utils/utils.jsx';
import AdminSettings from './admin_settings.jsx';
import BooleanSetting from './boolean_setting.jsx';
import SettingsGroup from './settings_group.jsx';
import TextSetting from './text_setting.jsx';
export default class PhabricatorSettings extends AdminSettings {
constructor(props) {
super(props);
this.getConfigFromState = this.getConfigFromState.bind(this);
this.renderSettings = this.renderSettings.bind(this);
this.updatePhabricatorUrl = this.updatePhabricatorUrl.bind(this);
}
getConfigFromState(config) {
config.PhabricatorSettings.Enable = this.state.enable;
config.PhabricatorSettings.Id = this.state.id;
config.PhabricatorSettings.Secret = this.state.secret;
config.PhabricatorSettings.UserApiEndpoint = this.state.userApiEndpoint;
config.PhabricatorSettings.AuthEndpoint = this.state.authEndpoint;
config.PhabricatorSettings.TokenEndpoint = this.state.tokenEndpoint;
return config;
}
getStateFromConfig(config) {
return {
enable: config.PhabricatorSettings.Enable,
id: config.PhabricatorSettings.Id,
secret: config.PhabricatorSettings.Secret,
phabricatorUrl: config.PhabricatorSettings.UserApiEndpoint.replace('/api/user.whoami', ''),
userApiEndpoint: config.PhabricatorSettings.UserApiEndpoint,
authEndpoint: config.PhabricatorSettings.AuthEndpoint,
tokenEndpoint: config.PhabricatorSettings.TokenEndpoint,
};
}
updatePhabricatorUrl(id, value) {
let trimmedValue = value;
if (value.endsWith('/')) {
trimmedValue = value.slice(0, -1);
}
this.setState({
saveNeeded: true,
phabricatorUrl: value,
userApiEndpoint: trimmedValue + '/api/user.whoami',
authEndpoint: trimmedValue + '/oauthserver/auth/',
tokenEndpoint: trimmedValue + '/oauthserver/token/',
});
}
isPhabricatorURLSetByEnv = () => {
// Assume that if one of these has been set using an environment variable,
// all of them have been set that way
return this.isSetByEnv('PhabricatorSettings.AuthEndpoint') ||
this.isSetByEnv('PhabricatorSettings.TokenEndpoint') ||
this.isSetByEnv('PhabricatorSettings.UserApiEndpoint');
};
renderTitle() {
return (
<FormattedMessage
id='admin.authentication.phabricator'
defaultMessage='Phabricator'
/>
);
}
renderSettings() {
return (
<SettingsGroup>
<BooleanSetting
id='enable'
label={
<FormattedMessage
id='admin.phabricator.enableTitle'
defaultMessage='Enable authentication with Phabricator: '
/>
}
helpText={
<div>
<FormattedMessage
id='admin.phabricator.enableDescription'
defaultMessage='When true, Mattermost allows team creation and account signup using Phabricator OAuth.'
/>
<br/>
<FormattedHTMLMessage
id='admin.phabricator.EnableHtmlDesc'
defaultMessage='<ol><li>Log in to your Phabricator account and go to Applications -> OAuth Server -> Create OAuth Server.</li><li>Enter Redirect URIs "<your-mattermost-url>/login/phabricator/complete" (example: http://localhost:8065/login/phabricator/complete) and "<your-mattermost-url>/signup/phabricator/complete". </li><li>Then use "Show Application Secret" and "Client PHID" fields from Phabricator to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>'
/>
</div>
}
value={this.state.enable}
onChange={this.handleChange}
setByEnv={this.isSetByEnv('PhabricatorSettings.Enable')}
/>
<TextSetting
id='id'
label={
<FormattedMessage
id='admin.phabricator.clientIdTitle'
defaultMessage='Application ID:'
/>
}
placeholder={Utils.localizeMessage('admin.phabricator.clientIdExample', 'E.g.: "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
helpText={
<FormattedMessage
id='admin.phabricator.clientIdDescription'
defaultMessage='Obtain this value via the instructions above for logging into Phabricator'
/>
}
value={this.state.id}
onChange={this.handleChange}
disabled={!this.state.enable}
setByEnv={this.isSetByEnv('PhabricatorSettings.Id')}
/>
<TextSetting
id='secret'
label={
<FormattedMessage
id='admin.phabricator.clientSecretTitle'
defaultMessage='Application Secret Key:'
/>
}
placeholder={Utils.localizeMessage('admin.phabricator.clientSecretExample', 'E.g.: "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"')}
helpText={
<FormattedMessage
id='admin.phabricator.clientSecretDescription'
defaultMessage='Obtain this value via the instructions above for logging into Phabricator.'
/>
}
value={this.state.secret}
onChange={this.handleChange}
disabled={!this.state.enable}
setByEnv={this.isSetByEnv('PhabricatorSettings.Secret')}
/>
<TextSetting
id='phabricatorUrl'
label={
<FormattedMessage
id='admin.phabricator.siteUrl'
defaultMessage='Phabricator Site URL:'
/>
}
placeholder={Utils.localizeMessage('admin.phabricator.siteUrlExample', 'E.g.: https://')}
helpText={
<FormattedMessage
id='admin.phabricator.siteUrlDescription'
defaultMessage='Enter the URL of your Phabricator instance, e.g. https://example.com:3000. If your Phabricator instance is not set up with SSL, start the URL with http:// instead of https://.'
/>
}
value={this.state.phabricatorUrl}
onChange={this.updatePhabricatorUrl}
disabled={!this.state.enable}
setByEnv={this.isPhabricatorURLSetByEnv()}
/>
<TextSetting
id='userApiEndpoint'
label={
<FormattedMessage
id='admin.phabricator.userTitle'
defaultMessage='User API Endpoint:'
/>
}
placeholder={''}
value={this.state.userApiEndpoint}
disabled={true}
setByEnv={false}
/>
<TextSetting
id='authEndpoint'
label={
<FormattedMessage
id='admin.phabricator.authTitle'
defaultMessage='Auth Endpoint:'
/>
}
placeholder={''}
value={this.state.authEndpoint}
disabled={true}
setByEnv={false}
/>
<TextSetting
id='tokenEndpoint'
label={
<FormattedMessage
id='admin.phabricator.tokenTitle'
defaultMessage='Token Endpoint:'
/>
}
placeholder={''}
value={this.state.tokenEndpoint}
disabled={true}
setByEnv={false}
/>
</SettingsGroup>
);
}
}
......@@ -196,24 +196,9 @@ export default class AnnouncementBar extends React.PureComponent {
return {message: null, color: null, colorText: null, textColor: null, type: null, allowDismissal: true};
}
isValidState(s) {
if (!s) {
return false;
}
if (!s.message) {
return false;
}
if (s.message === AnnouncementBarMessages.LICENSE_EXPIRING && !this.state.totalUsers) {
return false;
}
return true;
}
componentDidMount() {
if (this.props.isLoggedIn && !this.state.allowDismissal) {
const isFixed = this.shouldRender(this.props, this.state) && !this.state.allowDismissal;
if (isFixed) {
document.body.classList.add('announcement-bar--fixed');
}
......@@ -239,10 +224,13 @@ export default class AnnouncementBar extends React.PureComponent {
return;
}
if (!prevState.allowDismissal && this.state.allowDismissal) {
document.body.classList.remove('announcement-bar--fixed');
} else if (prevState.allowDismissal && !this.state.allowDismissal) {
const wasFixed = this.shouldRender(prevProps, prevState) && !prevState.allowDismissal;
const isFixed = this.shouldRender(this.props, this.state) && !this.state.allowDismissal;
if (!wasFixed && isFixed) {
document.body.classList.add('announcement-bar--fixed');
} else if (wasFixed && !isFixed) {
document.body.classList.remove('announcement-bar--fixed');
}
}
......@@ -284,12 +272,28 @@ export default class AnnouncementBar extends React.PureComponent {
this.setState(this.getState());
}
render() {
if (!this.isValidState(this.state)) {
return <div/>;
shouldRender(props, state) {
if (!state) {
return false;
}
if (!state.message) {
return false;
}
if (state.message === AnnouncementBarMessages.LICENSE_EXPIRING && !state.totalUsers) {
return false;
}
if (!this.props.isLoggedIn && this.state.type === AnnouncementBarTypes.ANNOUNCEMENT) {
if (!props.isLoggedIn && state.type === AnnouncementBarTypes.ANNOUNCEMENT) {
return false;
}
return true;
}
render() {
if (!this.shouldRender(this.props, this.state)) {
return <div/>;
}
......
......@@ -322,6 +322,50 @@ export default class ChannelHeader extends React.Component {
actions.openModal(inviteModalData);
};
renderMute = () => {
const channelMuted = isChannelMuted(this.props.channelMember);
if (channelMuted) {
return (
<li
key='dropdown_unmute'
role='presentation'
>
<button
className='style--none'
id='channelUnmute'
role='menuitem'
onClick={this.unmute}
>
<FormattedMessage
id='channel_header.unmute'
defaultMessage='Unmute Channel'
/>
</button>
</li>
);
}
return (
<li
key='dropdown_mute'
role='presentation'
>
<button
className='style--none'
id='channelMute'
role='menuitem'
onClick={this.mute}
>
<FormattedMessage
id='channel_header.mute'
defaultMessage='Mute Channel'
/>
</button>
</li>
);
};
render() {
const channelIsArchived = this.props.channel.delete_at !== 0;
if (Utils.isEmptyObject(this.props.channel) ||
......@@ -447,7 +491,7 @@ export default class ChannelHeader extends React.Component {
>
<div
id='webrtc-btn'
className={'webrtc__button ' + circleClass}
className={'webrtc__button hidden-xs ' + circleClass}
>
{'WebRTC'}
</div>
......@@ -528,6 +572,8 @@ export default class ChannelHeader extends React.Component {
</li>
);
dropdownContents.push(this.renderMute());
dropdownContents.push(
<li
key='add_members'
......@@ -610,47 +656,7 @@ export default class ChannelHeader extends React.Component {
);
}
if (!isDirect) {
if (channelMuted) {
dropdownContents.push(
<li
key='dropdown_unmute'
role='presentation'
>
<button
className='style--none'
id='channelUnmute'
role='menuitem'
onClick={this.unmute}
>
<FormattedMessage
id='channel_header.unmute'
defaultMessage='Unmute Channel'
/>
</button>
</li>
);
} else {
dropdownContents.push(
<li
key='dropdown_mute'
role='presentation'
>
<button
className='style--none'
id='channelMute'
role='menuitem'
onClick={this.mute}
>
<FormattedMessage
id='channel_header.mute'
defaultMessage='Mute Channel'
/>
</button>
</li>
);
}
}
dropdownContents.push(this.renderMute());
if (!this.props.isDefault) {
dropdownContents.push(
......@@ -1086,11 +1092,9 @@ export default class ChannelHeader extends React.Component {
id='toggleMute'
onClick={this.unmute}
className={'style--none color--link channel-header__mute inactive'}
aria-label={Utils.localizeMessage('generic_icons.muted', 'Muted Icon')}
>
<i
className={'icon fa fa-bell-slash-o'}
title={Utils.localizeMessage('generic_icons.muted', 'Muted Icon')}
/>
<i className={'icon fa fa-bell-slash-o'}/>
</button>
</OverlayTrigger>
);
......
......@@ -46,7 +46,7 @@ function mapStateToProps(state, ownProps) {
const config = getConfig(state);
let lastViewedChannelName = getLastViewedChannelName(state);
if (!lastViewedChannelName) {
if (!lastViewedChannelName || (channel && lastViewedChannelName === channel.name)) {
lastViewedChannelName = Constants.DEFAULT_CHANNEL;
}
......
......@@ -32,32 +32,31 @@ export default class ChannelNotificationsModal extends React.Component {
this.state = {
activeSection: NotificationSections.NONE,
desktopNotifyLevel: props.channelMember.notify_props.desktop,
markUnreadNotifyLevel: props.channelMember.notify_props.mark_unread,
pushNotifyLevel: props.channelMember.notify_props.push || NotificationLevels.DEFAULT,
serverError: null,
...this.getStateFromNotifyProps(props.channelMember.notify_props),
};
}
componentDidUpdate(prevProps) {
if (!Utils.areObjectsEqual(this.props.channelMember.notify_props, prevProps.channelMember.notify_props)) {
this.setStateFromNotifyProps(this.props.channelMember.notify_props);
this.resetStateFromNotifyProps(this.props.channelMember.notify_props);
}
}
setStateFromNotifyProps(notifyProps) {
this.setState({
desktopNotifyLevel: notifyProps.desktop,
markUnreadNotifyLevel: notifyProps.mark_unread,
resetStateFromNotifyProps(notifyProps) {
this.setState(this.getStateFromNotifyProps(notifyProps));
}
getStateFromNotifyProps(notifyProps) {
return {
desktopNotifyLevel: notifyProps.desktop || NotificationLevels.DEFAULT,
markUnreadNotifyLevel: notifyProps.mark_unread || NotificationLevels.ALL,
pushNotifyLevel: notifyProps.push || NotificationLevels.DEFAULT,
});
};
}
handleOnHide = () => {
this.setState({
activeSection: NotificationSections.NONE,
});
this.setStateFromNotifyProps(this.props.channelMember.notify_props);
this.updateSection('');
this.props.onHide();
}
......@@ -66,7 +65,12 @@ export default class ChannelNotificationsModal extends React.Component {
if ($('.section-max').length) {
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
}
this.setState({activeSection: section});
if (section === '') {
this.resetStateFromNotifyProps(this.props.channelMember.notify_props);
}
}
handleUpdateChannelNotifyProps = async (props) => {
......@@ -101,13 +105,6 @@ export default class ChannelNotificationsModal extends React.Component {
this.setState({desktopNotifyLevel});
}
handleUpdateDesktopSection = (section = NotificationSections.NONE) => {
this.updateSection(section);
this.setState({
desktopNotifyLevel: this.props.channelMember.notify_props.desktop,
});
}
handleSubmitMarkUnreadLevel = () => {
const {channelMember} = this.props;
const {markUnreadNotifyLevel} = this.state;
......@@ -125,13 +122,6 @@ export default class ChannelNotificationsModal extends React.Component {
this.setState({markUnreadNotifyLevel});
}
handleUpdateMarkUnreadSection = (section = NotificationSections.NONE) => {
this.updateSection(section);
this.setState({
markUnreadNotifyLevel: this.props.channelMember.notify_props.mark_unread,
});
}
handleSubmitPushNotificationLevel = () => {
const {pushNotifyLevel} = this.state;
......@@ -148,13 +138,6 @@ export default class ChannelNotificationsModal extends React.Component {
this.setState({pushNotifyLevel});
}
handleUpdatePushSection = (section = NotificationSections.NONE) => {
this.updateSection(section);
this.setState({
pushNotifyLevel: this.props.channelMember.notify_props.push,
});
}
render() {
const {
activeSection,
......@@ -208,7 +191,7 @@ export default class ChannelNotificationsModal extends React.Component {
memberNotificationLevel={markUnreadNotifyLevel}
onChange={this.handleUpdateMarkUnreadLevel}
onSubmit={this.handleSubmitMarkUnreadLevel}
onUpdateSection={this.handleUpdateMarkUnreadSection}
onUpdateSection={this.updateSection}
serverError={serverError}
/>
{!isChannelMuted(channelMember) &&
......@@ -221,7 +204,7 @@ export default class ChannelNotificationsModal extends React.Component {
globalNotificationLevel={currentUser.notify_props ? currentUser.notify_props.desktop : NotificationLevels.ALL}
onChange={this.handleUpdateDesktopNotifyLevel}
onSubmit={this.handleSubmitDesktopNotifyLevel}
onUpdateSection={this.handleUpdateDesktopSection}
onUpdateSection={this.updateSection}
serverError={serverError}
/>
<div className='divider-light'/>
......@@ -233,7 +216,7 @@ export default class ChannelNotificationsModal extends React.Component {
globalNotificationLevel={currentUser.notify_props ? currentUser.notify_props.push : NotificationLevels.ALL}
onChange={this.handleUpdatePushNotificationLevel}
onSubmit={this.handleSubmitPushNotificationLevel}
onUpdateSection={this.handleUpdatePushSection}
onUpdateSection={this.updateSection}
serverError={serverError}
/>
}
......
......@@ -34,7 +34,8 @@ function mapStateToProps(state) {
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
let lastViewedChannelName = getLastViewedChannelName(state);
if (!lastViewedChannelName) {
if (!lastViewedChannelName || (channel && lastViewedChannelName === channel.name)) {
lastViewedChannelName = Constants.DEFAULT_CHANNEL;
}
......
......@@ -9,6 +9,7 @@ import {emailToLdap} from 'actions/admin_actions.jsx';
import {checkMfa} from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import LoginMfa from 'components/login/login_mfa.jsx';
import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx';
export default class EmailToLDAP extends React.Component {
constructor(props) {
......@@ -85,9 +86,7 @@ export default class EmailToLDAP extends React.Component {
ldapId || this.state.ldapId,
ldapPassword || this.state.ldapPassword,
(data) => {
if (data.follow_link) {
window.location.href = data.follow_link;
}
emitUserLoggedOutEvent(data.follow_link, false, true);
},
(err) => {
switch (err.id) {
......
......@@ -60,9 +60,8 @@ export default class EmailToOAuth extends React.Component {
token,
this.props.newType,
(data) => {
if (data.follow_link) {
window.location.href = data.follow_link;
}
// Stay logged in and just redirect to the OAuth provider.
window.location.href = data.follow_link;
},
(err) => {
this.setState({error: err.message, showMfa: false});
......
......@@ -8,6 +8,7 @@ import {FormattedMessage} from 'react-intl';
import {checkMfa, switchFromLdapToEmail} from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import LoginMfa from 'components/login/login_mfa.jsx';
import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx';
export default class LDAPToEmail extends React.Component {
constructor(props) {
......@@ -89,9 +90,7 @@ export default class LDAPToEmail extends React.Component {
token,
ldapPassword || this.state.ldapPassword,
(data) => {
if (data.follow_link) {
window.location.href = data.follow_link;
}
emitUserLoggedOutEvent(data.follow_link, false, true);
},
(err) => {
if (err.id.startsWith('model.user.is_valid.pwd')) {
......
......@@ -9,6 +9,7 @@ import {FormattedMessage} from 'react-intl';
import {oauthToEmail} from 'actions/admin_actions.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx';
export default class OAuthToEmail extends React.Component {
constructor(props) {
......@@ -50,7 +51,9 @@ export default class OAuthToEmail extends React.Component {
this.props.currentType,
this.props.email,
password,
null,
() => {
emitUserLoggedOutEvent('/', false, true);
},
(err) => {
this.setState({error: err.message});
}
......
......@@ -6,33 +6,41 @@ import React from 'react';
import {ContextMenu, ContextMenuTrigger, MenuItem} from 'react-contextmenu';
import {FormattedMessage} from 'react-intl';
import * as Utils from '../utils/utils';
export default class CopyUrlContextMenu extends React.Component {
static propTypes = {
/**
* The child component that will be right-clicked on to show the context menu
*/
// The child component that will be right-clicked on to show the context menu
children: PropTypes.element,
/**
* The link to copy to the user's clipboard when the 'Copy' option is selected from the context menu
*/
// The link to copy to the user's clipboard when the 'Copy' option is selected from the context menu
link: PropTypes.string.isRequired,
/**
* A unique id differentiating this instance of context menu from others on the page. Will be set to a random value if not provided.
*/
// A unique id differentiating this instance of context menu from others on the page.
menuId: PropTypes.string.isRequired,
siteURL: PropTypes.string.isRequired,
actions: PropTypes.shape({
copyToClipboard: PropTypes.func.isRequired,
}),
};
copy = () => {
let link = this.props.link;
// Transform relative links to absolute ones for copy and paste.
if (link.indexOf('http://') === -1 && link.indexOf('https://') === -1) {
link = this.props.siteURL + link;
}
this.props.actions.copyToClipboard(link);
}
render() {
const contextMenu = (
<ContextMenu id={'copy-url-context-menu' + this.props.menuId}>
<MenuItem
data={{link: this.props.link}}
onClick={Utils.copyToClipboard}
onClick={this.copy}
>
<FormattedMessage
id='copy_url_context_menu.getChannelLink'
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {copyToClipboard} from 'utils/utils';
import CopyUrlContextMenu from './copy_url_context_menu.jsx';
function mapStateToProps(state) {
const config = getConfig(state);
return {
siteURL: config.SiteURL,
};
}
function mapDispatchToProps() {
return {
actions: {
copyToClipboard,
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(CopyUrlContextMenu);
......@@ -17,7 +17,7 @@ import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox} from '
import ConfirmModal from 'components/confirm_modal.jsx';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import FilePreview from 'components/file_preview/file_preview.jsx';
import FilePreview from 'components/file_preview.jsx';
import FileUpload from 'components/file_upload';
import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal.jsx';
......@@ -102,6 +102,11 @@ export default class CreateComment extends React.PureComponent {
*/
onUpdateCommentDraft: PropTypes.func.isRequired,
/**
* Called when comment draft needs to be updated for an specific root ID
*/
updateCommentDraftWithRootId: PropTypes.func.isRequired,
/**
* Called when submitting the comment
*/
......@@ -171,12 +176,10 @@ export default class CreateComment extends React.PureComponent {
uploadsInProgress: [],
fileInfos: [],
},
actualDrafts: {},
uploadsProgressPercent: {},
};
this.lastBlurAt = 0;
this.draftsTimeout = null;
this.draftsForPost = {};
}
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
......@@ -193,12 +196,6 @@ export default class CreateComment extends React.PureComponent {
componentWillUnmount() {
this.props.resetCreatePostRequest();
document.removeEventListener('keydown', this.focusTextboxIfNecessary);
if (this.draftsTimeout) {
const {draft, actualDrafts} = this.state;
clearTimeout(this.draftsTimeout);
this.props.onUpdateCommentDraft({...draft, fileInfos: actualDrafts.fileInfos, uploadsInProgress: actualDrafts.uploadsInProgress});
}
}
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
......@@ -283,11 +280,17 @@ export default class CreateComment extends React.PureComponent {
newMessage = `${draft.message} :${emojiAlias}: `;
}
this.props.onUpdateCommentDraft({...draft, message: newMessage});
const modifiedDraft = {
...draft,
message: newMessage,
};
this.props.onUpdateCommentDraft(modifiedDraft);
this.draftsForPost[this.props.rootId] = modifiedDraft;
this.setState({
showEmojiPicker: false,
draft: {...draft, message: newMessage},
draft: modifiedDraft,
});
this.focusTextbox();
......@@ -306,11 +309,17 @@ export default class CreateComment extends React.PureComponent {
newMessage = `${draft.message} ${gif} `;
}
this.props.onUpdateCommentDraft({...draft, message: newMessage});
const modifiedDraft = {
...draft,
message: newMessage,
};
this.props.onUpdateCommentDraft(modifiedDraft);
this.draftsForPost[this.props.rootId] = modifiedDraft;
this.setState({
showEmojiPicker: false,
draft: {...draft, message: newMessage},
draft: modifiedDraft,
});
this.focusTextbox();
......@@ -400,6 +409,7 @@ export default class CreateComment extends React.PureComponent {
const updatedDraft = {...draft, message};
this.props.onUpdateCommentDraft(updatedDraft);
this.setState({draft: updatedDraft}, () => this.handleSubmit(e));
this.draftsForPost[this.props.rootId] = updatedDraft;
} else {
this.handleSubmit(e);
}
......@@ -424,6 +434,7 @@ export default class CreateComment extends React.PureComponent {
this.setState({draft: updatedDraft}, () => {
this.scrollToBottom();
});
this.draftsForPost[this.props.rootId] = updatedDraft;
}
handleKeyDown = (e) => {
......@@ -466,21 +477,21 @@ export default class CreateComment extends React.PureComponent {
const {draft} = this.state;
const uploadsInProgress = [...draft.uploadsInProgress, ...clientIds];
this.props.onUpdateCommentDraft({...draft, uploadsInProgress});
this.setState({draft: {...draft, uploadsInProgress}});
const modifiedDraft = {
...draft,
uploadsInProgress,
};
this.props.onUpdateCommentDraft(modifiedDraft);
this.setState({draft: modifiedDraft});
this.draftsForPost[this.props.rootId] = modifiedDraft;
// this is a bit redundant with the code that sets focus when the file input is clicked,
// but this also resets the focus after a drag and drop
this.focusTextbox();
}
handleUploadProgress = (clientId, name, percent) => {
const uploadsProgressPercent = {...this.state.uploadsProgressPercent, [clientId]: {percent, name}};
this.setState({uploadsProgressPercent});
}
handleFileUploadComplete = (fileInfos, clientIds) => {
const {draft} = this.state;
handleFileUploadComplete = (fileInfos, clientIds, channelId, rootId) => {
const draft = this.draftsForPost[rootId];
const uploadsInProgress = [...draft.uploadsInProgress];
const newFileInfos = sortFileInfos([...draft.fileInfos, ...fileInfos], this.props.locale);
......@@ -493,15 +504,16 @@ export default class CreateComment extends React.PureComponent {
}
}
this.setState({
actualDrafts: {...draft, fileInfos: newFileInfos, uploadsInProgress},
});
this.draftsTimeout = setTimeout(() => {
clearTimeout(this.draftsTimeout);
this.props.onUpdateCommentDraft({...draft, fileInfos: newFileInfos, uploadsInProgress});
this.setState({draft: {...draft, fileInfos: newFileInfos, uploadsInProgress}});
}, 500);
const modifiedDraft = {
...draft,
fileInfos: newFileInfos,
uploadsInProgress,
};
this.props.updateCommentDraftWithRootId(rootId, modifiedDraft);
this.draftsForPost[rootId] = modifiedDraft;
if (this.props.rootId === rootId) {
this.setState({draft: modifiedDraft});
}
// Focus on preview if needed/possible - if user has switched teams since starting the file upload,
// the preview will be undefined and the switch will fail
......@@ -510,9 +522,9 @@ export default class CreateComment extends React.PureComponent {
}
}
handleUploadError = (err, clientId = -1) => {
handleUploadError = (err, clientId = -1, rootId = -1) => {
if (clientId !== -1) {
const {draft} = this.state;
const draft = {...this.draftsForPost[rootId]};
const uploadsInProgress = [...draft.uploadsInProgress];
const index = uploadsInProgress.indexOf(clientId);
......@@ -520,8 +532,15 @@ export default class CreateComment extends React.PureComponent {
uploadsInProgress.splice(index, 1);
}
this.props.onUpdateCommentDraft({...draft, uploadsInProgress});
this.setState({draft: {...draft, uploadsInProgress}});
const modifiedDraft = {
...draft,
uploadsInProgress,
};
this.props.updateCommentDraftWithRootId(rootId, modifiedDraft);
this.draftsForPost[rootId] = modifiedDraft;
if (this.props.rootId === rootId) {
this.setState({draft: modifiedDraft});
}
}
this.setState({serverError: err});
......@@ -551,8 +570,15 @@ export default class CreateComment extends React.PureComponent {
fileInfos.splice(index, 1);
}
this.props.onUpdateCommentDraft({...draft, fileInfos, uploadsInProgress});
this.setState({draft: {...draft, fileInfos, uploadsInProgress}});
const modifiedDraft = {
...draft,
fileInfos,
uploadsInProgress,
};
this.props.onUpdateCommentDraft(modifiedDraft);
this.setState({draft: modifiedDraft});
this.draftsForPost[this.props.rootId] = modifiedDraft;
this.handleFileUploadChange();
}
......@@ -649,7 +675,6 @@ export default class CreateComment extends React.PureComponent {
fileInfos={draft.fileInfos}
onRemove={this.removePreview}
uploadsInProgress={draft.uploadsInProgress}
uploadsProgressPercent={this.state.uploadsProgressPercent}
ref='preview'
/>
);
......@@ -688,9 +713,9 @@ export default class CreateComment extends React.PureComponent {
getTarget={this.getFileUploadTarget}
onFileUploadChange={this.handleFileUploadChange}
onUploadStart={this.handleUploadStart}
onUploadProgress={this.handleUploadProgress}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
rootId={this.props.rootId}
postType='comment'
/>
);
......
......@@ -100,6 +100,7 @@ function makeMapDispatchToProps() {
return bindActionCreators({
clearCommentDraftUploads,
onUpdateCommentDraft,
updateCommentDraftWithRootId: updateCommentDraft,
onSubmit,
onResetHistoryIndex,
onMoveHistoryIndexBack,
......
......@@ -20,7 +20,7 @@ import * as Utils from 'utils/utils.jsx';
import ConfirmModal from 'components/confirm_modal.jsx';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import FilePreview from 'components/file_preview/file_preview.jsx';
import FilePreview from 'components/file_preview.jsx';
import FileUpload from 'components/file_upload';
import MsgTyping from 'components/msg_typing';