...
 
Commits (101)
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,
......
......@@ -22,7 +22,7 @@ import store from 'stores/redux_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import {Constants, Preferences} from 'utils/constants.jsx';
import {Constants, Preferences, UserStatuses} from 'utils/constants.jsx';
const dispatch = store.dispatch;
const getState = store.getState;
......@@ -536,7 +536,7 @@ export async function createUserWithInvite(user, token, inviteId, success, error
if (resp && success) {
success(resp);
} else if (err && error) {
error({id: err.server_error_id, ...err});
error({id: err.server_error_id, message: err.message, ...err});
}
}
......@@ -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) {
......@@ -674,7 +674,7 @@ export function autoResetStatus() {
const {currentUserId} = getState().entities.users;
const {data: userStatus} = await UserActions.getStatus(currentUserId)(doDispatch, doGetState);
if (!userStatus.manual) {
if (userStatus.status === UserStatuses.OUT_OF_OFFICE || !userStatus.manual) {
return userStatus;
}
......
......@@ -14,6 +14,7 @@ import {
moveHistoryIndexForward,
} from 'mattermost-redux/actions/posts';
import {Posts} from 'mattermost-redux/constants';
import {isPostPendingOrFailed} from 'mattermost-redux/utils/post_utils';
import * as PostActions from 'actions/post_actions.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
......@@ -162,10 +163,13 @@ function makeGetCurrentUsersLatestPost(channelId, rootId) {
const post = getPostById(id) || {};
// don't edit webhook posts, deleted posts, or system messages
if (post.user_id !== userId ||
if (
post.user_id !== userId ||
(post.props && post.props.from_webhook) ||
post.state === Constants.POST_DELETED ||
(post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX))) {
(post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX)) ||
isPostPendingOrFailed(post)
) {
continue;
}
......@@ -195,10 +199,10 @@ export function makeOnEditLatestPost(channelId, rootId) {
const lastPost = getCurrentUsersLatestPost(state);
if (!lastPost) {
return;
return {data: false};
}
dispatch(PostActions.setEditingPost(
return dispatch(PostActions.setEditingPost(
lastPost.id,
getCommentCount(state, {post: lastPost}),
'reply_textbox',
......
......@@ -29,7 +29,12 @@ export function selectAttachmentMenuAction(postId, actionId, dataSource, display
dispatch({
type: ActionTypes.SELECT_ATTACHMENT_MENU_ACTION,
postId,
data: {displayText, value},
data: {
[actionId]: {
displayText,
value,
},
},
});
dispatch(PostActions.doPostAction(postId, actionId, value));
......
......@@ -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));
};
}
......
......@@ -36,7 +36,7 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins';
import {ActionTypes, Constants, ErrorBarTypes, Preferences, SocketEvents, UserStatuses} from 'utils/constants.jsx';
import {ActionTypes, Constants, AnnouncementBarMessages, Preferences, SocketEvents, UserStatuses} from 'utils/constants.jsx';
import {fromAutoResponder} from 'utils/post_utils';
import {getSiteURL} from 'utils/url.jsx';
......@@ -188,7 +188,7 @@ function handleFirstConnect() {
function handleClose(failCount) {
if (failCount > MAX_WEBSOCKET_FAILS) {
ErrorStore.storeLastError({message: ErrorBarTypes.WEBSOCKET_PORT_ERROR});
ErrorStore.storeLastError({message: AnnouncementBarMessages.WEBSOCKET_PORT_ERROR});
}
ErrorStore.setConnectionErrorCount(failCount);
......
......@@ -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') {
......@@ -344,7 +358,8 @@ export default class AdminSidebar extends React.Component {
const customPlugins = [];
if (this.props.config.PluginSettings.Enable) {
Object.values(this.props.plugins).forEach((p) => {
if (!p.settings_schema || Object.keys(p.settings_schema) === 0) {
const hasSettings = p.settings_schema && (p.settings_schema.header || p.settings_schema.footer || p.settings_schema.settings.length > 0);
if (!hasSettings) {
return;
}
......@@ -530,6 +545,8 @@ export default class AdminSidebar extends React.Component {
}
/>
{oauthSettings}
{gitlabSettings}
{phabricatorSettings}
{ldapSettings}
{samlSettings}
{mfaSettings}
......
......@@ -5,7 +5,7 @@ import React from 'react';
import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
import ErrorStore from 'stores/error_store.jsx';
import {ErrorBarTypes} from 'utils/constants.jsx';
import {AnnouncementBarMessages} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import EmailConnectionTest from 'components/admin_console/email_connection_test';
......@@ -53,7 +53,7 @@ export default class EmailSettings extends AdminSettings {
handleSaved(newConfig) {
if (newConfig.EmailSettings.SendEmailNotifications || !newConfig.EmailSettings.EnablePreviewModeBanner) {
ErrorStore.clearError(ErrorBarTypes.PREVIEW_MODE);
ErrorStore.clearError(AnnouncementBarMessages.PREVIEW_MODE);
}
}
......
// 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>
);
}
}
......@@ -567,18 +567,22 @@ export default class PluginManagement extends React.Component {
return 0;
});
pluginsList = plugins.map((pluginStatus) => (
<PluginItem
key={pluginStatus.id}
pluginStatus={pluginStatus}
removing={this.state.removing === pluginStatus.id}
handleEnable={this.handleEnable}
handleDisable={this.handleDisable}
handleRemove={this.handleRemove}
showInstances={showInstances}
hasSettings={Boolean(this.props.plugins[pluginStatus.id] && this.props.plugins[pluginStatus.id].settings_schema)}
/>
));
pluginsList = plugins.map((pluginStatus) => {
const p = this.props.plugins[pluginStatus.id];
const hasSettings = Boolean(p && p.settings_schema && (p.settings_schema.header || p.settings_schema.footer || (p.settings_schema.settings && p.settings_schema.settings.length > 0)));
return (
<PluginItem
key={pluginStatus.id}
pluginStatus={pluginStatus}
removing={this.state.removing === pluginStatus.id}
handleEnable={this.handleEnable}
handleDisable={this.handleDisable}
handleRemove={this.handleRemove}
showInstances={showInstances}
hasSettings={hasSettings}
/>
);
});
pluginsContainer = (
<div className='alert alert-transparent'>
......
......@@ -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/>;
}
......
......@@ -3,13 +3,16 @@
import PropTypes from 'prop-types';
import React from 'react';
import {OverlayTrigger} from 'react-bootstrap';
import {Overlay} from 'react-bootstrap';
import {Client4} from 'mattermost-redux/client';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import Pluggable from 'plugins/pluggable';
import ProfilePopover from 'components/profile_popover';
import {popOverOverlayPosition} from 'utils/position_utils.jsx';
const spaceRequiredForPopOver = 300;
export default class AtMention extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
......@@ -29,11 +32,12 @@ export default class AtMention extends React.PureComponent {
constructor(props) {
super(props);
this.hideProfilePopover = this.hideProfilePopover.bind(this);
this.state = {
user: this.getUserFromMentionName(props),
show: false,
};
this.overlayRef = React.createRef();
}
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
......@@ -44,8 +48,15 @@ export default class AtMention extends React.PureComponent {
}
}
hideProfilePopover() {
this.refs.overlay.hide();
handleClick = (e) => {
const targetBounds = this.overlayRef.current.getBoundingClientRect();
const placement = popOverOverlayPosition(targetBounds, window.innerHeight, {above: spaceRequiredForPopOver});
this.setState({target: e.target, show: !this.state.show, placement});
}
hideOverlay = () => {
this.setState({show: false});
}
getUserFromMentionName(props) {
......@@ -83,25 +94,29 @@ export default class AtMention extends React.PureComponent {
return (
<span>
<OverlayTrigger
ref='overlay'
trigger='click'
placement='top'
<Overlay
placement={this.state.placement}
show={this.state.show}
target={this.state.target}
rootClose={true}
overlay={
<Pluggable>
<ProfilePopover
user={user}
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
hide={this.hideProfilePopover}
isRHS={this.props.isRHS}
hasMention={this.props.hasMention}
/>
</Pluggable>
}
onHide={this.hideOverlay}
>
<Pluggable>
<ProfilePopover
user={user}
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
isRHS={this.props.isRHS}
hasMention={this.props.hasMention}
/>
</Pluggable>
</Overlay>
<a
className={className}
onClick={this.handleClick}
ref={this.overlayRef}
>
<a className={className}>{'@' + displayUsername(user, this.props.teammateNameDisplay)}</a>
</OverlayTrigger>
{'@' + displayUsername(user, this.props.teammateNameDisplay)}
</a>
{suffix}
</span>
);
......
......@@ -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,41 +32,45 @@ 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(NotificationSections.NONE);
this.props.onHide();
}
updateSection = (section) => {
updateSection = (section = NotificationSections.NONE) => {
if ($('.section-max').length) {
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
}
this.setState({activeSection: section});
if (section === NotificationSections.NONE) {
this.resetStateFromNotifyProps(this.props.channelMember.notify_props);
}
}
handleUpdateChannelNotifyProps = async (props) => {
......@@ -80,7 +84,7 @@ export default class ChannelNotificationsModal extends React.Component {
if (error) {
this.setState({serverError: error.message});
} else {
this.updateSection('');
this.updateSection(NotificationSections.NONE);
}
}
......@@ -89,7 +93,7 @@ export default class ChannelNotificationsModal extends React.Component {
const {desktopNotifyLevel} = this.state;
if (channelMember.notify_props.desktop === desktopNotifyLevel) {
this.updateSection('');
this.updateSection(NotificationSections.NONE);
return;
}
......@@ -101,19 +105,12 @@ 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;
if (channelMember.notify_props.mark_unread === markUnreadNotifyLevel) {
this.updateSection('');
this.updateSection(NotificationSections.NONE);
return;
}
......@@ -125,18 +122,11 @@ 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;
if (this.props.channelMember.notify_props.push === pushNotifyLevel) {
this.updateSection('');
this.updateSection(NotificationSections.NONE);
return;
}
......@@ -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}
/>
}
......
......@@ -4,6 +4,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import {NotificationSections} from 'utils/constants.jsx';
import CollapseView from './collapse_view.jsx';
import ExpandView from './expand_view.jsx';
......@@ -60,7 +62,7 @@ export default class NotificationSection extends React.PureComponent {
}
handleCollapseSection = () => {
this.props.onUpdateSection();
this.props.onUpdateSection(NotificationSections.NONE);
}
render() {
......
......@@ -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;
}
......
......@@ -50,7 +50,11 @@ export default class OAuthToEmail extends React.Component {
this.props.currentType,
this.props.email,
password,
null,
(data) => {
if (data.follow_link) {
window.location.href = data.follow_link;
}
},
(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.