websocket_actions.jsx 40.7 KB
Newer Older
1
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
// See LICENSE.txt for license information.
3

4
import {batchActions} from 'redux-batched-actions';
5
6
7
import {
    ChannelTypes,
    EmojiTypes,
8
    GroupTypes,
9
10
11
12
13
14
15
    PostTypes,
    TeamTypes,
    UserTypes,
    RoleTypes,
    GeneralTypes,
    AdminTypes,
    IntegrationTypes,
Joram Wilander's avatar
Joram Wilander committed
16
    PreferenceTypes,
17
} from 'mattermost-redux/action_types';
18
import {WebsocketEvents, General, Permissions} from 'mattermost-redux/constants';
19
import {addChannelToInitialCategory, fetchMyCategories, receivedCategoryOrder} from 'mattermost-redux/actions/channel_categories';
20
21
import {
    getChannelAndMyMember,
22
    getMyChannelMember,
23
    getChannelMember,
24
25
    getChannelStats,
    viewChannel,
26
    markChannelAsRead,
27
    getChannelMemberCountsByGroup,
28
} from 'mattermost-redux/actions/channels';
29
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
30
import {setServerVersion} from 'mattermost-redux/actions/general';
31
32
33
34
import {
    getCustomEmojiForReaction,
    getPosts,
    getProfilesAndStatusesForPosts,
35
    getThreadsForPosts,
36
    postDeleted,
37
    receivedNewPost,
38
39
    receivedPost,
} from 'mattermost-redux/actions/posts';
40
41
import {clearErrors, logError} from 'mattermost-redux/actions/errors';

42
import * as TeamActions from 'mattermost-redux/actions/teams';
43
44
45
46
47
import {
    checkForModifiedUsers,
    getMe,
    getMissingProfilesByIds,
    getStatusesByIds,
48
    getUser as loadUser,
49
} from 'mattermost-redux/actions/users';
50
import {removeNotVisibleUsers} from 'mattermost-redux/actions/websocket';
51
import {Client4} from 'mattermost-redux/client';
52
import {getCurrentUser, getCurrentUserId, getStatusForUserId, getUser, getIsManualStatusForUserId} from 'mattermost-redux/selectors/entities/users';
53
import {getMyTeams, getCurrentRelativeTeamUrl, getCurrentTeamId, getCurrentTeamUrl, getTeam} from 'mattermost-redux/selectors/entities/teams';
54
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
55
import {getChannelsInTeam, getChannel, getCurrentChannel, getCurrentChannelId, getRedirectChannelNameForTeam, getMembersInCurrentChannel, getChannelMembersInChannels} from 'mattermost-redux/selectors/entities/channels';
56
import {getPost, getMostRecentPostIdInChannel} from 'mattermost-redux/selectors/entities/posts';
57
import {haveISystemPermission, haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
58

59
60
import {getSelectedChannelId} from 'selectors/rhs';

61
import {openModal} from 'actions/views/modals';
62
import {incrementWsErrorCount, resetWsErrorCount} from 'actions/views/system';
63
import {closeRightHandSide} from 'actions/views/rhs';
64
import {syncPostsInChannel} from 'actions/views/channel';
65

66
import {browserHistory} from 'utils/browser_history';
67
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
68
import {redirectUserToDefaultTeam} from 'actions/global_actions.jsx';
69
import {handleNewPost} from 'actions/post_actions.jsx';
70
import * as StatusActions from 'actions/status_actions.jsx';
Joram Wilander's avatar
Joram Wilander committed
71
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
72
73
74
import store from 'stores/redux_store.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins';
75
import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers} from 'utils/constants';
76
import {getSiteURL} from 'utils/url';
77
import {isGuest} from 'utils/utils';
78
import RemovedFromChannelModal from 'components/removed_from_channel_modal';
79
import InteractiveDialog from 'components/interactive_dialog';
80

81
82
const dispatch = store.dispatch;
const getState = store.getState;
83

84
85
const MAX_WEBSOCKET_FAILS = 7;

86
87
const pluginEventHandlers = {};

88
export function initialize() {
89
90
91
92
    if (!window.WebSocket) {
        console.log('Browser does not support websocket'); //eslint-disable-line no-console
        return;
    }
93

94
    const config = getConfig(getState());
95
    let connUrl = '';
96
97
    if (config.WebsocketURL) {
        connUrl = config.WebsocketURL;
98
    } else {
99
        connUrl = new URL(getSiteURL());
100

101
        // replace the protocol with a websocket one
102
103
        if (connUrl.protocol === 'https:') {
            connUrl.protocol = 'wss:';
104
        } else {
105
            connUrl.protocol = 'ws:';
106
        }
107
108

        // append a port number if one isn't already specified
109
110
111
        if (!(/:\d+$/).test(connUrl.host)) {
            if (connUrl.protocol === 'wss:') {
                connUrl.host += ':' + config.WebsocketSecurePort;
112
            } else {
113
                connUrl.host += ':' + config.WebsocketPort;
114
            }
115
        }
116
117
118
119
120
121
122

        connUrl = connUrl.toString();
    }

    // Strip any trailing slash before appending the pathname below.
    if (connUrl.length > 0 && connUrl[connUrl.length - 1] === '/') {
        connUrl = connUrl.substring(0, connUrl.length - 1);
123
    }
124

125
126
    connUrl += Client4.getUrlVersion() + '/websocket';

127
128
    WebSocketClient.setEventCallback(handleEvent);
    WebSocketClient.setFirstConnectCallback(handleFirstConnect);
129
    WebSocketClient.setReconnectCallback(() => reconnect(false));
130
    WebSocketClient.setMissedEventCallback(() => reconnect(false));
131
132
    WebSocketClient.setCloseCallback(handleClose);
    WebSocketClient.initialize(connUrl);
133
134
}

