MM-24895 Load team member and roles in the WebSocket event (#4603)
* Load team member and roles in the WebSocket event * Split WebSocket actions and events into multiple files
This commit is contained in:
parent
be2211a8cc
commit
a6dd6e65ff
23 changed files with 2764 additions and 2335 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
179
app/actions/websocket/channels.test.js
Normal file
179
app/actions/websocket/channels.test.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as TeamActions from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import globalInitialState from '@store/initial_state';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Chanel Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Member Updated', async () => {
|
||||
const channelMember = TestHelper.basicChannelMember;
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const st = mockStore(globalInitialState);
|
||||
await st.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
channelMember.roles = 'channel_user channel_admin';
|
||||
const rolesToLoad = channelMember.roles.split(' ');
|
||||
|
||||
nock(Client4.getRolesRoute()).
|
||||
post('/names', JSON.stringify(rolesToLoad)).
|
||||
reply(200, rolesToLoad);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.CHANNEL_MEMBER_UPDATED,
|
||||
data: {
|
||||
channelMember: JSON.stringify(channelMember),
|
||||
},
|
||||
}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
const storeActions = st.getActions();
|
||||
const batch = storeActions.find((a) => a.type === 'BATCH_WS_CHANNEL_MEMBER_UPDATE');
|
||||
expect(batch).not.toBeNull();
|
||||
const memberAction = batch.payload.find((a) => a.type === ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER);
|
||||
expect(memberAction).not.toBeNull();
|
||||
const rolesActions = batch.payload.find((a) => a.type === RoleTypes.RECEIVED_ROLES);
|
||||
expect(rolesActions).not.toBeNull();
|
||||
expect(rolesActions.data).toEqual(rolesToLoad);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Created', async () => {
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
const channel = {id: channelId, display_name: 'test', name: TestHelper.basicChannel.name};
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: channel});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CHANNEL_CREATED, data: {channel_id: channelId, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: 't36kso9nwtdhbm8dbkd6g4eeby', channel_id: '', team_id: ''}, seq: 57}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels} = entities.channels;
|
||||
|
||||
assert.ok(channels[channel.id]);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Updated', async () => {
|
||||
const channelName = 'Test name';
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CHANNEL_UPDATED, data: {channel: `{"id":"${channelId}","create_at":1508253647983,"update_at":1508254198797,"delete_at":0,"team_id":"55pfercbm7bsmd11p5cjpgsbwr","type":"O","display_name":"${channelName}","name":"${TestHelper.basicChannel.name}","header":"header","purpose":"","last_post_at":1508253648004,"total_msg_count":0,"extra_update_at":1508253648001,"creator_id":"${TestHelper.basicUser.id}"}`}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 62}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels} = entities.channels;
|
||||
|
||||
assert.strictEqual(channels[channelId].display_name, channelName);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Deleted', async () => {
|
||||
const time = Date.now();
|
||||
await store.dispatch(TeamActions.selectTeam(TestHelper.basicTeam));
|
||||
await store.dispatch(ChannelActions.selectChannel(TestHelper.basicChannel.id));
|
||||
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: {id: TestHelper.generateId(), name: General.DEFAULT_CHANNEL, team_id: TestHelper.basicTeam.id, display_name: General.DEFAULT_CHANNEL}});
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: TestHelper.basicChannel});
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${TestHelper.basicTeam.id}/channels/members`).
|
||||
reply(201, [{user_id: TestHelper.basicUser.id, channel_id: TestHelper.basicChannel.id}]);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.CHANNEL_DELETED,
|
||||
data: {
|
||||
channel_id: TestHelper.basicChannel.id,
|
||||
delete_at: time,
|
||||
},
|
||||
broadcast: {
|
||||
omit_users: null,
|
||||
user_id: '',
|
||||
channel_id: '',
|
||||
team_id: TestHelper.basicTeam.id,
|
||||
},
|
||||
seq: 68,
|
||||
}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels, currentChannelId} = entities.channels;
|
||||
|
||||
assert.ok(channels[currentChannelId].name === General.DEFAULT_CHANNEL);
|
||||
});
|
||||
|
||||
it('Websocket Handle Channel Unarchive', async () => {
|
||||
await store.dispatch(TeamActions.selectTeam(TestHelper.basicTeam));
|
||||
await store.dispatch(ChannelActions.selectChannel(TestHelper.basicChannel.id));
|
||||
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: {id: TestHelper.generateId(), name: General.DEFAULT_CHANNEL, team_id: TestHelper.basicTeam.id, display_name: General.DEFAULT_CHANNEL}});
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: TestHelper.basicChannel});
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${TestHelper.basicTeam.id}/channels/members`).
|
||||
reply(201, [{user_id: TestHelper.basicUser.id, channel_id: TestHelper.basicChannel.id}]);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CHANNEL_UNARCHIVE, data: {channel_id: TestHelper.basicChannel.id}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: TestHelper.basicTeam.id}, seq: 68}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const {channels, currentChannelId} = entities.channels;
|
||||
|
||||
assert.ok(channels[currentChannelId].delete_at === 0);
|
||||
});
|
||||
|
||||
it('Websocket Handle Direct Channel', async () => {
|
||||
const channel = {id: TestHelper.generateId(), name: TestHelper.basicUser.id + '__' + TestHelper.generateId(), type: 'D'};
|
||||
|
||||
nock(Client4.getChannelsRoute()).
|
||||
get(`/${channel.id}/members/me`).
|
||||
reply(201, {user_id: TestHelper.basicUser.id, channel_id: channel.id});
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.DIRECT_ADDED, data: {teammate_id: 'btaxe5msnpnqurayosn5p8twuw'}, broadcast: {omit_users: null, user_id: '', channel_id: channel.id, team_id: ''}, seq: 2}));
|
||||
store.dispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: channel});
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const {channels} = store.getState().entities.channels;
|
||||
assert.ok(Object.keys(channels).length);
|
||||
});
|
||||
});
|
||||
238
app/actions/websocket/channels.ts
Normal file
238
app/actions/websocket/channels.ts
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchChannelAndMyMember} from '@actions/helpers/channels';
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {markChannelAsRead} from '@mm-redux/actions/channels';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {ChannelTypes, TeamTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {
|
||||
getAllChannels,
|
||||
getChannel,
|
||||
getChannelsNameMapInTeam,
|
||||
getCurrentChannelId,
|
||||
getRedirectChannelNameForTeam,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import {getChannelByName} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
export function handleChannelConvertedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const channelId = msg.data.channel_id;
|
||||
if (channelId) {
|
||||
const channel = getChannel(getState(), channelId);
|
||||
if (channel) {
|
||||
dispatch({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: {...channel, type: General.PRIVATE_CHANNEL},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelCreatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const {channel_id: channelId, team_id: teamId} = msg.data;
|
||||
const state = getState();
|
||||
const channels = getAllChannels(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
|
||||
if (teamId === currentTeamId && !channels[channelId]) {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
if (channelActions.length) {
|
||||
dispatch(batchActions(channelActions, 'BATCH_WS_CHANNEL_CREATED'));
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelDeletedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const config = getConfig(state);
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_DELETED,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
deleteAt: msg.data.delete_at,
|
||||
team_id: msg.broadcast.team_id,
|
||||
viewArchivedChannels,
|
||||
},
|
||||
}];
|
||||
|
||||
if (msg.broadcast.team_id === currentTeamId) {
|
||||
if (msg.data.channel_id === currentChannelId && !viewArchivedChannels) {
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, currentTeamId);
|
||||
const channel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, currentTeamId));
|
||||
if (channel && channel.id) {
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: channel.id});
|
||||
}
|
||||
EventEmitter.emit(General.DEFAULT_CHANNEL, '');
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CHANNEL_ARCHIVED'));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelMemberUpdatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const channelMember = JSON.parse(msg.data.channelMember);
|
||||
const rolesToLoad = channelMember.roles.split(' ');
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: channelMember,
|
||||
}];
|
||||
|
||||
const roles = await Client4.getRolesByNames(rolesToLoad);
|
||||
if (roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: roles,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CHANNEL_MEMBER_UPDATE'));
|
||||
} catch {
|
||||
//do nothing
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelSchemeUpdatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
if (channelActions.length) {
|
||||
dispatch(batchActions(channelActions, 'BATCH_WS_SCHEME_UPDATE'));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelUnarchiveEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const config = getConfig(state);
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
|
||||
if (msg.broadcast.team_id === currentTeamId) {
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
team_id: msg.data.team_id,
|
||||
deleteAt: 0,
|
||||
viewArchivedChannels,
|
||||
},
|
||||
}];
|
||||
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
||||
if (myData?.channels && myData?.channelMembers) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data: myData,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CHANNEL_UNARCHIVED'));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelUpdatedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
let channel;
|
||||
try {
|
||||
channel = msg.data ? JSON.parse(msg.data.channel) : null;
|
||||
} catch (err) {
|
||||
return {error: err};
|
||||
}
|
||||
|
||||
const currentChannelId = getCurrentChannelId(getState());
|
||||
if (channel) {
|
||||
dispatch({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: channel,
|
||||
});
|
||||
|
||||
if (currentChannelId === channel.id) {
|
||||
// Emit an event with the channel received as we need to handle
|
||||
// the changes without listening to the store
|
||||
EventEmitter.emit(WebsocketEvents.CHANNEL_UPDATED, channel);
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleChannelViewedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const {channel_id: channelId} = msg.data;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
if (channelId !== currentChannelId && currentUserId === msg.broadcast.user_id) {
|
||||
dispatch(markChannelAsRead(channelId, undefined, false));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleDirectAddedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
if (channelActions.length) {
|
||||
dispatch(batchActions(channelActions, 'BATCH_WS_DM_ADDED'));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUpdateMemberRoleEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
const memberData = JSON.parse(msg.data.member);
|
||||
const roles = memberData.roles.split(' ');
|
||||
const actions = [];
|
||||
|
||||
try {
|
||||
const newRoles = await Client4.getRolesByNames(roles);
|
||||
if (newRoles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: newRoles,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_MEMBER,
|
||||
data: memberData,
|
||||
});
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
60
app/actions/websocket/general.test.js
Normal file
60
app/actions/websocket/general.test.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket General Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('handle license changed', async () => {
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.LICENSE_CHANGED, data: {license: {IsLicensed: 'true'}}}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const license = state.entities.general.license;
|
||||
assert.ok(license);
|
||||
assert.ok(license.IsLicensed);
|
||||
});
|
||||
|
||||
it('handle config changed', async () => {
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.CONFIG_CHANGED, data: {config: {EnableCustomEmoji: 'true', EnableLinkPreviews: 'false'}}}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const config = state.entities.general.config;
|
||||
assert.ok(config);
|
||||
assert.ok(config.EnableCustomEmoji === 'true');
|
||||
assert.ok(config.EnableLinkPreviews === 'false');
|
||||
});
|
||||
});
|
||||
27
app/actions/websocket/general.ts
Normal file
27
app/actions/websocket/general.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleConfigChangedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const data = msg.data.config;
|
||||
|
||||
EventEmitter.emit(General.CONFIG_CHANGED, data);
|
||||
return {
|
||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleLicenseChangedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const data = msg.data.license;
|
||||
|
||||
return {
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
23
app/actions/websocket/groups.ts
Normal file
23
app/actions/websocket/groups.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GroupTypes} from '@mm-redux/action_types';
|
||||
import {ActionResult, DispatchFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleGroupUpdatedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc): ActionResult => {
|
||||
const data = JSON.parse(msg.data.group);
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: GroupTypes.RECEIVED_GROUP,
|
||||
data,
|
||||
},
|
||||
{
|
||||
type: GroupTypes.RECEIVED_MY_GROUPS,
|
||||
data: [data],
|
||||
},
|
||||
]));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
403
app/actions/websocket/index.ts
Normal file
403
app/actions/websocket/index.ts
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getPosts} from '@actions/views/post';
|
||||
import {loadMe} from '@actions/views/user';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {ChannelTypes, GeneralTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getUsers, getUserStatuses} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {TeamMembership} from '@mm-redux/types/teams';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import websocketClient from '@websocket';
|
||||
|
||||
import {
|
||||
handleChannelConvertedEvent,
|
||||
handleChannelCreatedEvent,
|
||||
handleChannelDeletedEvent,
|
||||
handleChannelMemberUpdatedEvent,
|
||||
handleChannelSchemeUpdatedEvent,
|
||||
handleChannelUnarchiveEvent,
|
||||
handleChannelUpdatedEvent,
|
||||
handleChannelViewedEvent,
|
||||
handleDirectAddedEvent,
|
||||
handleUpdateMemberRoleEvent,
|
||||
} from './channels';
|
||||
import {handleConfigChangedEvent, handleLicenseChangedEvent} from './general';
|
||||
import {handleGroupUpdatedEvent} from './groups';
|
||||
import {handleOpenDialogEvent} from './integrations';
|
||||
import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
|
||||
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
|
||||
import {handleAddEmoji, handleReactionAddedEvent, handleReactionRemovedEvent} from './reactions';
|
||||
import {handleRoleAddedEvent, handleRoleRemovedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
import {handleLeaveTeamEvent, handleUpdateTeamEvent, handleTeamAddedEvent} from './teams';
|
||||
import {handleStatusChangedEvent, handleUserAddedEvent, handleUserRemovedEvent, handleUserRoleUpdated, handleUserUpdatedEvent} from './users';
|
||||
|
||||
export function init(additionalOptions: any = {}) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const config = getConfig(getState());
|
||||
let connUrl = additionalOptions.websocketUrl || config.WebsocketURL || Client4.getUrl();
|
||||
const authToken = Client4.getToken();
|
||||
|
||||
connUrl += `${Client4.getUrlVersion()}/websocket`;
|
||||
websocketClient.setFirstConnectCallback(() => dispatch(handleFirstConnect()));
|
||||
websocketClient.setEventCallback((evt: WebSocketMessage) => dispatch(handleEvent(evt)));
|
||||
websocketClient.setReconnectCallback(() => dispatch(handleReconnect()));
|
||||
websocketClient.setCloseCallback((connectFailCount: number) => dispatch(handleClose(connectFailCount)));
|
||||
|
||||
const websocketOpts = {
|
||||
connectionUrl: connUrl,
|
||||
...additionalOptions,
|
||||
};
|
||||
|
||||
return websocketClient.initialize(authToken, websocketOpts);
|
||||
};
|
||||
}
|
||||
|
||||
let reconnect = false;
|
||||
export function close(shouldReconnect = false): GenericAction {
|
||||
reconnect = shouldReconnect;
|
||||
websocketClient.close(true);
|
||||
|
||||
return {
|
||||
type: GeneralTypes.WEBSOCKET_CLOSED,
|
||||
timestamp: Date.now(),
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function doFirstConnect(now: number) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
timestamp: now,
|
||||
data: null,
|
||||
}];
|
||||
|
||||
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const users = getUsers(state);
|
||||
const userIds = Object.keys(users);
|
||||
const userUpdates = await Client4.getProfilesByIds(userIds, {since: lastDisconnectAt});
|
||||
|
||||
if (userUpdates.length) {
|
||||
removeUserFromList(currentUserId, userUpdates);
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||
data: userUpdates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_CONNCET'));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function doReconnect(now: number) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const users = getUsers(state);
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
dispatch({
|
||||
type: GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
timestamp: now,
|
||||
data: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const {data: me}: any = await dispatch(loadMe(null, null, true));
|
||||
|
||||
if (!me.error) {
|
||||
const roles = [];
|
||||
|
||||
if (me.roles?.length) {
|
||||
roles.push(...me.roles);
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: PreferenceTypes.RECEIVED_ALL_PREFERENCES,
|
||||
data: me.preferences,
|
||||
}, {
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_UNREADS,
|
||||
data: me.teamUnreads,
|
||||
}, {
|
||||
type: TeamTypes.RECEIVED_TEAMS_LIST,
|
||||
data: me.teams,
|
||||
}, {
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_MEMBERS,
|
||||
data: me.teamMembers,
|
||||
});
|
||||
|
||||
const currentTeamMembership = me.teamMembers.find((tm: TeamMembership) => tm.team_id === currentTeamId && tm.delete_at === 0);
|
||||
|
||||
if (currentTeamMembership) {
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
||||
|
||||
if (myData?.channels && myData?.channelMembers) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data: myData,
|
||||
});
|
||||
|
||||
const stillMemberOfCurrentChannel = myData.channelMembers.find((cm: ChannelMembership) => cm.channel_id === currentChannelId);
|
||||
|
||||
const channelStillExists = myData.channels.find((c: Channel) => c.id === currentChannelId);
|
||||
const config = me.config || getConfig(getState());
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
if (!stillMemberOfCurrentChannel || !channelStillExists || (!viewArchivedChannels && channelStillExists.delete_at !== 0)) {
|
||||
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
|
||||
} else {
|
||||
dispatch(getPosts(currentChannelId));
|
||||
}
|
||||
}
|
||||
|
||||
if (myData.roles?.length) {
|
||||
roles.push(...myData.roles);
|
||||
}
|
||||
} else {
|
||||
// If the user is no longer a member of this team when reconnecting
|
||||
const newMsg = {
|
||||
data: {
|
||||
user_id: currentUserId,
|
||||
team_id: currentTeamId,
|
||||
},
|
||||
};
|
||||
dispatch(handleLeaveTeamEvent(newMsg));
|
||||
}
|
||||
|
||||
if (roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: roles,
|
||||
});
|
||||
}
|
||||
|
||||
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
|
||||
const userIds = Object.keys(users);
|
||||
const userUpdates = await Client4.getProfilesByIds(userIds, {since: lastDisconnectAt});
|
||||
|
||||
if (userUpdates.length) {
|
||||
removeUserFromList(currentUserId, userUpdates);
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||
data: userUpdates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_WS_RECONNECT'));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserTypingEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
if (currentChannelId === msg.broadcast.channel_id) {
|
||||
const profiles = getUsers(state);
|
||||
const statuses = getUserStatuses(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const config = getConfig(state);
|
||||
const userId = msg.data.user_id;
|
||||
|
||||
const data = {
|
||||
id: msg.broadcast.channel_id + msg.data.parent_id,
|
||||
userId,
|
||||
now: Date.now(),
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: WebsocketEvents.TYPING,
|
||||
data,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const newState = getState();
|
||||
const {typing} = newState.entities;
|
||||
|
||||
if (typing && typing[data.id]) {
|
||||
dispatch({
|
||||
type: WebsocketEvents.STOP_TYPING,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}, parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds!, 10));
|
||||
|
||||
if (!profiles[userId] && userId !== currentUserId) {
|
||||
dispatch(getProfilesByIds([userId]));
|
||||
}
|
||||
|
||||
const status = statuses[userId];
|
||||
if (status !== General.ONLINE) {
|
||||
dispatch(getStatusesByIds([userId]));
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
function handleFirstConnect() {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
const now = Date.now();
|
||||
|
||||
if (reconnect) {
|
||||
reconnect = false;
|
||||
return dispatch(doReconnect(now));
|
||||
}
|
||||
return dispatch(doFirstConnect(now));
|
||||
};
|
||||
}
|
||||
|
||||
function handleReconnect() {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(doReconnect(Date.now()));
|
||||
};
|
||||
}
|
||||
|
||||
function handleClose(connectFailCount: number) {
|
||||
return {
|
||||
type: GeneralTypes.WEBSOCKET_FAILURE,
|
||||
error: connectFailCount,
|
||||
data: null,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function handleEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
return dispatch(handleNewPostEvent(msg));
|
||||
case WebsocketEvents.POST_EDITED:
|
||||
return dispatch(handlePostEdited(msg));
|
||||
case WebsocketEvents.POST_DELETED:
|
||||
return dispatch(handlePostDeleted(msg));
|
||||
case WebsocketEvents.POST_UNREAD:
|
||||
return dispatch(handlePostUnread(msg));
|
||||
case WebsocketEvents.LEAVE_TEAM:
|
||||
return dispatch(handleLeaveTeamEvent(msg));
|
||||
case WebsocketEvents.UPDATE_TEAM:
|
||||
return dispatch(handleUpdateTeamEvent(msg));
|
||||
case WebsocketEvents.ADDED_TO_TEAM:
|
||||
return dispatch(handleTeamAddedEvent(msg));
|
||||
case WebsocketEvents.USER_ADDED:
|
||||
return dispatch(handleUserAddedEvent(msg));
|
||||
case WebsocketEvents.USER_REMOVED:
|
||||
return dispatch(handleUserRemovedEvent(msg));
|
||||
case WebsocketEvents.USER_UPDATED:
|
||||
return dispatch(handleUserUpdatedEvent(msg));
|
||||
case WebsocketEvents.ROLE_ADDED:
|
||||
return dispatch(handleRoleAddedEvent(msg));
|
||||
case WebsocketEvents.ROLE_REMOVED:
|
||||
return dispatch(handleRoleRemovedEvent(msg));
|
||||
case WebsocketEvents.ROLE_UPDATED:
|
||||
return dispatch(handleRoleUpdatedEvent(msg));
|
||||
case WebsocketEvents.USER_ROLE_UPDATED:
|
||||
return dispatch(handleUserRoleUpdated(msg));
|
||||
case WebsocketEvents.MEMBERROLE_UPDATED:
|
||||
return dispatch(handleUpdateMemberRoleEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_CREATED:
|
||||
return dispatch(handleChannelCreatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_DELETED:
|
||||
return dispatch(handleChannelDeletedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_UNARCHIVED:
|
||||
return dispatch(handleChannelUnarchiveEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_UPDATED:
|
||||
return dispatch(handleChannelUpdatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_CONVERTED:
|
||||
return dispatch(handleChannelConvertedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_VIEWED:
|
||||
return dispatch(handleChannelViewedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
|
||||
return dispatch(handleChannelMemberUpdatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_SCHEME_UPDATED:
|
||||
return dispatch(handleChannelSchemeUpdatedEvent(msg));
|
||||
case WebsocketEvents.DIRECT_ADDED:
|
||||
return dispatch(handleDirectAddedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCE_CHANGED:
|
||||
return dispatch(handlePreferenceChangedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCES_CHANGED:
|
||||
return dispatch(handlePreferencesChangedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCES_DELETED:
|
||||
return dispatch(handlePreferencesDeletedEvent(msg));
|
||||
case WebsocketEvents.STATUS_CHANGED:
|
||||
return dispatch(handleStatusChangedEvent(msg));
|
||||
case WebsocketEvents.TYPING:
|
||||
return dispatch(handleUserTypingEvent(msg));
|
||||
case WebsocketEvents.HELLO:
|
||||
handleHelloEvent(msg);
|
||||
break;
|
||||
case WebsocketEvents.REACTION_ADDED:
|
||||
return dispatch(handleReactionAddedEvent(msg));
|
||||
case WebsocketEvents.REACTION_REMOVED:
|
||||
return dispatch(handleReactionRemovedEvent(msg));
|
||||
case WebsocketEvents.EMOJI_ADDED:
|
||||
return dispatch(handleAddEmoji(msg));
|
||||
case WebsocketEvents.LICENSE_CHANGED:
|
||||
return dispatch(handleLicenseChangedEvent(msg));
|
||||
case WebsocketEvents.CONFIG_CHANGED:
|
||||
return dispatch(handleConfigChangedEvent(msg));
|
||||
case WebsocketEvents.OPEN_DIALOG:
|
||||
return dispatch(handleOpenDialogEvent(msg));
|
||||
case WebsocketEvents.RECEIVED_GROUP:
|
||||
return dispatch(handleGroupUpdatedEvent(msg));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
function handleHelloEvent(msg: WebSocketMessage) {
|
||||
const serverVersion = msg.data.server_version;
|
||||
if (serverVersion && Client4.serverVersion !== serverVersion) {
|
||||
Client4.serverVersion = serverVersion;
|
||||
EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
let lastTimeTypingSent = 0;
|
||||
export function userTyping(state: GlobalState, channelId: string, parentPostId: string): void {
|
||||
const config = getConfig(state);
|
||||
const t = Date.now();
|
||||
const stats = getCurrentChannelStats(state);
|
||||
const membersInChannel = stats ? stats.member_count : 0;
|
||||
|
||||
if (((t - lastTimeTypingSent) > parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds!, 10)) &&
|
||||
(membersInChannel < parseInt(config.MaxNotificationsPerChannel!, 10)) && (config.EnableUserTypingMessages === 'true')) {
|
||||
websocketClient.userTyping(channelId, parentPostId);
|
||||
lastTimeTypingSent = t;
|
||||
}
|
||||
}
|
||||
47
app/actions/websocket/integrations.test.js
Normal file
47
app/actions/websocket/integrations.test.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Integration Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('handle open dialog', async () => {
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.OPEN_DIALOG, data: {dialog: JSON.stringify({url: 'someurl', trigger_id: 'sometriggerid', dialog: {}})}}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const dialog = state.entities.integrations.dialog;
|
||||
assert.ok(dialog);
|
||||
assert.ok(dialog.url === 'someurl');
|
||||
assert.ok(dialog.trigger_id === 'sometriggerid');
|
||||
assert.ok(dialog.dialog);
|
||||
});
|
||||
});
|
||||
14
app/actions/websocket/integrations.ts
Normal file
14
app/actions/websocket/integrations.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {ActionResult, DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleOpenDialogEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc): ActionResult => {
|
||||
const data = (msg.data && msg.data.dialog) || {};
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: JSON.parse(data)});
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
265
app/actions/websocket/posts.test.js
Normal file
265
app/actions/websocket/posts.test.js
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as PostActions from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts} from '@mm-redux/constants';
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Post Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle New Post if post does not exist', async () => {
|
||||
PostSelectors.getPost = jest.fn();
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
const message = JSON.stringify({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/users/ids').
|
||||
reply(200, [TestHelper.basicUser.id]);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/users/status/ids').
|
||||
reply(200, [{user_id: TestHelper.basicUser.id, status: 'online', manual: false, last_activity_at: 1507662212199}]);
|
||||
|
||||
// Mock that post already exists and check it is not added
|
||||
PostSelectors.getPost.mockReturnValueOnce(true);
|
||||
mockServer.emit('message', message);
|
||||
let entities = store.getState().entities;
|
||||
let posts = entities.posts.posts;
|
||||
assert.deepEqual(posts, {});
|
||||
|
||||
// Mock that post does not exist and check it is added
|
||||
PostSelectors.getPost.mockReturnValueOnce(false);
|
||||
mockServer.emit('message', message);
|
||||
await TestHelper.wait(100);
|
||||
entities = store.getState().entities;
|
||||
posts = entities.posts.posts;
|
||||
const postId = Object.keys(posts)[0];
|
||||
assert.ok(posts[postId].message.indexOf('Unit Test') > -1);
|
||||
entities = store.getState().entities;
|
||||
});
|
||||
|
||||
it('Websocket Handle New Post emits INCREASE_POST_VISIBILITY_BY_ONE for current channel when post does not exist', async () => {
|
||||
PostSelectors.getPost = jest.fn();
|
||||
const emit = jest.spyOn(EventEmitter, 'emit');
|
||||
const currentChannelId = TestHelper.generateId();
|
||||
const otherChannelId = TestHelper.generateId();
|
||||
const messageFor = (channelId) => ({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
|
||||
|
||||
await store.dispatch(ChannelActions.selectChannel(currentChannelId));
|
||||
await TestHelper.wait(100);
|
||||
|
||||
// Post does not exist and is not for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(false);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(otherChannelId)));
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
|
||||
// Post exists and is not for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(true);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(otherChannelId)));
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
|
||||
// Post exists and is for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(true);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(currentChannelId)));
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
|
||||
// Post does not exist and is for current channel
|
||||
PostSelectors.getPost.mockReturnValueOnce(false);
|
||||
mockServer.emit('message', JSON.stringify(messageFor(currentChannelId)));
|
||||
expect(emit).toHaveBeenCalledWith(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE);
|
||||
});
|
||||
|
||||
it('Websocket Handle New Post if status is manually set do not set to online', async () => {
|
||||
const userId = TestHelper.generateId();
|
||||
|
||||
store = await configureStore({
|
||||
entities: {
|
||||
users: {
|
||||
statuses: {
|
||||
[userId]: General.DND,
|
||||
},
|
||||
isManualStatus: {
|
||||
[userId]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
|
||||
const channelId = TestHelper.basicChannel.id;
|
||||
const message = JSON.stringify({
|
||||
event: WebsocketEvents.POSTED,
|
||||
data: {
|
||||
channel_display_name: TestHelper.basicChannel.display_name,
|
||||
channel_name: TestHelper.basicChannel.name,
|
||||
channel_type: 'O',
|
||||
post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${userId}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`,
|
||||
sender_name: TestHelper.basicUser.username,
|
||||
team_id: TestHelper.basicTeam.id,
|
||||
},
|
||||
broadcast: {
|
||||
omit_users: null,
|
||||
user_id: userId,
|
||||
channel_id: channelId,
|
||||
team_id: '',
|
||||
},
|
||||
seq: 2,
|
||||
});
|
||||
|
||||
mockServer.emit('message', message);
|
||||
const entities = store.getState().entities;
|
||||
const statuses = entities.users.statuses;
|
||||
assert.equal(statuses[userId], General.DND);
|
||||
});
|
||||
|
||||
it('Websocket Handle Post Edited', async () => {
|
||||
const post = {id: '71k8gz5ompbpfkrzaxzodffj8w'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_EDITED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1585236976007,"edit_at": 1585236976007,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${TestHelper.basicChannel.id}","root_id": "","parent_id": "","original_id": "","message": "Unit Test (edited)","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 2}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const {posts} = store.getState().entities.posts;
|
||||
assert.ok(posts);
|
||||
assert.ok(posts[post.id]);
|
||||
assert.ok(posts[post.id].message.indexOf('(edited)') > -1);
|
||||
});
|
||||
|
||||
it('Websocket Handle Post Deleted', async () => {
|
||||
const post = TestHelper.fakePost();
|
||||
post.channel_id = TestHelper.basicChannel.id;
|
||||
|
||||
post.id = '71k8gz5ompbpfkrzaxzodffj8w';
|
||||
store.dispatch(PostActions.receivedPost(post));
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_DELETED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1508247709215,"edit_at": 1508247709215,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${post.channel_id}","root_id": "","parent_id": "","original_id": "","message": "Unit Test","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 7}));
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const {posts} = entities.posts;
|
||||
assert.strictEqual(posts[post.id].state, Posts.POST_DELETED);
|
||||
});
|
||||
|
||||
it('Websocket handle Post Unread', async () => {
|
||||
const teamId = TestHelper.generateId();
|
||||
const channelId = TestHelper.generateId();
|
||||
const userId = TestHelper.generateId();
|
||||
|
||||
store = await configureStore({
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
[channelId]: {id: channelId},
|
||||
},
|
||||
myMembers: {
|
||||
[channelId]: {msg_count: 10, mention_count: 0, last_viewed_at: 0},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
myMembers: {
|
||||
[teamId]: {msg_count: 10, mention_count: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.POST_UNREAD,
|
||||
data: {
|
||||
last_viewed_at: 25,
|
||||
msg_count: 3,
|
||||
mention_count: 2,
|
||||
delta_msg: 7,
|
||||
},
|
||||
broadcast: {omit_users: null, user_id: userId, channel_id: channelId, team_id: teamId},
|
||||
seq: 7,
|
||||
}));
|
||||
|
||||
const state = store.getState();
|
||||
assert.equal(state.entities.channels.manuallyUnread[channelId], true);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].msg_count, 3);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].mention_count, 2);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].last_viewed_at, 25);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].msg_count, 3);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].mention_count, 2);
|
||||
});
|
||||
|
||||
it('Websocket handle Post Unread When marked on the same client', async () => {
|
||||
const teamId = TestHelper.generateId();
|
||||
const channelId = TestHelper.generateId();
|
||||
const userId = TestHelper.generateId();
|
||||
|
||||
store = await configureStore({
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
[channelId]: {id: channelId},
|
||||
},
|
||||
myMembers: {
|
||||
[channelId]: {msg_count: 5, mention_count: 4, last_viewed_at: 14},
|
||||
},
|
||||
manuallyUnread: {
|
||||
[channelId]: true,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
myMembers: {
|
||||
[teamId]: {msg_count: 5, mention_count: 4},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
|
||||
mockServer.emit('message', JSON.stringify({
|
||||
event: WebsocketEvents.POST_UNREAD,
|
||||
data: {
|
||||
last_viewed_at: 25,
|
||||
msg_count: 5,
|
||||
mention_count: 4,
|
||||
delta_msg: 1,
|
||||
},
|
||||
broadcast: {omit_users: null, user_id: userId, channel_id: channelId, team_id: teamId},
|
||||
seq: 17,
|
||||
}));
|
||||
|
||||
const state = store.getState();
|
||||
assert.equal(state.entities.channels.manuallyUnread[channelId], true);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].msg_count, 5);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].mention_count, 4);
|
||||
assert.equal(state.entities.channels.myMembers[channelId].last_viewed_at, 14);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].msg_count, 5);
|
||||
assert.equal(state.entities.teams.myMembers[teamId].mention_count, 4);
|
||||
});
|
||||
});
|
||||
209
app/actions/websocket/posts.ts
Normal file
209
app/actions/websocket/posts.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
fetchMyChannel,
|
||||
fetchMyChannelMember,
|
||||
makeDirectChannelVisibleIfNecessary,
|
||||
makeGroupMessageVisibleIfNecessary,
|
||||
markChannelAsUnread,
|
||||
} from '@actions/helpers/channels';
|
||||
import {markAsViewedAndReadBatch} from '@actions/views/channel';
|
||||
import {getPostsAdditionalDataBatch, getPostThread} from '@actions/views/post';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {ChannelTypes} from '@mm-redux/action_types';
|
||||
import {getUnreadPostData, postDeleted, receivedNewPost, receivedPost} from '@mm-redux/actions/posts';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {
|
||||
getChannel,
|
||||
getCurrentChannelId,
|
||||
getMyChannelMember as selectMyChannelMember,
|
||||
isManuallyUnread,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getPost as selectPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {isFromWebhook, isSystemMessage, shouldIgnorePost} from '@mm-redux/utils/post_utils';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleNewPostEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const data = JSON.parse(msg.data.post);
|
||||
const post = {
|
||||
...data,
|
||||
ownPost: data.user_id === currentUserId,
|
||||
};
|
||||
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
const exists = selectPost(state, post.pending_post_id);
|
||||
|
||||
if (!exists) {
|
||||
if (getCurrentChannelId(state) === post.channel_id) {
|
||||
EventEmitter.emit(WebsocketEvents.INCREASE_POST_VISIBILITY_BY_ONE);
|
||||
}
|
||||
|
||||
const myChannel = getChannel(state, post.channel_id);
|
||||
if (!myChannel) {
|
||||
const channel = await fetchMyChannel(post.channel_id);
|
||||
if (channel.data) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: channel.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const myChannelMember = selectMyChannelMember(state, post.channel_id);
|
||||
if (!myChannelMember) {
|
||||
const member = await fetchMyChannelMember(post.channel_id);
|
||||
if (member.data) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: member.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
actions.push(receivedNewPost(post));
|
||||
|
||||
// If we don't have the thread for this post, fetch it from the server
|
||||
// and include the actions in the batch
|
||||
if (post.root_id) {
|
||||
const rootPost = selectPost(state, post.root_id);
|
||||
|
||||
if (!rootPost) {
|
||||
const thread: any = await dispatch(getPostThread(post.root_id, true));
|
||||
if (thread.data?.length) {
|
||||
actions.push(...thread.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (post.channel_id === currentChannelId) {
|
||||
const id = post.channel_id + post.root_id;
|
||||
const {typing} = state.entities;
|
||||
|
||||
if (typing[id]) {
|
||||
actions.push({
|
||||
type: WebsocketEvents.STOP_TYPING,
|
||||
data: {
|
||||
id,
|
||||
userId: post.user_id,
|
||||
now: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and batch additional post data
|
||||
const additional: any = await dispatch(getPostsAdditionalDataBatch([post]));
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
if (msg.data.channel_type === General.DM_CHANNEL) {
|
||||
const otherUserId = getUserIdFromChannelName(currentUserId, msg.data.channel_name);
|
||||
const dmAction = makeDirectChannelVisibleIfNecessary(state, otherUserId);
|
||||
if (dmAction) {
|
||||
actions.push(dmAction);
|
||||
}
|
||||
} else if (msg.data.channel_type === General.GM_CHANNEL) {
|
||||
const gmActions = await makeGroupMessageVisibleIfNecessary(state, post.channel_id);
|
||||
if (gmActions) {
|
||||
actions.push(...gmActions);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldIgnorePost(post)) {
|
||||
let markAsRead = false;
|
||||
let markAsReadOnServer = false;
|
||||
|
||||
if (!isManuallyUnread(state, post.channel_id)) {
|
||||
if (
|
||||
post.user_id === getCurrentUserId(state) &&
|
||||
!isSystemMessage(post) &&
|
||||
!isFromWebhook(post)
|
||||
) {
|
||||
markAsRead = true;
|
||||
markAsReadOnServer = false;
|
||||
} else if (post.channel_id === currentChannelId) {
|
||||
markAsRead = true;
|
||||
markAsReadOnServer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (markAsRead) {
|
||||
const readActions = markAsViewedAndReadBatch(state, post.channel_id, undefined, markAsReadOnServer);
|
||||
actions.push(...readActions);
|
||||
} else {
|
||||
const unreadActions = markChannelAsUnread(state, msg.data.team_id, post.channel_id, msg.data.mentions);
|
||||
actions.push(...unreadActions);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_NEW_POST'));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePostEdited(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const data = JSON.parse(msg.data.post);
|
||||
const post = {
|
||||
...data,
|
||||
ownPost: data.user_id === currentUserId,
|
||||
};
|
||||
const actions = [receivedPost(post)];
|
||||
|
||||
const additional: any = await dispatch(getPostsAdditionalDataBatch([post]));
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_POST_EDITED'));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePostDeleted(msg: WebSocketMessage): GenericAction {
|
||||
const data = JSON.parse(msg.data.post);
|
||||
|
||||
return postDeleted(data);
|
||||
}
|
||||
|
||||
export function handlePostUnread(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
const manual = isManuallyUnread(state, msg.broadcast.channel_id);
|
||||
|
||||
if (!manual) {
|
||||
const member = selectMyChannelMember(state, msg.broadcast.channel_id);
|
||||
const delta = member ? member.msg_count - msg.data.msg_count : msg.data.msg_count;
|
||||
const info = {
|
||||
...msg.data,
|
||||
user_id: msg.broadcast.user_id,
|
||||
team_id: msg.broadcast.team_id,
|
||||
channel_id: msg.broadcast.channel_id,
|
||||
deltaMsgs: delta,
|
||||
};
|
||||
const data = getUnreadPostData(info, state);
|
||||
dispatch({
|
||||
type: ChannelTypes.POST_UNREAD_SUCCESS,
|
||||
data,
|
||||
});
|
||||
return {data};
|
||||
}
|
||||
|
||||
return {data: null};
|
||||
};
|
||||
}
|
||||
60
app/actions/websocket/preferences.ts
Normal file
60
app/actions/websocket/preferences.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getAddedDmUsersIfNecessary} from '@actions/helpers/channels';
|
||||
import {getPost} from '@actions/views/post';
|
||||
import {PreferenceTypes} from '@mm-redux/action_types';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getAllPosts} from '@mm-redux/selectors/entities/posts';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {PreferenceType} from '@mm-redux/types/preferences';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handlePreferenceChangedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const preference = JSON.parse(msg.data.preference);
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: [preference],
|
||||
}];
|
||||
|
||||
const dmActions = await getAddedDmUsersIfNecessary(getState(), [preference]);
|
||||
if (dmActions.length) {
|
||||
actions.push(...dmActions);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_PREFERENCE_CHANGED'));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePreferencesChangedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const preferences: PreferenceType[] = JSON.parse(msg.data.preferences);
|
||||
const posts = getAllPosts(getState());
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: preferences,
|
||||
}];
|
||||
|
||||
preferences.forEach((pref) => {
|
||||
if (pref.category === Preferences.CATEGORY_FLAGGED_POST && !posts[pref.name]) {
|
||||
dispatch(getPost(pref.name));
|
||||
}
|
||||
});
|
||||
|
||||
const dmActions = await getAddedDmUsersIfNecessary(getState(), preferences);
|
||||
if (dmActions.length) {
|
||||
actions.push(...dmActions);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_PREFERENCES_CHANGED'));
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePreferencesDeletedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const preferences = JSON.parse(msg.data.preferences);
|
||||
|
||||
return {type: PreferenceTypes.DELETED_PREFERENCES, data: preferences};
|
||||
}
|
||||
60
app/actions/websocket/reactions.test.js
Normal file
60
app/actions/websocket/reactions.test.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Reaction Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle Reaction Added to Post', async () => {
|
||||
const emoji = '+1';
|
||||
const post = {id: 'w7yo9377zbfi9mgiq5gbfpn3ha'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.REACTION_ADDED, data: {reaction: `{"user_id":"${TestHelper.basicUser.id}","post_id":"w7yo9377zbfi9mgiq5gbfpn3ha","emoji_name":"${emoji}","create_at":1508249125852}`}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 12}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
const nextEntities = store.getState().entities;
|
||||
const {reactions} = nextEntities.posts;
|
||||
const reactionsForPost = reactions[post.id];
|
||||
|
||||
assert.ok(reactionsForPost.hasOwnProperty(`${TestHelper.basicUser.id}-${emoji}`));
|
||||
});
|
||||
|
||||
it('Websocket handle emoji added', async () => {
|
||||
const created = {id: '1mmgakhhupfgfm8oug6pooc5no'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.EMOJI_ADDED, data: {emoji: `{"id":"1mmgakhhupfgfm8oug6pooc5no","create_at":1508263941321,"update_at":1508263941321,"delete_at":0,"creator_id":"t36kso9nwtdhbm8dbkd6g4eeby","name":"${TestHelper.generateId()}"}`}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 2}));
|
||||
|
||||
await TestHelper.wait(200);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
const emojis = state.entities.emojis.customEmoji;
|
||||
assert.ok(emojis);
|
||||
assert.ok(emojis[created.id]);
|
||||
});
|
||||
});
|
||||
41
app/actions/websocket/reactions.ts
Normal file
41
app/actions/websocket/reactions.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {EmojiTypes, PostTypes} from '@mm-redux/action_types';
|
||||
import {getCustomEmojiForReaction} from '@mm-redux/actions/posts';
|
||||
import {ActionResult, DispatchFunc, GenericAction} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleAddEmoji(msg: WebSocketMessage): GenericAction {
|
||||
const data = JSON.parse(msg.data.emoji);
|
||||
|
||||
return {
|
||||
type: EmojiTypes.RECEIVED_CUSTOM_EMOJI,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleReactionAddedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc): ActionResult => {
|
||||
const {data} = msg;
|
||||
const reaction = JSON.parse(data.reaction);
|
||||
|
||||
dispatch(getCustomEmojiForReaction(reaction.emoji_name));
|
||||
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_REACTION,
|
||||
data: reaction,
|
||||
});
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleReactionRemovedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const {data} = msg;
|
||||
const reaction = JSON.parse(data.reaction);
|
||||
|
||||
return {
|
||||
type: PostTypes.REACTION_DELETED,
|
||||
data: reaction,
|
||||
};
|
||||
}
|
||||
33
app/actions/websocket/roles.ts
Normal file
33
app/actions/websocket/roles.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RoleTypes} from '@mm-redux/action_types';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleRoleAddedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const role = JSON.parse(msg.data.role);
|
||||
|
||||
return {
|
||||
type: RoleTypes.RECEIVED_ROLE,
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleRoleRemovedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const role = JSON.parse(msg.data.role);
|
||||
|
||||
return {
|
||||
type: RoleTypes.ROLE_DELETED,
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleRoleUpdatedEvent(msg: WebSocketMessage): GenericAction {
|
||||
const role = JSON.parse(msg.data.role);
|
||||
|
||||
return {
|
||||
type: RoleTypes.RECEIVED_ROLE,
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
106
app/actions/websocket/teams.test.js
Normal file
106
app/actions/websocket/teams.test.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket Team Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
store.dispatch(batchActions([
|
||||
{type: UserTypes.RECEIVED_ME, data: TestHelper.basicUser},
|
||||
{type: TeamTypes.RECEIVED_TEAM, data: TestHelper.basicTeam},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, data: TestHelper.basicTeamMember},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_UNREADS, data: [TestHelper.basicTeamMember]},
|
||||
]));
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
// If we move this test lower it will fail cause of a permissions issue
|
||||
it('Websocket handle team updated', async () => {
|
||||
const team = {id: '55pfercbm7bsmd11p5cjpgsbwr'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.UPDATE_TEAM, data: {team: `{"id":"55pfercbm7bsmd11p5cjpgsbwr","create_at":1495553950859,"update_at":1508250370054,"delete_at":0,"display_name":"${TestHelper.basicTeam.display_name}","name":"${TestHelper.basicTeam.name}","description":"description","email":"","type":"O","company_name":"","allowed_domains":"","invite_id":"m93f54fu5bfntewp8ctwonw19w","allow_open_invite":true}`}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 26}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const {teams} = entities.teams;
|
||||
const updated = teams[team.id];
|
||||
assert.ok(updated);
|
||||
assert.strictEqual(updated.allow_open_invite, true);
|
||||
});
|
||||
|
||||
it('Websocket handle team patched', async () => {
|
||||
const team = {id: '55pfercbm7bsmd11p5cjpgsbwr'};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.UPDATE_TEAM, data: {team: `{"id":"55pfercbm7bsmd11p5cjpgsbwr","create_at":1495553950859,"update_at":1508250370054,"delete_at":0,"display_name":"${TestHelper.basicTeam.display_name}","name":"${TestHelper.basicTeam.name}","description":"description","email":"","type":"O","company_name":"","allowed_domains":"","invite_id":"m93f54fu5bfntewp8ctwonw19w","allow_open_invite":true}`}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 26}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const {teams} = entities.teams;
|
||||
const updated = teams[team.id];
|
||||
assert.ok(updated);
|
||||
assert.strictEqual(updated.allow_open_invite, true);
|
||||
});
|
||||
|
||||
it('Websocket handle user added to team', async () => {
|
||||
const team = TestHelper.basicTeam;
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get(`/teams/${team.id}`).
|
||||
reply(200, team);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/me/teams/unread').
|
||||
reply(200, [{team_id: team.id, msg_count: 0, mention_count: 0}]);
|
||||
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.ADDED_TO_TEAM, data: {team_id: team.id, user_id: TestHelper.basicUser.id}, broadcast: {omit_users: null, user_id: TestHelper.basicUser.id, channel_id: '', team_id: ''}, seq: 2}));
|
||||
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const {teams, myMembers} = store.getState().entities.teams;
|
||||
assert.ok(teams[team.id]);
|
||||
assert.ok(myMembers[team.id]);
|
||||
|
||||
const member = myMembers[team.id];
|
||||
assert.ok(member.hasOwnProperty('mention_count'));
|
||||
});
|
||||
|
||||
it('WebSocket Leave Team', async () => {
|
||||
const team = TestHelper.basicTeam;
|
||||
store.dispatch(batchActions([
|
||||
{type: UserTypes.RECEIVED_ME, data: TestHelper.basicUser},
|
||||
{type: TeamTypes.RECEIVED_TEAM, data: TestHelper.basicTeam},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, data: TestHelper.basicTeamMember},
|
||||
]));
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.LEAVE_TEAM, data: {team_id: team.id, user_id: TestHelper.basicUser.id}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: team.id}, seq: 35}));
|
||||
|
||||
const {myMembers} = store.getState().entities.teams;
|
||||
assert.ifError(myMembers[team.id]);
|
||||
});
|
||||
});
|
||||
106
app/actions/websocket/teams.ts
Normal file
106
app/actions/websocket/teams.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RoleTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getCurrentTeamId, getTeams as getTeamsSelector} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {isGuest} from '@mm-redux/utils/user_utils';
|
||||
|
||||
export function handleLeaveTeamEvent(msg: Partial<WebSocketMessage>) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const teams = getTeamsSelector(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
if (currentUser.id === msg.data.user_id) {
|
||||
const actions: Array<GenericAction> = [{type: TeamTypes.LEAVE_TEAM, data: teams[msg.data.team_id]}];
|
||||
if (isGuest(currentUser.roles)) {
|
||||
const notVisible = await notVisibleUsersActions(state);
|
||||
if (notVisible.length) {
|
||||
actions.push(...notVisible);
|
||||
}
|
||||
}
|
||||
dispatch(batchActions(actions, 'BATCH_WS_LEAVE_TEAM'));
|
||||
|
||||
// if they are on the team being removed deselect the current team and channel
|
||||
if (currentTeamId === msg.data.team_id) {
|
||||
EventEmitter.emit('leave_team');
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUpdateTeamEvent(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: TeamTypes.UPDATED_TEAM,
|
||||
data: JSON.parse(msg.data.team),
|
||||
};
|
||||
}
|
||||
|
||||
export function handleTeamAddedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const teamId = msg.data.team_id;
|
||||
const userId = msg.data.user_id;
|
||||
const [team, member, teamUnreads] = await Promise.all([
|
||||
Client4.getTeam(msg.data.team_id),
|
||||
Client4.getTeamMember(teamId, userId),
|
||||
Client4.getMyTeamUnreads(),
|
||||
]);
|
||||
|
||||
const actions = [];
|
||||
if (team) {
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_TEAM,
|
||||
data: team,
|
||||
});
|
||||
|
||||
if (member) {
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_MEMBER,
|
||||
data: member,
|
||||
});
|
||||
|
||||
if (member.roles) {
|
||||
const rolesToLoad = new Set<string>();
|
||||
for (const role of member.roles.split(' ')) {
|
||||
rolesToLoad.add(role);
|
||||
}
|
||||
|
||||
if (rolesToLoad.size > 0) {
|
||||
const roles = await Client4.getRolesByNames(Array.from(rolesToLoad));
|
||||
if (roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: roles,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (teamUnreads) {
|
||||
actions.push({
|
||||
type: TeamTypes.RECEIVED_MY_TEAM_UNREADS,
|
||||
data: teamUnreads,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_WS_TEAM_ADDED'));
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
94
app/actions/websocket/users.test.js
Normal file
94
app/actions/websocket/users.test.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
describe('Websocket User Events', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
store.dispatch(batchActions([
|
||||
{type: UserTypes.RECEIVED_ME, data: TestHelper.basicUser},
|
||||
{type: TeamTypes.RECEIVED_TEAM, data: TestHelper.basicTeam},
|
||||
{type: TeamTypes.RECEIVED_MY_TEAM_MEMBER, data: TestHelper.basicTeamMember},
|
||||
]));
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('Websocket Handle User Added', async () => {
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_ADDED, data: {team_id: TestHelper.basicTeam.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
const entities = store.getState().entities;
|
||||
const profilesInChannel = entities.users.profilesInChannel;
|
||||
assert.ok(profilesInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
});
|
||||
|
||||
it('Websocket Handle User Removed', async () => {
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_REMOVED, data: {remover_id: TestHelper.basicUser.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const profilesNotInChannel = entities.users.profilesNotInChannel;
|
||||
|
||||
assert.ok(profilesNotInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
});
|
||||
|
||||
it('Websocket Handle User Removed when Current is Guest', async () => {
|
||||
const basicGuestUser = TestHelper.fakeUserWithId();
|
||||
basicGuestUser.roles = 'system_guest';
|
||||
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
|
||||
// add user first
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_ADDED, data: {team_id: TestHelper.basicTeam.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
assert.ok(store.getState().entities.users.profilesInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
|
||||
// remove user
|
||||
store.dispatch({type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, data: {id: TestHelper.basicChannel.id, user_id: user.id}});
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_REMOVED, data: {remover_id: basicGuestUser.id, user_id: user.id}, broadcast: {omit_users: null, user_id: '', channel_id: TestHelper.basicChannel.id, team_id: ''}, seq: 42}));
|
||||
|
||||
assert.ok(!store.getState().entities.users.profilesInChannel[TestHelper.basicChannel.id].has(user.id));
|
||||
});
|
||||
|
||||
it('Websocket Handle User Updated', async () => {
|
||||
const user = {...TestHelper.fakeUser(), id: TestHelper.generateId()};
|
||||
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.USER_UPDATED, data: {user: {id: user.id, create_at: 1495570297229, update_at: 1508253268652, delete_at: 0, username: 'tim', auth_data: '', auth_service: '', email: 'tim@bladekick.com', nickname: '', first_name: 'tester4', last_name: '', position: '', roles: 'system_user', locale: 'en'}}, broadcast: {omit_users: null, user_id: '', channel_id: '', team_id: ''}, seq: 53}));
|
||||
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
const entities = state.entities;
|
||||
const profiles = entities.users.profiles;
|
||||
|
||||
assert.strictEqual(profiles[user.id].first_name, 'tester4');
|
||||
});
|
||||
});
|
||||
});
|
||||
204
app/actions/websocket/users.ts
Normal file
204
app/actions/websocket/users.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchChannelAndMyMember} from '@actions/helpers/channels';
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getMe} from '@actions/views/user';
|
||||
import {ChannelTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getAllChannels, getCurrentChannelId, getChannelMembersInChannels} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUser, getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import {isGuest} from '@mm-redux/utils/user_utils';
|
||||
|
||||
export function handleStatusChangedEvent(msg: WebSocketMessage): GenericAction {
|
||||
return {
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: [{user_id: msg.data.user_id, status: msg.data.status}],
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserAddedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const teamId = msg.data.team_id;
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: ChannelTypes.CHANNEL_MEMBER_ADDED,
|
||||
data: {
|
||||
channel_id: msg.broadcast.channel_id,
|
||||
user_id: msg.data.user_id,
|
||||
},
|
||||
}];
|
||||
|
||||
if (msg.broadcast.channel_id === currentChannelId) {
|
||||
const stat = await Client4.getChannelStats(currentChannelId);
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_STATS,
|
||||
data: stat,
|
||||
});
|
||||
}
|
||||
|
||||
if (teamId === currentTeamId && msg.data.user_id === currentUserId) {
|
||||
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
|
||||
|
||||
if (channelActions.length) {
|
||||
actions.push(...channelActions);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_USER_ADDED'));
|
||||
} catch (error) {
|
||||
//do nothing
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserRemovedEvent(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const state = getState();
|
||||
const channels = getAllChannels(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
const actions: Array<GenericAction> = [];
|
||||
let channelId;
|
||||
let userId;
|
||||
|
||||
if (msg.data.user_id) {
|
||||
userId = msg.data.user_id;
|
||||
channelId = msg.broadcast.channel_id;
|
||||
} else if (msg.broadcast.user_id) {
|
||||
channelId = msg.data.channel_id;
|
||||
userId = msg.broadcast.user_id;
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
actions.push({
|
||||
type: ChannelTypes.CHANNEL_MEMBER_REMOVED,
|
||||
data: {
|
||||
channel_id: channelId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const channel = channels[currentChannelId];
|
||||
|
||||
if (msg.data?.user_id !== currentUser.id) {
|
||||
const members = getChannelMembersInChannels(state);
|
||||
const isMember = Object.values(members).some((member) => member[msg.data.user_id]);
|
||||
if (channel && isGuest(currentUser.roles) && !isMember) {
|
||||
actions.push({
|
||||
type: UserTypes.PROFILE_NO_LONGER_VISIBLE,
|
||||
data: {user_id: msg.data.user_id},
|
||||
}, {
|
||||
type: TeamTypes.REMOVE_MEMBER_FROM_TEAM,
|
||||
data: {team_id: channel.team_id, user_id: msg.data.user_id},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let redirectToDefaultChannel = false;
|
||||
if (msg.broadcast.user_id === currentUser.id && currentTeamId) {
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
||||
|
||||
if (myData?.channels && myData?.channelMembers) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data: myData,
|
||||
});
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
actions.push({
|
||||
type: ChannelTypes.LEAVE_CHANNEL,
|
||||
data: {
|
||||
id: msg.data.channel_id,
|
||||
user_id: currentUser.id,
|
||||
team_id: channel.team_id,
|
||||
type: channel.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (msg.data.channel_id === currentChannelId) {
|
||||
// emit the event so the client can change his own state
|
||||
redirectToDefaultChannel = true;
|
||||
}
|
||||
if (isGuest(currentUser.roles)) {
|
||||
const notVisible = await notVisibleUsersActions(state);
|
||||
if (notVisible.length) {
|
||||
actions.push(...notVisible);
|
||||
}
|
||||
}
|
||||
} else if (msg.data.channel_id === currentChannelId) {
|
||||
const stat = await Client4.getChannelStats(currentChannelId);
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_STATS,
|
||||
data: stat,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_WS_USER_REMOVED'));
|
||||
if (redirectToDefaultChannel) {
|
||||
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserRoleUpdated(msg: WebSocketMessage) {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
try {
|
||||
const roles = msg.data.roles.split(' ');
|
||||
const data = await Client4.getRolesByNames(roles);
|
||||
|
||||
dispatch({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: data.roles,
|
||||
});
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUserUpdatedEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const currentUser = getCurrentUser(getState());
|
||||
const user = msg.data.user;
|
||||
|
||||
if (user.id === currentUser.id) {
|
||||
if (user.update_at > currentUser.update_at) {
|
||||
// Need to request me to make sure we don't override with sanitized fields from the
|
||||
// websocket event
|
||||
dispatch(getMe());
|
||||
}
|
||||
} else {
|
||||
dispatch({
|
||||
type: UserTypes.RECEIVED_PROFILES,
|
||||
data: {
|
||||
[user.id]: user,
|
||||
},
|
||||
});
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
547
app/actions/websocket/websocket.test.js
Normal file
547
app/actions/websocket/websocket.test.js
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import {GeneralTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts, RequestStatus} from '@mm-redux/constants';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import TestHelper from 'test/test_helper';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
const mockConfigRequest = (config = {}) => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/config/client?format=old').
|
||||
reply(200, config);
|
||||
};
|
||||
|
||||
const mockChanelsRequest = (teamId, channels = []) => {
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${teamId}/channels?include_deleted=true`).
|
||||
reply(200, channels);
|
||||
};
|
||||
|
||||
const mockGetKnownUsersRequest = (userIds = []) => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/known').
|
||||
reply(200, userIds);
|
||||
};
|
||||
|
||||
const mockRolesRequest = (rolesToLoad = []) => {
|
||||
nock(Client4.getRolesRoute()).
|
||||
post('/names', JSON.stringify(rolesToLoad)).
|
||||
reply(200, rolesToLoad);
|
||||
};
|
||||
|
||||
const mockTeamMemberRequest = (tm = []) => {
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get('/teams/members').
|
||||
reply(200, tm);
|
||||
};
|
||||
|
||||
describe('Actions.Websocket', () => {
|
||||
let store;
|
||||
let mockServer;
|
||||
beforeAll(async () => {
|
||||
store = await configureStore();
|
||||
await TestHelper.initBasic(Client4);
|
||||
|
||||
const connUrl = (Client4.getUrl() + '/api/v4/websocket').replace(/^http:/, 'ws:');
|
||||
mockServer = new Server(connUrl);
|
||||
return store.dispatch(Actions.init({websocketUrl: Client4.getUrl().replace(/^http:/, 'ws:')}));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
mockServer.stop();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('WebSocket Connect', () => {
|
||||
const ws = store.getState().requests.general.websocket;
|
||||
assert.ok(ws.status === RequestStatus.SUCCESS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.Websocket doReconnect', () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const me = TestHelper.fakeUserWithId();
|
||||
const team = TestHelper.fakeTeamWithId();
|
||||
const teamMember = TestHelper.fakeTeamMember(me.id, team.id);
|
||||
const channel1 = TestHelper.fakeChannelWithId(team.id);
|
||||
const channel2 = TestHelper.fakeChannelWithId(team.id);
|
||||
const cMember1 = TestHelper.fakeChannelMember(me.id, channel1.id);
|
||||
const cMember2 = TestHelper.fakeChannelMember(me.id, channel2.id);
|
||||
|
||||
const currentTeamId = team.id;
|
||||
const currentUserId = me.id;
|
||||
const currentChannelId = channel1.id;
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId,
|
||||
myMembers: {
|
||||
[currentTeamId]: teamMember,
|
||||
},
|
||||
teams: {
|
||||
[currentTeamId]: team,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: channel1,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {
|
||||
[me.id]: me,
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
posts: {
|
||||
posts: {},
|
||||
postsInChannel: {},
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
connected: false,
|
||||
lastConnectAt: 0,
|
||||
lastDisconnectAt: 0,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
return TestHelper.initBasic(Client4);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/me').
|
||||
reply(200, me);
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get('/teams').
|
||||
reply(200, [team]);
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get('/teams/unread').
|
||||
reply(200, [{id: team.id, msg_count: 0, mention_count: 0}]);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get('/users/me/preferences').
|
||||
reply(200, []);
|
||||
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${team.id}/channels/members`).
|
||||
reply(200, [cMember1, cMember2]);
|
||||
|
||||
nock(Client4.getChannelRoute(channel1.id)).
|
||||
get(`/posts?page=0&per_page=${Posts.POST_CHUNK_SIZE}`).
|
||||
reply(200, {
|
||||
posts: {
|
||||
post1: {id: 'post1', create_at: 0, message: 'hey'},
|
||||
},
|
||||
order: ['post1'],
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
Actions.close()();
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('handle doReconnect', async () => {
|
||||
const state = {...initialState};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
'BATCH_GET_POSTS',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [channel1, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
const actionTypes = testStore.getActions().map((a) => a.type);
|
||||
expect(actionTypes).toEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived or the user left it', async () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
channels: {
|
||||
...initialState.entities.channels,
|
||||
currentChannelId: 'channel-3',
|
||||
},
|
||||
},
|
||||
};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [channel1, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
|
||||
expect(actions).toEqual(expect.arrayContaining(expectedActions));
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived and setting is on', async () => {
|
||||
const archived = {
|
||||
...channel1,
|
||||
delete_at: 123,
|
||||
};
|
||||
const state = {
|
||||
...initialState,
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: archived,
|
||||
},
|
||||
},
|
||||
};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
|
||||
mockConfigRequest({ExperimentalViewArchivedChannels: 'true'});
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [archived, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
expect(actions).toEqual(expect.arrayContaining(expectedActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after the current channel was archived and setting is off', async () => {
|
||||
const archived = {
|
||||
...channel1,
|
||||
delete_at: 123,
|
||||
};
|
||||
|
||||
const state = {
|
||||
...initialState,
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: archived,
|
||||
},
|
||||
},
|
||||
};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS',
|
||||
];
|
||||
|
||||
mockConfigRequest({ExperimentalViewArchivedChannels: 'false'});
|
||||
mockTeamMemberRequest([teamMember]);
|
||||
mockChanelsRequest(team.id, [archived, channel2]);
|
||||
|
||||
let rolesToLoad = Array.from(new Set(me.roles.split(' ').
|
||||
concat(teamMember.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
expect(actions).toEqual(expectedActions);
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
|
||||
it('handle doReconnect after user left current team', async () => {
|
||||
const state = {...initialState};
|
||||
state.entities.teams.myMembers = {};
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_LEAVE_TEAM',
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_GET_POSTS',
|
||||
];
|
||||
|
||||
mockConfigRequest();
|
||||
mockTeamMemberRequest([]);
|
||||
mockChanelsRequest(team.id, [channel1, channel2]);
|
||||
|
||||
let rolesToLoad = me.roles.split(' ');
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
rolesToLoad = Array.from(new Set(cMember1.roles.split(' ').
|
||||
concat(cMember2.roles.split(' '))));
|
||||
mockRolesRequest(rolesToLoad);
|
||||
|
||||
await testStore.dispatch(Actions.doReconnect(timestamp));
|
||||
await TestHelper.wait(300);
|
||||
const actions = testStore.getActions().map((a) => a.type);
|
||||
expect(actions).toEqual(expectedActions);
|
||||
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.Websocket notVisibleUsersActions', () => {
|
||||
configureMockStore([thunk]);
|
||||
|
||||
const me = TestHelper.fakeUserWithId();
|
||||
const user = TestHelper.fakeUserWithId();
|
||||
const user2 = TestHelper.fakeUserWithId();
|
||||
const user3 = TestHelper.fakeUserWithId();
|
||||
const user4 = TestHelper.fakeUserWithId();
|
||||
const user5 = TestHelper.fakeUserWithId();
|
||||
|
||||
it('should do nothing if the known users and the profiles list are the same', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user.id]: user,
|
||||
[user2.id]: user2,
|
||||
[user3.id]: user3,
|
||||
};
|
||||
Client4.serverVersion = '5.23.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user2.id, user3.id]);
|
||||
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should do nothing if there are known users in my memberships but not in the profiles list', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user3.id]: user3,
|
||||
};
|
||||
Client4.serverVersion = '5.23.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user2.id, user3.id]);
|
||||
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should remove the users if there are unknown users in the profiles list', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user.id]: user,
|
||||
[user2.id]: user2,
|
||||
[user3.id]: user3,
|
||||
[user4.id]: user4,
|
||||
[user5.id]: user5,
|
||||
};
|
||||
Client4.serverVersion = '5.23.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user3.id]);
|
||||
|
||||
const expectedAction = [
|
||||
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user2.id}},
|
||||
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user4.id}},
|
||||
{type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: user5.id}},
|
||||
];
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(3);
|
||||
expect(actions).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should do nothing if the server version is less than 5.23', async () => {
|
||||
const profiles = {
|
||||
[me.id]: me,
|
||||
[user.id]: user,
|
||||
[user2.id]: user2,
|
||||
[user3.id]: user3,
|
||||
[user4.id]: user4,
|
||||
[user5.id]: user5,
|
||||
};
|
||||
Client4.serverVersion = '5.22.0';
|
||||
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: me.id,
|
||||
profiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockGetKnownUsersRequest([user.id, user3.id]);
|
||||
|
||||
const actions = await notVisibleUsersActions(state);
|
||||
expect(actions.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions.Websocket handleUserTypingEvent', () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
const currentUserId = 'user-id';
|
||||
const otherUserId = 'other-user-id';
|
||||
const currentChannelId = 'channel-id';
|
||||
const otherChannelId = 'other-channel-id';
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
channels: {
|
||||
currentChannelId,
|
||||
channels: {
|
||||
currentChannelId: {
|
||||
id: currentChannelId,
|
||||
name: 'channel',
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {
|
||||
[currentUserId]: {},
|
||||
[otherUserId]: {},
|
||||
},
|
||||
statuses: {
|
||||
[currentUserId]: General.ONLINE,
|
||||
[otherUserId]: General.OFFLINE,
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('dispatches actions for current channel if other user is typing', async () => {
|
||||
const state = {...initialState};
|
||||
const testStore = await mockStore(state);
|
||||
const msg = {broadcast: {channel_id: currentChannelId}, data: {parent_id: 'parent-id', user_id: otherUserId}};
|
||||
|
||||
nock(Client4.getUsersRoute()).
|
||||
post('/status/ids', JSON.stringify([otherUserId])).
|
||||
reply(200, ['away']);
|
||||
|
||||
const expectedActionsTypes = [
|
||||
WebsocketEvents.TYPING,
|
||||
UserTypes.RECEIVED_STATUSES,
|
||||
];
|
||||
|
||||
await testStore.dispatch(Actions.handleUserTypingEvent(msg));
|
||||
await TestHelper.wait(300);
|
||||
const actionTypes = testStore.getActions().map((action) => action.type);
|
||||
expect(actionTypes).toEqual(expectedActionsTypes);
|
||||
});
|
||||
|
||||
it('does not dispatch actions for non current channel', async () => {
|
||||
const state = {...initialState};
|
||||
const testStore = await mockStore(state);
|
||||
const msg = {broadcast: {channel_id: otherChannelId}, data: {parent_id: 'parent-id', user_id: otherUserId}};
|
||||
|
||||
const expectedActionsTypes = [];
|
||||
|
||||
await testStore.dispatch(Actions.handleUserTypingEvent(msg));
|
||||
const actionTypes = testStore.getActions().map((action) => action.type);
|
||||
expect(actionTypes).toEqual(expectedActionsTypes);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,13 +2,19 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {logout} from '@actions/views/user';
|
||||
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {Client4Error} from '@mm-redux/types/client4';
|
||||
import {getCurrentUserId, getUsers} from '@mm-redux/selectors/entities/users';
|
||||
import {batchActions, Action, ActionFunc, GenericAction, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {logError} from './errors';
|
||||
|
||||
type ActionType = string;
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export function forceLogoutIfNecessary(err: Client4Error, dispatch: DispatchFunc, getState: GetStateFunc) {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
|
||||
|
|
@ -131,6 +137,29 @@ export function debounce(func: (...args: any) => unknown, wait: number, immediat
|
|||
};
|
||||
}
|
||||
|
||||
export async function notVisibleUsersActions(state: GlobalState): Promise<Array<GenericAction>> {
|
||||
if (!isMinimumServerVersion(Client4.getServerVersion(), 5, 23)) {
|
||||
return [];
|
||||
}
|
||||
let knownUsers: Set<string>;
|
||||
try {
|
||||
const fetchResult = await Client4.getKnownUsers();
|
||||
knownUsers = new Set(fetchResult);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
knownUsers.add(getCurrentUserId(state));
|
||||
const allUsers = Object.keys(getUsers(state));
|
||||
const usersToRemove = new Set(allUsers.filter((x) => !knownUsers.has(x)));
|
||||
|
||||
const actions = [];
|
||||
for (const userToRemove of usersToRemove.values()) {
|
||||
actions.push({type: UserTypes.PROFILE_NO_LONGER_VISIBLE, data: {user_id: userToRemove}});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export class FormattedError extends Error {
|
||||
intl: {
|
||||
id: string;
|
||||
|
|
|
|||
18
app/mm-redux/types/websocket.ts
Normal file
18
app/mm-redux/types/websocket.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
|
||||
export type WebsocketBroadcast = {
|
||||
omit_users: Dictionary<boolean>;
|
||||
user_id: string;
|
||||
channel_id: string;
|
||||
team_id: string;
|
||||
}
|
||||
|
||||
export type WebSocketMessage = {
|
||||
event: string;
|
||||
data: any;
|
||||
broadcast: WebsocketBroadcast;
|
||||
seq: number;
|
||||
}
|
||||
Loading…
Reference in a new issue