Commit 21a0affa authored by James Addison's avatar James Addison
Browse files

Merge branch 'master' into collabora

parents d09100ff ccae9a6a
......@@ -2,15 +2,13 @@
"root": true,
"extends": [
"plugin:mattermost/react",
"plugin:cypress/recommended",
"plugin:jquery/deprecated"
"plugin:cypress/recommended"
],
"plugins": [
"babel",
"mattermost",
"import",
"cypress",
"jquery",
"no-only-tests",
"@typescript-eslint"
],
......@@ -153,55 +151,6 @@
"babel/no-unused-expressions": 0,
"func-names": 0,
"import/no-unresolved": 0,
"jquery/no-ajax": 0,
"jquery/no-ajax-events": 0,
"jquery/no-animate": 0,
"jquery/no-attr": 0,
"jquery/no-bind": 0,
"jquery/no-class": 0,
"jquery/no-clone": 0,
"jquery/no-closest": 0,
"jquery/no-css": 0,
"jquery/no-data": 0,
"jquery/no-deferred": 0,
"jquery/no-delegate": 0,
"jquery/no-each": 0,
"jquery/no-extend": 0,
"jquery/no-fade": 0,
"jquery/no-filter": 0,
"jquery/no-find": 0,
"jquery/no-global-eval": 0,
"jquery/no-grep": 0,
"jquery/no-has": 0,
"jquery/no-hide": 0,
"jquery/no-html": 0,
"jquery/no-in-array": 0,
"jquery/no-is-array": 0,
"jquery/no-is-function": 0,
"jquery/no-is": 0,
"jquery/no-load": 0,
"jquery/no-map": 0,
"jquery/no-merge": 0,
"jquery/no-param": 0,
"jquery/no-parent": 0,
"jquery/no-parents": 0,
"jquery/no-parse-html": 0,
"jquery/no-prop": 0,
"jquery/no-proxy": 0,
"jquery/no-ready": 0,
"jquery/no-serialize": 0,
"jquery/no-show": 0,
"jquery/no-size": 0,
"jquery/no-sizzle": 0,
"jquery/no-slide": 0,
"jquery/no-submit": 0,
"jquery/no-text": 0,
"jquery/no-toggle": 0,
"jquery/no-trigger": 0,
"jquery/no-trim": 0,
"jquery/no-val": 0,
"jquery/no-when": 0,
"jquery/no-wrap": 0,
"max-nested-callbacks": 0,
"no-process-env": 0,
"no-unused-expressions": 0
......
......@@ -19,6 +19,7 @@ coverage
# disable folders generated by Cypress
e2e/node_modules
e2e/cypress/downloads
e2e/cypress/screenshots
e2e/cypress/videos
e2e/cypress/benchmark/__benchmarks__
......
......@@ -315,49 +315,6 @@ SOFTWARE.
---
## compass-mixins
This product contains 'compass-mixins' by Guillaume Balaine.
Compass stylesheets
* HOMEPAGE:
* https://github.com/Igosuki/compass-mixins#readme
* LICENSE: MIT
Copyright (c) 2009 Christopher M. Eppstein
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
No attribution is required by products that make use of this software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Except as contained in this notice, the name(s) of the above copyright
holders shall not be used in advertising or otherwise to promote the sale,
use or other dealings in this Software without prior written authorization.
Contributors to this project agree to grant all rights to the copyright
holder of the primary product. Attribution is maintained in the source
control history of the product.
---
## core-js
This product contains 'core-js' by Denis Pushkarev.
......@@ -439,25 +396,25 @@ The MIT License
Copyright (c) 2013 Dominic Tarr
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
associated documentation files (the "Software"), to
deal in the Software without restriction, including
without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom
the Software is furnished to do so,
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
associated documentation files (the "Software"), to
deal in the Software without restriction, including
without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom
the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice
The above copyright notice and this permission notice
shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
......@@ -535,9 +492,9 @@ SOFTWARE.
## emoji-datasource & emoji-datasource-apple by Cal Henderson
This product contains 'emoji-datasource' & 'emoji-datasource-apple' by
This product contains 'emoji-datasource' & 'emoji-datasource-apple' by
* HOMEPAGE:
* HOMEPAGE:
* https://github.com/iamcal/emoji-data
* LICENSE: MIT
......@@ -1410,40 +1367,6 @@ Additional features and components for Bootstrap
---
## jquery
This product contains 'jquery' by JS Foundation and other contributors.
JavaScript library for DOM operations
* HOMEPAGE:
* https://jquery.com
* LICENSE: MIT
Copyright JS Foundation and other contributors, https://js.foundation/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## katex
This product contains 'katex' by GitHub user "KaTeX".
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import request from 'superagent';
import * as IntegrationActions from 'mattermost-redux/actions/integrations';
import {getProfilesByIds} from 'mattermost-redux/actions/users';
import {getUser} from 'mattermost-redux/selectors/entities/users';
......@@ -128,19 +126,3 @@ export function loadProfilesForOAuthApps(apps) {
dispatch(getProfilesByIds(list));
};
}
export function getYoutubeVideoInfo(googleKey, videoId, success, error) {
request.get('https://www.googleapis.com/youtube/v3/videos').
query({part: 'snippet', id: videoId, key: googleKey}).
end((err, res) => {
if (err) {
return error(err);
}
if (!res.body) {
console.error('Missing response body for getYoutubeVideoInfo'); // eslint-disable-line no-console
}
return success(res.body);
});
}
......@@ -11,6 +11,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {browserHistory} from 'utils/browser_history';
import {Preferences} from 'utils/constants';
import {selectTeam} from 'mattermost-redux/actions/teams';
export function removeUserFromTeamAndGetStats(teamId, userId) {
return async (dispatch, getState) => {
......@@ -80,7 +81,7 @@ export function addUsersToTeam(teamId, userIds) {
};
}
export function switchTeam(url) {
export function switchTeam(url, setTeam = undefined) {
return (dispatch, getState) => {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
......@@ -88,7 +89,11 @@ export function switchTeam(url) {
dispatch(viewChannel(currentChannelId));
}
browserHistory.push(url);
if (setTeam) {
dispatch(selectTeam(setTeam));
} else {
browserHistory.push(url);
}
};
}
......
......@@ -5,30 +5,31 @@ import {GenericAction} from 'mattermost-redux/types/actions';
import {Constants, ActionTypes, WindowSizes} from 'utils/constants';
export function emitBrowserWindowResized(): GenericAction {
const width = window.innerWidth;
export function emitBrowserWindowResized(windowSize: string): GenericAction {
let newWindowSize = windowSize;
if (!windowSize) {
const width = window.innerWidth;
let windowSize;
switch (true) {
case width > Constants.TABLET_SCREEN_WIDTH && width <= Constants.DESKTOP_SCREEN_WIDTH: {
windowSize = WindowSizes.SMALL_DESKTOP_VIEW;
break;
switch (true) {
case width > Constants.TABLET_SCREEN_WIDTH && width <= Constants.DESKTOP_SCREEN_WIDTH: {
newWindowSize = WindowSizes.SMALL_DESKTOP_VIEW;
break;
}
case width > Constants.MOBILE_SCREEN_WIDTH && width <= Constants.TABLET_SCREEN_WIDTH: {
newWindowSize = WindowSizes.TABLET_VIEW;
break;
}
case width <= Constants.MOBILE_SCREEN_WIDTH: {
newWindowSize = WindowSizes.MOBILE_VIEW;
break;
}
default: {
newWindowSize = WindowSizes.DESKTOP_VIEW; // width > Constants.DESKTOP_SCREEN_WIDTH
}
}
}
case width > Constants.MOBILE_SCREEN_WIDTH && width <= Constants.TABLET_SCREEN_WIDTH: {
windowSize = WindowSizes.TABLET_VIEW;
break;
}
case width <= Constants.MOBILE_SCREEN_WIDTH: {
windowSize = WindowSizes.MOBILE_VIEW;
break;
}
default: {
windowSize = WindowSizes.DESKTOP_VIEW; // width > Constants.DESKTOP_SCREEN_WIDTH
}
}
return {
type: ActionTypes.BROWSER_WINDOW_RESIZED,
data: windowSize,
data: newWindowSize,
};
}
......@@ -8,6 +8,7 @@ import {
joinChannel,
markChannelAsRead,
unfavoriteChannel,
deleteChannel as deleteChannelRedux,
} from 'mattermost-redux/actions/channels';
import * as PostActions from 'mattermost-redux/actions/posts';
import {TeamTypes} from 'mattermost-redux/action_types';
......@@ -34,11 +35,13 @@ import {makeAddLastViewAtToProfiles} from 'mattermost-redux/selectors/entities/u
import {getChannelByName} from 'mattermost-redux/utils/channel_utils';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {closeRightHandSide} from 'actions/views/rhs';
import {openDirectChannelToUserId} from 'actions/channel_actions.jsx';
import {loadCustomStatusEmojisForPostList} from 'actions/emoji_actions';
import {getLastViewedChannelName} from 'selectors/local_storage';
import {getLastPostsApiTimeForChannel} from 'selectors/views/channel';
import {getSocketStatus} from 'selectors/views/websocket';
import {getSelectedPost, getSelectedPostId} from 'selectors/rhs';
import {browserHistory} from 'utils/browser_history';
import {Constants, ActionTypes, EventTypes, PostRequestTypes} from 'utils/constants';
......@@ -147,6 +150,11 @@ export function leaveChannel(channelId) {
if (!prevChannel || !getMyChannelMemberships(state)[prevChannel.id]) {
LocalStorageStore.removePreviousChannelName(currentUserId, currentTeam.id, state);
}
const selectedPost = getSelectedPost(state);
const selectedPostId = getSelectedPostId(state);
if (selectedPostId && selectedPost.exists === false) {
dispatch(closeRightHandSide());
}
if (getMyChannels(getState()).filter((c) => c.type === Constants.OPEN_CHANNEL || c.type === Constants.PRIVATE_CHANNEL).length === 0) {
LocalStorageStore.removePreviousChannelName(currentUserId, currentTeam.id, state);
......@@ -456,3 +464,21 @@ export function updateToastStatus(status) {
});
};
}
export function deleteChannel(channelId) {
return async (dispatch, getState) => {
const res = await dispatch(deleteChannelRedux(channelId));
if (res.error) {
return {data: false};
}
const state = getState();
const selectedPost = getSelectedPost(state);
const selectedPostId = getSelectedPostId(state);
if (selectedPostId && !selectedPost.exists) {
dispatch(closeRightHandSide());
}
return {data: true};
};
}
......@@ -11,6 +11,7 @@ import * as PostActions from 'mattermost-redux/actions/posts';
import {browserHistory} from 'utils/browser_history';
import * as Actions from 'actions/views/channel';
import {closeRightHandSide} from 'actions/views/rhs';
import {ActionTypes, PostRequestTypes} from 'utils/constants';
const mockStore = configureStore([thunk]);
......@@ -38,6 +39,11 @@ jest.mock('mattermost-redux/actions/channels', () => ({
leaveChannel: jest.fn(() => ({type: ''})),
}));
jest.mock('actions/views/rhs', () => ({
...jest.requireActual('actions/views/rhs'),
closeRightHandSide: jest.fn(() => ({type: ''})),
}));
jest.mock('mattermost-redux/actions/posts');
jest.mock('selectors/local_storage', () => ({
......@@ -93,6 +99,7 @@ describe('channel view actions', () => {
},
posts: {
postsInChannel: {},
posts: {},
},
channelCategories: {
byId: {},
......@@ -103,6 +110,9 @@ describe('channel view actions', () => {
loadingPosts: {},
postVisibility: {current_channel_id: 60},
},
rhs: {
selectedPostId: '',
},
},
};
......@@ -129,6 +139,22 @@ describe('channel view actions', () => {
await store.dispatch(Actions.leaveChannel('channelid1'));
expect(browserHistory.push).toHaveBeenCalledWith(`/${team1.name}`);
expect(leaveChannel).toHaveBeenCalledWith('channelid1');
expect(closeRightHandSide).not.toHaveBeenCalled();
});
test('leave a channel successfully with a thread open', async () => {
store = mockStore({
...initialState,
views: {
...initialState.views,
rhs: {
selectedPostId: '1',
},
},
});
await store.dispatch(Actions.leaveChannel('channelid1'));
expect(browserHistory.push).toHaveBeenCalledWith(`/${team1.name}`);
expect(leaveChannel).toHaveBeenCalledWith('channelid1');
expect(closeRightHandSide).toHaveBeenCalled();
});
test('leave the last channel successfully', async () => {
store = mockStore({
......
......@@ -171,6 +171,15 @@ export function multiSelectChannelAdd(channelId: string) {
};
}
export function setFirstChannelName(channelName: string) {
return (dispatch: DispatchFunc) => {
dispatch({
type: ActionTypes.FIRST_CHANNEL_NAME,
data: channelName,
});
};
}
// Much of this logic was pulled from the react-beautiful-dnd sample multiselect implementation
// Found here: https://github.com/atlassian/react-beautiful-dnd/tree/master/stories/src/multi-drag
export function multiSelectChannelTo(channelId: string) {
......
......@@ -24,11 +24,13 @@ export function selectAttachmentMenuAction(postId, actionId, cookie, dataSource,
return async (dispatch) => {
dispatch({
type: ActionTypes.SELECT_ATTACHMENT_MENU_ACTION,
postId,
data: {
[actionId]: {
text,
value,
postId,
actions: {
[actionId]: {
text,
value,
},
},
},
});
......
......@@ -20,7 +20,7 @@ import {getCurrentChannelId, getCurrentChannelNameForSearchShortcut} from 'matte
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
import {DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions';
import {Action, ActionResult, DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions';
import {Post} from 'mattermost-redux/types/posts';
import {trackEvent} from 'actions/telemetry_actions.jsx';
......@@ -30,6 +30,7 @@ import * as Utils from 'utils/utils';
import {getBrowserUtcOffset, getUtcOffsetForTimeZone} from 'utils/timezone';
import {RhsState} from 'types/store/rhs';
import {GlobalState} from 'types/store';
import {getPostsByIds} from 'mattermost-redux/actions/posts';
function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
......@@ -284,7 +285,7 @@ export function showPinnedPosts(channelId?: string) {
}
export function showChannelFiles(channelId: string) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
return async (dispatch: (action: Action, getState?: GetStateFunc | null) => Promise<ActionResult|[ActionResult, ActionResult]>, getState: GetStateFunc) => {
const state = getState();
const teamId = getCurrentTeamId(state);
......@@ -294,10 +295,25 @@ export function showChannelFiles(channelId: string) {
state: RHSStates.CHANNEL_FILES,
});
const results: any = await dispatch(performSearch('channel:' + channelId));
const results = await dispatch(performSearch('channel:' + channelId));
const fileData = results instanceof Array ? results[0].data : null;
const missingPostIds: string[] = [];
if (fileData) {
Object.values(fileData.file_infos).forEach((file: any) => {
const postId = file?.post_id;
if (postId && !getPost(state, postId)) {
missingPostIds.push(postId);
}
});
}
if (missingPostIds.length > 0) {
await dispatch(getPostsByIds(missingPostIds));
}
let data: any;
if (results && results.length === 2 && 'data' in results[1]) {
if (results && results instanceof Array && results.length === 2 && 'data' in results[1]) {
data = results[1].data;
}
......
......@@ -9,7 +9,7 @@ import {GetStateFunc, DispatchFunc} from 'mattermost-redux/types/actions';
import {browserHistory} from 'utils/browser_history';
import {GlobalState} from 'types/store';
import {Threads} from 'utils/constants';
import {ActionTypes, Threads} from 'utils/constants';
export function updateThreadLastOpened(threadId: string, lastViewedAt: number) {
return {
......@@ -50,3 +50,10 @@ export function switchToGlobalThreads() {
return {data: true};
};
}
export function updateThreadToastStatus(status: boolean) {
return {
type: ActionTypes.UPDATE_THREAD_TOAST_STATUS,
data: status,
};
}
......@@ -22,7 +22,6 @@ import {addChannelToInitialCategory, fetchMyCategories, receivedCategoryOrder} f
import {
getChannelAndMyMember,
getMyChannelMember,
getChannelMember,
getChannelStats,
viewChannel,
markChannelAsRead,
......@@ -70,7 +69,14 @@ import {Client4} from 'mattermost-redux/client';
import {getCurrentUser, getCurrentUserId, getStatusForUserId, getUser, getIsManualStatusForUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {getMyTeams, getCurrentRelativeTeamUrl, getCurrentTeamId, getCurrentTeamUrl, getTeam} from 'mattermost-redux/selectors/entities/teams';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getChannelsInTeam, getChannel, getCurrentChannel, getCurrentChannelId, getRedirectChannelNameForTeam, getMembersInCurrentChannel, getChannelMembersInChannels} from 'mattermost-redux/selectors/entities/channels';
import {
getChannel,
getChannelMembersInChannels,
getChannelsInTeam,
getCurrentChannel,
getCurrentChannelId,
getRedirectChannelNameForTeam,
} from 'mattermost-redux/selectors/entities/channels';
import {getPost, getMostRecentPostIdInChannel} from 'mattermost-redux/selectors/entities/posts';
import {haveISystemPermission, haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
......@@ -946,9 +952,15 @@ export function handleUserRemovedEvent(msg) {
}
}
const channel = getChannel(state, msg.data.channel_id);
dispatch({
type: ChannelTypes.LEAVE_CHANNEL,
data: {id: msg.data.channel_id, user_id: msg.broadcast.user_id},
data: {
id: msg.data.channel_id,
user_id: msg.broadcast.user_id,
team_id: channel?.team_id,
},
});
if (msg.data.channel_id === currentChannel.id) {
......@@ -1006,6 +1018,10 @@ export function handleUserRemovedEvent(msg) {
}
export async function handleUserUpdatedEvent(msg) {
// This websocket event is sent to all non-guest users on the server, so be careful requesting data from the server
// in response to it. That can overwhelm the server if every connected user makes such a request at the same time.
// See https://mattermost.atlassian.net/browse/MM-40050 for more information.
const state = getState();
const currentUser = getCurrentUser(state);
const user = msg.data.user;
......@@ -1014,33 +1030,6 @@ export async function handleUserUpdatedEvent(msg) {
dispatch(loadCustomEmojisIfNeeded([customStatus?.emoji]));
}
const config = getConfig(state);
const license = getLicense(state);
const userIsGuest = isGuest(user.roles);
const isTimezoneEnabled = config.ExperimentalTimezone === 'true';