135
136
137
138
export function close() {
    WebSocketClient.close();
}

139
function reconnectWebSocket() {
140
141
    close();
    initialize();
142
143
}

144
145
146
147
148
149
150
151
152
153
const pluginReconnectHandlers = {};

export function registerPluginReconnectHandler(pluginId, handler) {
    pluginReconnectHandlers[pluginId] = handler;
}

export function unregisterPluginReconnectHandler(pluginId) {
    Reflect.deleteProperty(pluginReconnectHandlers, pluginId);
}

154
155
156
157
export function reconnect(includeWebSocket = true) {
    if (includeWebSocket) {
        reconnectWebSocket();
    }
158

159
160
    dispatch({
        type: GeneralTypes.WEBSOCKET_SUCCESS,
161
        timestamp: Date.now(),
162
163
    });

164
    loadPluginsIfNecessary();
165

166
167
168
169
170
171
    Object.values(pluginReconnectHandlers).forEach((handler) => {
        if (handler && typeof handler === 'function') {
            handler();
        }
    });

172
173
    const state = getState();
    const currentTeamId = state.entities.teams.currentTeamId;
174
    if (currentTeamId) {
175
176
177
        const currentChannelId = getCurrentChannelId(state);
        const mostRecentId = getMostRecentPostIdInChannel(state, currentChannelId);
        const mostRecentPost = getPost(state, mostRecentId);
178
        dispatch(loadChannelsForCurrentUser());
179
        if (mostRecentPost) {
Eli Yukelzon's avatar
Eli Yukelzon committed
180
            dispatch(syncPostsInChannel(currentChannelId, mostRecentPost.create_at));
181
182
183
184
185
        } else {
            // if network timed-out the first time when loading a channel
            // we can request for getPosts again when socket is connected
            dispatch(getPosts(currentChannelId));
        }
186
187
        StatusActions.loadStatusesForChannelAndSidebar();
        dispatch(TeamActions.getMyTeamUnreads());
188
    }
189

190
191
192
193
    if (state.websocket.lastDisconnectAt) {
        dispatch(checkForModifiedUsers());
    }

194
195
    dispatch(resetWsErrorCount());
    dispatch(clearErrors());
196
197
}

198
199
200
201
202
203
204
205
let intervalId = '';
const SYNC_INTERVAL_MILLISECONDS = 1000 * 60 * 15; // 15 minutes

export function startPeriodicSync() {
    clearInterval(intervalId);

    intervalId = setInterval(
        () => {
206
            if (getCurrentUser(getState()) != null) {
207
208
209
                reconnect(false);
            }
        },
210
        SYNC_INTERVAL_MILLISECONDS,
211
212
213
214
215
216
217
    );
}

export function stopPeriodicSync() {
    clearInterval(intervalId);
}

218
219
220
221
222
export function registerPluginWebSocketEvent(pluginId, event, action) {
    if (!pluginEventHandlers[pluginId]) {
        pluginEventHandlers[pluginId] = {};
    }
    pluginEventHandlers[pluginId][event] = action;
223
224
}

225
226
227
228
229
230
231
232
233
234
235
export function unregisterPluginWebSocketEvent(pluginId, event) {
    const events = pluginEventHandlers[pluginId];
    if (!events) {
        return;
    }

    Reflect.deleteProperty(events, event);
}

export function unregisterAllPluginWebSocketEvents(pluginId) {
    Reflect.deleteProperty(pluginEventHandlers, pluginId);
236
237
}

238
function handleFirstConnect() {
239
    dispatch(batchActions([
240
241
242
243
        {
            type: GeneralTypes.WEBSOCKET_SUCCESS,
            timestamp: Date.now(),
        },
244
245
        clearErrors(),
    ]));
246
247
248
249
}

function handleClose(failCount) {
    if (failCount > MAX_WEBSOCKET_FAILS) {
250
        dispatch(logError({type: 'critical', message: AnnouncementBarMessages.WEBSOCKET_PORT_ERROR}, true));
251
    }
252
    dispatch(batchActions([
253
254
255
256
        {
            type: GeneralTypes.WEBSOCKET_FAILURE,
            timestamp: Date.now(),
        },
257
258
        incrementWsErrorCount(),
    ]));
259
}
260

