Commit 2af87750 authored by James Addison's avatar James Addison
Browse files

Merge remote-tracking branch 'upstream/release-5.35' into collabora-5.35

parents f788db1b 8d78d72a
Pipeline #26896 passed with stage
in 16 minutes and 1 second
......@@ -126,16 +126,18 @@
}
},
{
"files": ["tests/**", "**/*.test.*"],
"files": ["tests/**", "**/*.test.*", "tests/*.js"],
"env": {
"jest": true
},
"rules": {
"func-names": 0,
"global-require": 0,
"max-lines": 0,
"new-cap": 0,
"prefer-arrow-callback": 0,
"no-import-assign": 0
"no-import-assign": 0,
"no-process-env": 0,
"prefer-arrow-callback": 0
}
},
{
......
......@@ -4,6 +4,7 @@
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import {ChannelTypes} from 'mattermost-redux/action_types';
import {receivedNewPost} from 'mattermost-redux/actions/posts';
import {Posts} from 'mattermost-redux/constants';
......@@ -13,9 +14,8 @@ import {Constants} from 'utils/constants';
const mockStore = configureStore([thunk]);
jest.mock('mattermost-redux/actions/channels', () => ({
markChannelAsUnread: (...args) => ({type: 'MOCK_MARK_CHANNEL_AS_UNREAD', args}),
markChannelAsRead: (...args) => ({type: 'MOCK_MARK_CHANNEL_AS_READ', args}),
markChannelAsViewed: (...args) => ({type: 'MOCK_MARK_CHANNEL_AS_VIEWED', args}),
...jest.requireActual('mattermost-redux/actions/channels'),
markChannelAsReadOnServer: (...args) => ({type: 'MOCK_MARK_CHANNEL_AS_READ_ON_SERVER', args}),
}));
const POST_CREATED_TIME = Date.now();
......@@ -90,30 +90,33 @@ describe('actions/new_post', () => {
};
test('completePostReceive', async () => {
const testStore = await mockStore(initialState);
const testStore = mockStore(initialState);
const newPost = {id: 'new_post_id', channel_id: 'current_channel_id', message: 'new message', type: Constants.PostTypes.ADD_TO_CHANNEL, user_id: 'some_user_id', create_at: POST_CREATED_TIME, props: {addedUserId: 'other_user_id'}};
const websocketProps = {team_id: 'team_id', mentions: ['current_user_id']};
await testStore.dispatch(NewPostActions.completePostReceive(newPost, websocketProps));
expect(testStore.getActions()).toEqual([
INCREASED_POST_VISIBILITY,
{
meta: {batch: true},
payload: [receivedNewPost(newPost), STOP_TYPING],
payload: [
INCREASED_POST_VISIBILITY,
receivedNewPost(newPost),
STOP_TYPING,
],
type: 'BATCHING_REDUCER.BATCH',
},
]);
});
describe('setChannelReadAndViewed', () => {
test('should mark channel as read when viewing channel', async () => {
test('should mark channel as read when viewing channel', () => {
const channelId = 'channel';
const currentUserId = 'user';
const post1 = {id: 'post1', channel_id: channelId, create_at: 1000};
const post2 = {id: 'post2', channel_id: channelId, create_at: 2000};
const testStore = await mockStore({
const testStore = mockStore({
entities: {
channels: {
currentChannelId: channelId,
......@@ -146,25 +149,44 @@ describe('actions/new_post', () => {
window.isActive = true;
await testStore.dispatch(NewPostActions.setChannelReadAndViewed(post2, {}, false));
const actions = NewPostActions.setChannelReadAndViewed(testStore.dispatch, testStore.getState, post2, {}, false);
expect(testStore.getActions()).toEqual([{
type: 'MOCK_MARK_CHANNEL_AS_READ',
args: [channelId, undefined, true],
}, {
type: 'MOCK_MARK_CHANNEL_AS_VIEWED',
args: [channelId],
}]);
expect(actions).toMatchObject([
{
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: {
channel_id: channelId,
},
},
]);
expect(testStore.getActions()).toMatchObject([
{
type: 'MOCK_MARK_CHANNEL_AS_READ_ON_SERVER',
args: [channelId],
},
]);
});
test('should mark channel as unread when not actively viewing channel', async () => {
test('should mark channel as unread when not actively viewing channel', () => {
const channelId = 'channel';
const currentUserId = 'user';
const post1 = {id: 'post1', channel_id: channelId, create_at: 1000};
const post2 = {id: 'post2', channel_id: channelId, create_at: 2000};
const testStore = await mockStore({
const testStore = mockStore({
entities: {
channels: {
currentChannelId: channelId,
......@@ -197,15 +219,26 @@ describe('actions/new_post', () => {
window.isActive = false;
await testStore.dispatch(NewPostActions.setChannelReadAndViewed(post2, {}, false));
const actions = NewPostActions.setChannelReadAndViewed(testStore.dispatch, testStore.getState, post2, {}, false);
expect(testStore.getActions()).toEqual([{
type: 'MOCK_MARK_CHANNEL_AS_UNREAD',
args: [undefined, channelId, undefined, false],
}]);
expect(actions).toMatchObject([
{
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId,
},
},
]);
expect(testStore.getActions()).toEqual([]);
});
test('should not mark channel as read when not viewing channel', async () => {
test('should not mark channel as read when not viewing channel', () => {
const channelId = 'channel1';
const otherChannelId = 'channel2';
const currentUserId = 'user';
......@@ -213,7 +246,7 @@ describe('actions/new_post', () => {
const post1 = {id: 'post1', channel_id: channelId, create_at: 1000};
const post2 = {id: 'post2', channel_id: channelId, create_at: 2000};
const testStore = await mockStore({
const testStore = mockStore({
entities: {
channels: {
currentChannelId: otherChannelId,
......@@ -248,15 +281,26 @@ describe('actions/new_post', () => {
window.isActive = true;
await testStore.dispatch(NewPostActions.setChannelReadAndViewed(post2, {}, false));
const actions = NewPostActions.setChannelReadAndViewed(testStore.dispatch, testStore.getState, post2, {}, false);
expect(testStore.getActions()).toEqual([{
type: 'MOCK_MARK_CHANNEL_AS_UNREAD',
args: [undefined, channelId, undefined, false],
}]);
expect(actions).toMatchObject([
{
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId,
},
},
]);
expect(testStore.getActions()).toEqual([]);
});
test('should mark channel as read when not viewing channel and post is from current user', async () => {
test('should mark channel as read when not viewing channel and post is from current user', () => {
const channelId = 'channel1';
const otherChannelId = 'channel2';
const currentUserId = 'user';
......@@ -264,7 +308,7 @@ describe('actions/new_post', () => {
const post1 = {id: 'post1', channel_id: channelId, create_at: 1000};
const post2 = {id: 'post2', channel_id: channelId, create_at: 2000, user_id: currentUserId};
const testStore = await mockStore({
const testStore = mockStore({
entities: {
channels: {
currentChannelId: otherChannelId,
......@@ -297,15 +341,31 @@ describe('actions/new_post', () => {
},
});
await testStore.dispatch(NewPostActions.setChannelReadAndViewed(post2, {}, false));
const actions = NewPostActions.setChannelReadAndViewed(testStore.dispatch, testStore.getState, post2, {}, false);
expect(testStore.getActions()).toEqual([{
type: 'MOCK_MARK_CHANNEL_AS_READ',
args: [channelId, undefined, false],
}, {
type: 'MOCK_MARK_CHANNEL_AS_VIEWED',
args: [channelId],
}]);
expect(actions).toMatchObject([
{
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: {
channel_id: channelId,
},
},
]);
// The post is from the current user, so no request should be made to the server to mark it as read
expect(testStore.getActions()).toEqual([]);
});
test('should mark channel as unread when not viewing channel and post is from webhook owned by current user', async () => {
......@@ -316,7 +376,7 @@ describe('actions/new_post', () => {
const post1 = {id: 'post1', channel_id: channelId, create_at: 1000};
const post2 = {id: 'post2', channel_id: channelId, create_at: 2000, props: {from_webhook: 'true'}, user_id: currentUserId};
const testStore = await mockStore({
const testStore = mockStore({
entities: {
channels: {
currentChannelId: otherChannelId,
......@@ -349,22 +409,33 @@ describe('actions/new_post', () => {
},
});
await testStore.dispatch(NewPostActions.setChannelReadAndViewed(post2, {}, false));
const actions = NewPostActions.setChannelReadAndViewed(testStore.dispatch, testStore.getState, post2, {}, false);
expect(testStore.getActions()).toEqual([{
type: 'MOCK_MARK_CHANNEL_AS_UNREAD',
args: [undefined, channelId, undefined, false],
}]);
expect(actions).toMatchObject([
{
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId,
},
},
]);
expect(testStore.getActions()).toEqual([]);
});
test('should not mark channel as read when viewing channel that was marked as unread', async () => {
test('should not mark channel as read when viewing channel that was marked as unread', () => {
const channelId = 'channel1';
const currentUserId = 'user';
const post1 = {id: 'post1', channel_id: channelId, create_at: 1000};
const post2 = {id: 'post2', channel_id: channelId, create_at: 2000};
const testStore = await mockStore({
const testStore = mockStore({
entities: {
channels: {
currentChannelId: channelId,
......@@ -389,12 +460,23 @@ describe('actions/new_post', () => {
},
});
await testStore.dispatch(NewPostActions.setChannelReadAndViewed(post2, {}, false));
const actions = NewPostActions.setChannelReadAndViewed(testStore.dispatch, testStore.getState, post2, {}, false);
expect(testStore.getActions()).toEqual([{
type: 'MOCK_MARK_CHANNEL_AS_UNREAD',
args: [undefined, channelId, undefined, false],
}]);
expect(actions).toMatchObject([
{
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
data: {
channelId,
},
},
{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId,
},
},
]);
expect(testStore.getActions()).toEqual([]);
});
});
});
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as Redux from 'redux';
import {batchActions} from 'redux-batched-actions';
import {
markChannelAsRead,
markChannelAsUnread,
markChannelAsViewed,
actionsToMarkChannelAsRead,
actionsToMarkChannelAsUnread,
actionsToMarkChannelAsViewed,
markChannelAsReadOnServer,
} from 'mattermost-redux/actions/channels';
import * as PostActions from 'mattermost-redux/actions/posts';
......@@ -44,18 +46,17 @@ export function completePostReceive(post: Post, websocketMessageProps: NewPostMe
return result;
}
}
const actions: Redux.AnyAction[] = [];
if (post.channel_id === getCurrentChannelId(getState())) {
dispatch({
actions.push({
type: ActionTypes.INCREASE_POST_VISIBILITY,
data: post.channel_id,
amount: 1,
});
}
// Need manual dispatch to remove pending post
const actions = [
actions.push(
PostActions.receivedNewPost(post),
{
type: WebsocketEvents.STOP_TYPING,
......@@ -65,57 +66,58 @@ export function completePostReceive(post: Post, websocketMessageProps: NewPostMe
now: Date.now(),
},
},
];
...setChannelReadAndViewed(dispatch, getState, post, websocketMessageProps, fetchedChannelMember),
);
dispatch(batchActions(actions));
// Still needed to update unreads
dispatch(setChannelReadAndViewed(post, websocketMessageProps, fetchedChannelMember));
return dispatch(sendDesktopNotification(post, websocketMessageProps) as unknown as ActionFunc);
};
}
export function setChannelReadAndViewed(post: Post, websocketMessageProps: NewPostMessageProps, fetchedChannelMember: boolean): ActionFunc {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const currentUserId = getCurrentUserId(state);
// ignore system message posts, except when added to a team
if (shouldIgnorePost(post, currentUserId)) {
return {data: false};
// setChannelReadAndViewed returns an array of actions to mark the channel read and viewed, and it dispatches an action
// to asynchronously mark the channel as read on the server if necessary.
export function setChannelReadAndViewed(dispatch: DispatchFunc, getState: GetStateFunc, post: Post, websocketMessageProps: NewPostMessageProps, fetchedChannelMember: boolean): Redux.AnyAction[] {
const state = getState();
const currentUserId = getCurrentUserId(state);
// ignore system message posts, except when added to a team
if (shouldIgnorePost(post, currentUserId)) {
return [];
}
let markAsRead = false;
let markAsReadOnServer = false;
// Skip marking a channel as read (when the user is viewing a channel)
// if they have manually marked it as unread.
if (!isManuallyUnread(getState(), post.channel_id)) {
if (
post.user_id === getCurrentUserId(state) &&
!isSystemMessage(post) &&
!isFromWebhook(post)
) {
markAsRead = true;
markAsReadOnServer = false;
} else if (
post.channel_id === getCurrentChannelId(state) &&
window.isActive
) {
markAsRead = true;
markAsReadOnServer = true;
}
}
let markAsRead = false;
let markAsReadOnServer = false;
// Skip marking a channel as read (when the user is viewing a channel)
// if they have manually marked it as unread.
if (!isManuallyUnread(getState(), post.channel_id)) {
if (
post.user_id === getCurrentUserId(state) &&
!isSystemMessage(post) &&
!isFromWebhook(post)
) {
markAsRead = true;
markAsReadOnServer = false;
} else if (
post.channel_id === getCurrentChannelId(state) &&
window.isActive
) {
markAsRead = true;
markAsReadOnServer = true;
}
if (markAsRead) {
if (markAsReadOnServer) {
dispatch(markChannelAsReadOnServer(post.channel_id));
}
if (markAsRead) {
dispatch(markChannelAsRead(post.channel_id, undefined, markAsReadOnServer));
dispatch(markChannelAsViewed(post.channel_id));
} else {
dispatch(markChannelAsUnread(websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions, fetchedChannelMember));
}
return [
...actionsToMarkChannelAsRead(getState, post.channel_id),
...actionsToMarkChannelAsViewed(getState, post.channel_id),
];
}
return {data: true};
};
return actionsToMarkChannelAsUnread(getState, websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions, fetchedChannelMember);
}
......@@ -4,7 +4,7 @@
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import {SearchTypes} from 'mattermost-redux/action_types';
import {ChannelTypes, SearchTypes} from 'mattermost-redux/action_types';
import * as PostActions from 'mattermost-redux/actions/posts';
import {Posts} from 'mattermost-redux/constants';
......@@ -28,6 +28,10 @@ jest.mock('actions/emoji_actions', () => ({
addRecentEmoji: (...args) => ({type: 'MOCK_ADD_RECENT_EMOJI', args}),
}));
jest.mock('actions/notification_actions', () => ({
sendDesktopNotification: jest.fn().mockReturnValue({type: 'MOCK_SEND_DESKTOP_NOTIFICATION'}),
}));
jest.mock('actions/storage', () => {
const original = jest.requireActual('actions/storage');
return {
......@@ -92,6 +96,7 @@ describe('Actions.Posts', () => {
channels: {
current_channel_id: {team_a: 'team_a', id: 'current_channel_id'},
},
manuallyUnread: {},
},
preferences: {
myPreferences: {
......@@ -174,12 +179,18 @@ describe('Actions.Posts', () => {
await testStore.dispatch(Actions.handleNewPost(newPost, msg));
expect(testStore.getActions()).toEqual([
INCREASED_POST_VISIBILITY,
{
meta: {batch: true},
payload: [PostActions.receivedNewPost(newPost), STOP_TYPING],
payload: [
INCREASED_POST_VISIBILITY,
PostActions.receivedNewPost(newPost),
STOP_TYPING,
],
type: 'BATCHING_REDUCER.BATCH',
},
{
type: 'MOCK_SEND_DESKTOP_NOTIFICATION',
},
]);
});
......@@ -201,9 +212,38 @@ describe('Actions.Posts', () => {
now: POST_CREATED_TIME,
userId: newPost.user_id},
},
{
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
data: {
amount: 1,
channelId: 'other_channel_id',
fetchedChannelMember: false,
onlyMentions: undefined,
teamId: undefined,
},
},
{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
amount: 1,
channelId: 'other_channel_id',
},
},
{
type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT,
data: {
amount: 1,
channelId: 'other_channel_id',
fetchedChannelMember: false,
teamId: undefined,
},
},
],
type: 'BATCHING_REDUCER.BATCH',
},
{
type: 'MOCK_SEND_DESKTOP_NOTIFICATION',
},
]);
});
......
......@@ -4,7 +4,7 @@
import {createCategory as createCategoryRedux, moveChannelsToCategory} from 'mattermost-redux/actions/channel_categories';
import {General} from 'mattermost-redux/constants';
import {CategoryTypes} from 'mattermost-redux/constants/channel_categories';
import {getCategory, makeGetChannelsForCategory} from 'mattermost-redux/selectors/entities/channel_categories';
import {getCategory, makeGetChannelIdsForCategory} from 'mattermost-redux/selectors/entities/channel_categories';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
import {insertMultipleWithoutDuplicates} from 'mattermost-redux/utils/array_utils';
......@@ -103,15 +103,14 @@ export function adjustTargetIndexForMove(state: GlobalState, categoryId: string,
}
const category = getCategory(state, categoryId);
const filteredChannels = makeGetChannelsForCategory()(state, category);
const filteredChannelIds = filteredChannels.map((channel) => channel.id);
const filteredChannelIds = makeGetChannelIdsForCategory()(state, category);
// When dragging multiple channels, we don't actually remove all of them from the list as react-beautiful-dnd doesn't support that
// Account for channels removed above the insert point, except the one currently being dragged which is already accounted for by react-beautiful-dnd
const removedChannelsAboveInsert = filteredChannelIds.filter((channel, index) => channel !== draggableChannelId && channelIds.indexOf(channel) !== -1 && index <= targetIndex);
const shiftedIndex = targetIndex - removedChannelsAboveInsert.length;
if (category.channel_ids.length === filteredChannels.length) {
if (category.channel_ids.length === filteredChannelIds.length) {
// There are no archived channels in the category, so the shiftedIndex will be correct
return shiftedIndex;
}
......
......@@ -2461,7 +2461,7 @@ const AdminDefinition = {
placeholder: t('admin.customization.restrictLinkPreviewsExample'),
placeholder_default: 'E.g.: "internal.mycompany.com, images.example.com"',
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource('site')),
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
it.configIsFalse('ServiceSettings', 'EnableLinkPreviews'),
),
},
......
......@@ -4,6 +4,7 @@
import PropTypes from 'prop-types';
import React from 'react';