261
export function handleEvent(msg) {
262
    switch (msg.event) {
263
264
    case SocketEvents.POSTED:
    case SocketEvents.EPHEMERAL_MESSAGE:
265
        handleNewPostEventDebounced(msg);
266
267
268
269
270
271
272
273
274
275
        break;

    case SocketEvents.POST_EDITED:
        handlePostEditEvent(msg);
        break;

    case SocketEvents.POST_DELETED:
        handlePostDeleteEvent(msg);
        break;

276
277
278
279
    case SocketEvents.POST_UNREAD:
        handlePostUnreadEvent(msg);
        break;

280
281
282
283
    case SocketEvents.LEAVE_TEAM:
        handleLeaveTeamEvent(msg);
        break;

enahum's avatar
enahum committed
284
285
286
287
    case SocketEvents.UPDATE_TEAM:
        handleUpdateTeamEvent(msg);
        break;

288
289
290
291
    case SocketEvents.UPDATE_TEAM_SCHEME:
        handleUpdateTeamSchemeEvent(msg);
        break;

292
293
294
295
    case SocketEvents.DELETE_TEAM:
        handleDeleteTeamEvent(msg);
        break;

296
297
298
299
    case SocketEvents.ADDED_TO_TEAM:
        handleTeamAddedEvent(msg);
        break;

300
    case SocketEvents.USER_ADDED:
Harrison Healey's avatar
Harrison Healey committed
301
        dispatch(handleUserAddedEvent(msg));
302
303
304
305
306
307
        break;

    case SocketEvents.USER_REMOVED:
        handleUserRemovedEvent(msg);
        break;

308
309
310
311
    case SocketEvents.USER_UPDATED:
        handleUserUpdatedEvent(msg);
        break;

312
    case SocketEvents.ROLE_ADDED:
313
        handleRoleAddedEvent(msg);
314
315
316
        break;

    case SocketEvents.ROLE_REMOVED:
317
        handleRoleRemovedEvent(msg);
318
319
        break;

320
321
322
323
    case SocketEvents.CHANNEL_SCHEME_UPDATED:
        handleChannelSchemeUpdatedEvent(msg);
        break;

324
325
326
327
    case SocketEvents.MEMBERROLE_UPDATED:
        handleUpdateMemberRoleEvent(msg);
        break;

328
    case SocketEvents.ROLE_UPDATED:
329
        handleRoleUpdatedEvent(msg);
330
331
        break;

332
333
334
335
    case SocketEvents.CHANNEL_CREATED:
        handleChannelCreatedEvent(msg);
        break;

336
337
338
339
    case SocketEvents.CHANNEL_DELETED:
        handleChannelDeletedEvent(msg);
        break;

340
341
342
343
    case SocketEvents.CHANNEL_UNARCHIVED:
        handleChannelUnarchivedEvent(msg);
        break;

344
345
346
347
    case SocketEvents.CHANNEL_CONVERTED:
        handleChannelConvertedEvent(msg);
        break;

348
    case SocketEvents.CHANNEL_UPDATED:
349
        dispatch(handleChannelUpdatedEvent(msg));
350
351
        break;

352
353
354
355
    case SocketEvents.CHANNEL_MEMBER_UPDATED:
        handleChannelMemberUpdatedEvent(msg);
        break;

356
    case SocketEvents.DIRECT_ADDED:
Harrison Healey's avatar
Harrison Healey committed
357
358
359
360
361
        dispatch(handleDirectAddedEvent(msg));
        break;

    case SocketEvents.GROUP_ADDED:
        dispatch(handleGroupAddedEvent(msg));
362
363
        break;

364
365
366
367
    case SocketEvents.PREFERENCE_CHANGED:
        handlePreferenceChangedEvent(msg);
        break;

368
369
370
371
372
373
374
375
    case SocketEvents.PREFERENCES_CHANGED:
        handlePreferencesChangedEvent(msg);
        break;

    case SocketEvents.PREFERENCES_DELETED:
        handlePreferencesDeletedEvent(msg);
        break;

376
    case SocketEvents.TYPING:
377
        dispatch(handleUserTypingEvent(msg));
378
379
        break;

380
381
382
383
    case SocketEvents.STATUS_CHANGED:
        handleStatusChangedEvent(msg);
        break;

384
385
386
387
    case SocketEvents.HELLO:
        handleHelloEvent(msg);
        break;

388
389
390
391
392
393
394
395
    case SocketEvents.REACTION_ADDED:
        handleReactionAddedEvent(msg);
        break;

    case SocketEvents.REACTION_REMOVED:
        handleReactionRemovedEvent(msg);
        break;

396
397
398
399
    case SocketEvents.EMOJI_ADDED:
        handleAddEmoji(msg);
        break;

400
401
402
403
    case SocketEvents.CHANNEL_VIEWED:
        handleChannelViewedEvent(msg);
        break;

404
405
    case SocketEvents.PLUGIN_ENABLED:
        handlePluginEnabled(msg);
406
407
        break;

408
409
    case SocketEvents.PLUGIN_DISABLED:
        handlePluginDisabled(msg);
410
411
        break;

412
413
414
415
    case SocketEvents.USER_ROLE_UPDATED:
        handleUserRoleUpdated(msg);
        break;

416
417
418
419
420
421
422
423
    case SocketEvents.CONFIG_CHANGED:
        handleConfigChanged(msg);
        break;

    case SocketEvents.LICENSE_CHANGED:
        handleLicenseChanged(msg);
        break;

424
425
426
427
    case SocketEvents.PLUGIN_STATUSES_CHANGED:
        handlePluginStatusesChangedEvent(msg);
        break;

428
429
430
431
    case SocketEvents.OPEN_DIALOG:
        handleOpenDialogEvent(msg);
        break;

432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
    case SocketEvents.RECEIVED_GROUP:
        handleGroupUpdatedEvent(msg);
        break;

    case SocketEvents.RECEIVED_GROUP_ASSOCIATED_TO_TEAM:
        handleGroupAssociatedToTeamEvent(msg);
        break;

    case SocketEvents.RECEIVED_GROUP_NOT_ASSOCIATED_TO_TEAM:
        handleGroupNotAssociatedToTeamEvent(msg);
        break;

    case SocketEvents.RECEIVED_GROUP_ASSOCIATED_TO_CHANNEL:
        handleGroupAssociatedToChannelEvent(msg);
        break;

    case SocketEvents.RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL:
        handleGroupNotAssociatedToChannelEvent(msg);
        break;

452
453
454
455
456
457
458
459
    case SocketEvents.WARN_METRIC_STATUS_RECEIVED:
        handleWarnMetricStatusReceivedEvent(msg);
        break;

    case SocketEvents.WARN_METRIC_STATUS_REMOVED:
        handleWarnMetricStatusRemovedEvent(msg);
        break;

460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
    case SocketEvents.SIDEBAR_CATEGORY_CREATED:
        dispatch(handleSidebarCategoryCreated(msg));
        break;

    case SocketEvents.SIDEBAR_CATEGORY_UPDATED:
        dispatch(handleSidebarCategoryUpdated(msg));
        break;

    case SocketEvents.SIDEBAR_CATEGORY_DELETED:
        dispatch(handleSidebarCategoryDeleted(msg));
        break;

    case SocketEvents.SIDEBAR_CATEGORY_ORDER_UPDATED:
        dispatch(handleSidebarCategoryOrderUpdated(msg));
        break;

476
477
    default:
    }
478

479
480
481
482
483
484
485
486
487
    Object.values(pluginEventHandlers).forEach((pluginEvents) => {
        if (!pluginEvents) {
            return;
        }

        if (pluginEvents.hasOwnProperty(msg.event) && typeof pluginEvents[msg.event] === 'function') {
            pluginEvents[msg.event](msg);
        }
    });
488
489
}

490
// handleChannelConvertedEvent handles updating of channel which is converted from public to private
491
492
function handleChannelConvertedEvent(msg) {
    const channelId = msg.data.channel_id;
493
494
495
496
497
498
499
500
501
    if (channelId) {
        const channel = getChannel(getState(), channelId);
        if (channel) {
            dispatch({
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: {...channel, type: General.PRIVATE_CHANNEL},
            });
        }
    }
502
503
}

504
505
506
507
508
509
510
511
512
513
514
export function handleChannelUpdatedEvent(msg) {
    return (doDispatch, doGetState) => {
        const channel = JSON.parse(msg.data.channel);

        doDispatch({type: ChannelTypes.RECEIVED_CHANNEL, data: channel});

        const state = doGetState();
        if (channel.id === getCurrentChannelId(state)) {
            browserHistory.replace(`${getCurrentRelativeTeamUrl(state)}/channels/${channel.name}`);
        }
    };
515
516
}

517
518
function handleChannelMemberUpdatedEvent(msg) {
    const channelMember = JSON.parse(msg.data.channelMember);
519
520
    const roles = channelMember.roles.split(' ');
    dispatch(loadRolesIfNeeded(roles));
521
522
523
    dispatch({type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: channelMember});
}

524
function debouncePostEvent(wait) {
525
526
527
528
529
530
531
    let timeout;
    let queue = [];
    let count = 0;

    // Called when timeout triggered
    const triggered = () => {
        timeout = null;
532

533
534
        if (queue.length > 0) {
            dispatch(handleNewPostEvents(queue));
535
        }
536

537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
        queue = [];
        count = 0;
    };

    return function fx(msg) {
        if (timeout && count > 2) {
            // If the timeout is going this is the second or further event so queue them up.
            if (queue.push(msg) > 200) {
                // Don't run us out of memory, give up if the queue gets insane
                queue = [];
                console.log('channel broken because of too many incoming messages'); //eslint-disable-line no-console
            }
            clearTimeout(timeout);
            timeout = setTimeout(triggered, wait);
        } else {
            // Apply immediately for events up until count reaches limit
            count += 1;
554
            dispatch(handleNewPostEvent(msg));
555
556
557
558
559
560
            clearTimeout(timeout);
            timeout = setTimeout(triggered, wait);
        }
    };
}

561
const handleNewPostEventDebounced = debouncePostEvent(100);
562

563
564
565
566
export function handleNewPostEvent(msg) {
    return (myDispatch, myGetState) => {
        const post = JSON.parse(msg.data.post);
        myDispatch(handleNewPost(post, msg));
567

568
        getProfilesAndStatusesForPosts([post], myDispatch, myGetState);
569

570
571
572
573
574
575
576
577
578
579
        // Since status updates aren't real time, assume another user is online if they have posted and:
        // 1. The user hasn't set their status manually to something that isn't online
        // 2. The server hasn't told the client not to set the user to online. This happens when:
        //     a. The post is from the auto responder
        //     b. The post is a response to a push notification
        if (
            post.user_id !== getCurrentUserId(myGetState()) &&
            !getIsManualStatusForUserId(myGetState(), post.user_id) &&
            msg.data.set_online
        ) {
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
            myDispatch({
                type: UserTypes.RECEIVED_STATUSES,
                data: [{user_id: post.user_id, status: UserStatuses.ONLINE}],
            });
        }
    };
}

export function handleNewPostEvents(queue) {
    return (myDispatch, myGetState) => {
        const posts = queue.map((msg) => JSON.parse(msg.data.post));

        // Receive the posts as one continuous block since they were received within a short period
        const actions = posts.map(receivedNewPost);
        myDispatch(batchActions(actions));

596
597
598
        // Load the posts' threads
        myDispatch(getThreadsForPosts(posts));

599
600
601
        // And any other data needed for them
        getProfilesAndStatusesForPosts(posts, myDispatch, myGetState);
    };
602
603
}

604
export function handlePostEditEvent(msg) {
605
    // Store post
606
    const post = JSON.parse(msg.data.post);
607
    dispatch(receivedPost(post));
608

609
    getProfilesAndStatusesForPosts([post], dispatch, getState);
610
    const currentChannelId = getCurrentChannelId(getState());
611

612
    // Update channel state
613
    if (currentChannelId === msg.broadcast.channel_id) {
614
        dispatch(getChannelStats(currentChannelId));
615
        if (window.isActive) {
616
            dispatch(viewChannel(currentChannelId));
617
618
619
620
621
        }
    }
}

function handlePostDeleteEvent(msg) {
622
    const post = JSON.parse(msg.data.post);
623
    dispatch(postDeleted(post));
624
625
626
    if (post.is_pinned) {
        dispatch(getChannelStats(post.channel_id));
    }
627
628
}

629
630
631
632
633
634
635
636
637
638
export function handlePostUnreadEvent(msg) {
    dispatch(
        {
            type: ActionTypes.POST_UNREAD_SUCCESS,
            data: {
                lastViewedAt: msg.data.last_viewed_at,
                channelId: msg.broadcast.channel_id,
                msgCount: msg.data.msg_count,
                mentionCount: msg.data.mention_count,
            },
639
        },
640
641
642
    );
}

643
async function handleTeamAddedEvent(msg) {
644
645
646
    await dispatch(TeamActions.getTeam(msg.data.team_id));
    await dispatch(TeamActions.getMyTeamMembers());
    await dispatch(TeamActions.getMyTeamUnreads());
647
648
}

649
export function handleLeaveTeamEvent(msg) {
650
    const state = getState();
651

652
    const actions = [
653
654
655
656
657
658
659
660
        {
            type: UserTypes.RECEIVED_PROFILE_NOT_IN_TEAM,
            data: {id: msg.data.team_id, user_id: msg.data.user_id},
        },
        {
            type: TeamTypes.REMOVE_MEMBER_FROM_TEAM,
            data: {team_id: msg.data.team_id, user_id: msg.data.user_id},
        },
661
662
663
664
665
666
667
668
669
670
671
672
673
    ];

    const channelsPerTeam = getChannelsInTeam(state);
    const channels = (channelsPerTeam && channelsPerTeam[msg.data.team_id]) || [];

    for (const channel of channels) {
        actions.push({
            type: ChannelTypes.REMOVE_MEMBER_FROM_CHANNEL,
            data: {id: channel, user_id: msg.data.user_id},
        });
    }

    dispatch(batchActions(actions));
674
    const currentUser = getCurrentUser(state);
675

676
    if (currentUser.id === msg.data.user_id) {
677
678
679
680
        dispatch({type: TeamTypes.LEAVE_TEAM, data: {id: msg.data.team_id}});

        // if they are on the team being removed redirect them to default team
        if (getCurrentTeamId(state) === msg.data.team_id) {
681
            if (!global.location.pathname.startsWith('/admin_console')) {
682
                redirectUserToDefaultTeam();
683
            }
684
        }
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
        if (isGuest(currentUser)) {
            dispatch(removeNotVisibleUsers());
        }
    } else {
        const team = getTeam(state, msg.data.team_id);
        const members = getChannelMembersInChannels(state);
        const isMember = Object.values(members).some((member) => member[msg.data.user_id]);
        if (team && isGuest(currentUser) && !isMember) {
            dispatch(batchActions([
                {
                    type: UserTypes.PROFILE_NO_LONGER_VISIBLE,
                    data: {user_id: msg.data.user_id},
                },
                {
                    type: TeamTypes.REMOVE_MEMBER_FROM_TEAM,
                    data: {team_id: team.id, user_id: msg.data.user_id},
                },
            ]));
        }
704
705
706
    }
}

enahum's avatar
enahum committed
707
function handleUpdateTeamEvent(msg) {
708
    dispatch({type: TeamTypes.UPDATED_TEAM, data: JSON.parse(msg.data.team)});
enahum's avatar
enahum committed
709
710
}

711
712
713
714
function handleUpdateTeamSchemeEvent() {
    dispatch(TeamActions.getMyTeamMembers());
}

715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
function handleDeleteTeamEvent(msg) {
    const deletedTeam = JSON.parse(msg.data.team);
    const state = store.getState();
    const {teams} = state.entities.teams;
    if (
        deletedTeam &&
        teams &&
        teams[deletedTeam.id] &&
        teams[deletedTeam.id].delete_at === 0
    ) {
        const {currentUserId} = state.entities.users;
        const {currentTeamId, myMembers} = state.entities.teams;
        const teamMembers = Object.values(myMembers);
        const teamMember = teamMembers.find((m) => m.user_id === currentUserId && m.team_id === currentTeamId);

        let newTeamId = '';
        if (
            deletedTeam &&
            teamMember &&
            deletedTeam.id === teamMember.team_id
        ) {
            const myTeams = {};
            getMyTeams(state).forEach((t) => {
                myTeams[t.id] = t;
            });

            for (let i = 0; i < teamMembers.length; i++) {
                const memberTeamId = teamMembers[i].team_id;
                if (
                    myTeams &&
                    myTeams[memberTeamId] &&
                    myTeams[memberTeamId].delete_at === 0 &&
                    deletedTeam.id !== memberTeamId
                ) {
                    newTeamId = memberTeamId;
                    break;
                }
            }
        }

        dispatch(batchActions([
            {type: TeamTypes.RECEIVED_TEAM_DELETED, data: {id: deletedTeam.id}},
757
            {type: TeamTypes.UPDATED_TEAM, data: deletedTeam},
758
759
760
761
        ]));

        if (newTeamId) {
            dispatch({type: TeamTypes.SELECT_TEAM, data: newTeamId});
762
763
764
            const globalState = getState();
            const redirectChannel = getRedirectChannelNameForTeam(globalState, newTeamId);
            browserHistory.push(`${getCurrentTeamUrl(globalState)}/channels/${redirectChannel}`);
765
766
767
768
769
770
        } else {
            browserHistory.push('/');
        }
    }
}

771
function handleUpdateMemberRoleEvent(msg) {
772
773
774
775
776
    const memberData = JSON.parse(msg.data.member);
    const newRoles = memberData.roles.split(' ');

    dispatch(loadRolesIfNeeded(newRoles));

777
778
    dispatch({
        type: TeamTypes.RECEIVED_MY_TEAM_MEMBER,
779
        data: memberData,
780
    });
781
782
}

783
function handleDirectAddedEvent(msg) {
Harrison Healey's avatar
Harrison Healey committed
784
785
786
787
788
    return fetchChannelAndAddToSidebar(msg.broadcast.channel_id);
}

function handleGroupAddedEvent(msg) {
    return fetchChannelAndAddToSidebar(msg.broadcast.channel_id);
789
790
}

791
function handleUserAddedEvent(msg) {
Harrison Healey's avatar
Harrison Healey committed
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
    return async (doDispatch, doGetState) => {
        const state = doGetState();
        const config = getConfig(state);
        const license = getLicense(state);
        const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
        const currentChannelId = getCurrentChannelId(state);
        if (currentChannelId === msg.broadcast.channel_id) {
            doDispatch(getChannelStats(currentChannelId));
            doDispatch({
                type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL,
                data: {id: msg.broadcast.channel_id, user_id: msg.data.user_id},
            });
            if (license?.IsLicensed === 'true' && license?.LDAPGroups === 'true') {
                doDispatch(getChannelMemberCountsByGroup(currentChannelId, isTimezoneEnabled));
            }
807
        }
808

Harrison Healey's avatar
Harrison Healey committed
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
        // Load the channel so that it appears in the sidebar
        const currentTeamId = getCurrentTeamId(doGetState());
        const currentUserId = getCurrentUserId(doGetState());
        if (currentTeamId === msg.data.team_id && currentUserId === msg.data.user_id) {
            doDispatch(fetchChannelAndAddToSidebar(msg.broadcast.channel_id));
        }
    };
}

function fetchChannelAndAddToSidebar(channelId) {
    return async (doDispatch) => {
        const {data, error} = await doDispatch(getChannelAndMyMember(channelId));

        if (!error) {
            doDispatch(addChannelToInitialCategory(data.channel));
        }
    };
826
827
}

828
export async function handleUserRemovedEvent(msg) {
829
830
    const state = getState();
    const currentChannel = getCurrentChannel(state) || {};
831
    const currentUser = getCurrentUser(state);
832
    const config = getConfig(state);
833
    const license = getLicense(state);
834
    const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
835

836
    if (msg.broadcast.user_id === currentUser.id) {
837
        dispatch(loadChannelsForCurrentUser());
838

839
840
841
842
        const rhsChannelId = getSelectedChannelId(state);
        if (msg.data.channel_id === rhsChannelId) {
            dispatch(closeRightHandSide());
        }
843

844
        if (msg.data.channel_id === currentChannel.id) {
845
846
            if (msg.data.remover_id === msg.broadcast.user_id) {
                browserHistory.push(getCurrentRelativeTeamUrl(state));
847
848
849
850
851

                await dispatch({
                    type: ChannelTypes.LEAVE_CHANNEL,
                    data: {id: msg.data.channel_id, user_id: msg.broadcast.user_id},
                });
852
            } else {
853
                const user = getUser(state, msg.data.remover_id);
854
                if (!user) {
855
                    dispatch(loadUser(msg.data.remover_id));
856
                }
857

858
                dispatch(openModal({
859
860
                    modalId: ModalIdentifiers.REMOVED_FROM_CHANNEL,
                    dialogType: RemovedFromChannelModal,
861
862
                    dialogProps: {
                        channelName: currentChannel.display_name,
863
                        removerId: msg.data.remover_id,
864
865
                    },
                }));
866
867
868
869
870
871

                await dispatch({
                    type: ChannelTypes.LEAVE_CHANNEL,
                    data: {id: msg.data.channel_id, user_id: msg.broadcast.user_id},
                });

872
                redirectUserToDefaultTeam();
873
874
            }
        }
875

876
877
878
        if (isGuest(currentUser)) {
            dispatch(removeNotVisibleUsers());
        }
879
    } else if (msg.broadcast.channel_id === currentChannel.id) {
880
        dispatch(getChannelStats(currentChannel.id));
881
882
        dispatch({
            type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL,
883
            data: {id: msg.broadcast.channel_id, user_id: msg.data.user_id},
884
        });
885
886
        if (license?.IsLicensed === 'true' && license?.LDAPGroups === 'true') {
            dispatch(getChannelMemberCountsByGroup(currentChannel.id, isTimezoneEnabled));
887
        }
888
    }
889

890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
    if (msg.broadcast.user_id !== currentUser.id) {
        const channel = getChannel(state, msg.broadcast.channel_id);
        const members = getChannelMembersInChannels(state);
        const isMember = Object.values(members).some((member) => member[msg.data.user_id]);
        if (channel && isGuest(currentUser) && !isMember) {
            const actions = [
                {
                    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},
                },
            ];
            dispatch(batchActions(actions));
        }
    }

909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
    const channelId = msg.broadcast.channel_id || msg.data.channel_id;
    const userId = msg.broadcast.user_id || msg.data.user_id;
    const channel = getChannel(state, channelId);
    if (channel && !haveISystemPermission(state, {permission: Permissions.VIEW_MEMBERS}) && !haveITeamPermission(state, {permission: Permissions.VIEW_MEMBERS, team: channel.team_id})) {
        dispatch(batchActions([
            {
                type: UserTypes.RECEIVED_PROFILE_NOT_IN_TEAM,
                data: {id: channel.team_id, user_id: userId},
            },
            {
                type: TeamTypes.REMOVE_MEMBER_FROM_TEAM,
                data: {team_id: channel.team_id, user_id: userId},
            },
        ]));
    }
924
925
}

926
927
928
export async function handleUserUpdatedEvent(msg) {
    const state = getState();
    const currentUser = getCurrentUser(state);
929
    const user = msg.data.user;
930

931
    const config = getConfig(state);
932
933
    const license = getLicense(state);

934
    const userIsGuest = isGuest(user);
935
936
937
938
    const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
    const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';

    if (userIsGuest || (isTimezoneEnabled && isLDAPEnabled)) {
939
940
        let members = getMembersInCurrentChannel(state);
        const currentChannelId = getCurrentChannelId(state);
941
942
        let memberExists = members && members[user.id];
        if (!memberExists) {
943
944
            await dispatch(getChannelMember(currentChannelId, user.id));
            members = getMembersInCurrentChannel(getState());
945
946
947
948
            memberExists = members && members[user.id];
        }

        if (memberExists) {
949
            if (isLDAPEnabled && isTimezoneEnabled) {
950
951
952
                dispatch(getChannelMemberCountsByGroup(currentChannelId, true));
            }
            if (isGuest(user)) {
953
954
955
956
957
                dispatch(getChannelStats(currentChannelId));
            }
        }
    }

958
    if (currentUser.id === user.id) {
959
960
961
962
963
        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
            getMe()(dispatch, getState);
        }
964
    } else {
965
966
967
968
        dispatch({
            type: UserTypes.RECEIVED_PROFILE,
            data: user,
        });
969
970
971
    }
}

972
973
974
975
976
function handleRoleAddedEvent(msg) {
    const role = JSON.parse(msg.data.role);

    dispatch({
        type: RoleTypes.RECEIVED_ROLE,
977
        data: role,
978
979
980
981
982
983
984
985
    });
}

function handleRoleRemovedEvent(msg) {
    const role = JSON.parse(msg.data.role);

    dispatch({
        type: RoleTypes.ROLE_DELETED,
986
        data: role,
987
988
989
    });
}

990
991
992
993
function handleChannelSchemeUpdatedEvent(msg) {
    dispatch(getMyChannelMember(msg.broadcast.channel_id));
}

994
995
996
997
998
function handleRoleUpdatedEvent(msg) {
    const role = JSON.parse(msg.data.role);

    dispatch({
        type: RoleTypes.RECEIVED_ROLE,
999
        data: role,
1000
1001
1002
    });
}

1003
1004
function handleChannelCreatedEvent(msg) {
    const channelId = msg.data.channel_id;
enahum's avatar
enahum committed
1005
    const teamId = msg.data.team_id;
1006
    const state = getState();
1007

1008
1009
    if (getCurrentTeamId(state) === teamId && !getChannel(state, channelId)) {
        dispatch(getChannelAndMyMember(channelId));
1010
1011
1012
    }
}

1013
function handleChannelDeletedEvent(msg) {
1014
1015
1016
    const state = getState();
    const config = getConfig(state);
    const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
1017
1018
    if (getCurrentChannelId(state) === msg.data.channel_id && !viewArchivedChannels) {
        const teamUrl = getCurrentRelativeTeamUrl(state);
1019
1020
1021
        const currentTeamId = getCurrentTeamId(state);
        const redirectChannel = getRedirectChannelNameForTeam(state, currentTeamId);
        browserHistory.push(teamUrl + '/channels/' + redirectChannel);
1022
    }
1023

1024
    dispatch({type: ChannelTypes.RECEIVED_CHANNEL_DELETED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, deleteAt: msg.data.delete_at, viewArchivedChannels}});
1025
1026
}

1027
1028
1029
1030
1031
1032
1033
1034
function handleChannelUnarchivedEvent(msg) {
    const state = getState();
    const config = getConfig(state);
    const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';

    dispatch({type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, viewArchivedChannels}});
}

1035
function handlePreferenceChangedEvent(msg) {
1036
    const preference = JSON.parse(msg.data.preference);
Joram Wilander's avatar
Joram Wilander committed
1037
1038
1039
1040
1041
    dispatch({type: PreferenceTypes.RECEIVED_PREFERENCES, data: [preference]});

    if (addedNewDmUser(preference)) {
        loadProfilesForSidebar();
    }
1042
1043
}

1044
1045
function handlePreferencesChangedEvent(msg) {
    const preferences = JSON.parse(msg.data.preferences);
Joram Wilander's avatar
Joram Wilander committed
1046
1047
1048
1049
1050
    dispatch({type: PreferenceTypes.RECEIVED_PREFERENCES, data: preferences});

    if (preferences.findIndex(addedNewDmUser) !== -1) {
        loadProfilesForSidebar();
    }
1051
1052
1053
1054
}

function handlePreferencesDeletedEvent(msg) {
    const preferences = JSON.parse(msg.data.preferences);
Joram Wilander's avatar
Joram Wilander committed
1055
1056
1057
1058
1059
    dispatch({type: PreferenceTypes.DELETED_PREFERENCES, data: preferences});
}

function addedNewDmUser(preference) {
    return preference.category === Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW && preference.value === 'true';
1060
1061
}

1062
export function handleUserTypingEvent(msg) {
1063
    return async (doDispatch, doGetState) => {
1064
1065
1066
1067
        const state = doGetState();
        const config = getConfig(state);
        const currentUserId = getCurrentUserId(state);
        const userId = msg.data.user_id;
1068

1069
1070
1071
1072
1073
        const data = {
            id: msg.broadcast.channel_id + msg.data.parent_id,
            userId,
            now: Date.now(),
        };
1074

1075
1076
        doDispatch({
            type: WebsocketEvents.TYPING,
1077
            data,
1078
        });
1079

1080
1081
1082
1083
1084
1085
        setTimeout(() => {
            doDispatch({
                type: WebsocketEvents.STOP_TYPING,
                data,
            });
        }, parseInt(config.TimeBetweenUserTypingUpdatesMilliseconds, 10));
1086

1087
        if (userId !== currentUserId) {