export function createChannel(channel: Channel, userId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let created;
        try {
            created = await Client4.createChannel(channel);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {
                    type: ChannelTypes.CREATE_CHANNEL_FAILURE,
                    error,
                },
                logError(error),
            ]));
            return {error};
        }

        const member = {
            channel_id: created.id,
            user_id: userId,
            roles: `${General.CHANNEL_USER_ROLE} ${General.CHANNEL_ADMIN_ROLE}`,
            last_viewed_at: 0,
            msg_count: 0,
            mention_count: 0,
            notify_props: {desktop: 'default', mark_unread: 'all'},
            last_update_at: created.create_at,
        };

        const actions: Action[] = [];
        const {channels, myMembers} = getState().entities.channels;

        if (!channels[created.id]) {
            actions.push({type: ChannelTypes.RECEIVED_CHANNEL, data: created});
        }

        if (!myMembers[created.id]) {
            actions.push({type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member});
            dispatch(loadRolesIfNeeded(member.roles.split(' ')));
        }

        dispatch(batchActions([
            ...actions,
            {
                type: ChannelTypes.CREATE_CHANNEL_SUCCESS,
            },
        ]));

        dispatch(addChannelToInitialCategory(created, true));

        return {data: created};
    };
}


export function createDirectChannel(userId: string, otherUserId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.CREATE_CHANNEL_REQUEST, data: null});

        let created;
        try {
            created = await Client4.createDirectChannel([userId, otherUserId]);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CREATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        const member = {
            channel_id: created.id,
            user_id: userId,
            roles: `${General.CHANNEL_USER_ROLE}`,
            last_viewed_at: 0,
            msg_count: 0,
            mention_count: 0,
            notify_props: {desktop: 'default', mark_unread: 'all'},
            last_update_at: created.create_at,
        };

        const preferences = [
            {user_id: userId, category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: otherUserId, value: 'true'},
            {user_id: userId, category: Preferences.CATEGORY_CHANNEL_OPEN_TIME, name: created.id, value: new Date().getTime().toString()},
        ];

        savePreferences(userId, preferences)(dispatch);

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: created,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
            {
                type: PreferenceTypes.RECEIVED_PREFERENCES,
                data: preferences,
            },
            {
                type: ChannelTypes.CREATE_CHANNEL_SUCCESS,
            },
            {
                type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL,
                id: created.id,
                data: [{id: userId}, {id: otherUserId}],
            },
        ]));

        dispatch(addChannelToInitialCategory(created));

        dispatch(loadRolesIfNeeded(member.roles.split(' ')));

        return {data: created};
    };
}


export function markGroupChannelOpen(channelId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const {currentUserId} = getState().entities.users;

        const preferences: PreferenceType[] = [
            {user_id: currentUserId, category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW, name: channelId, value: 'true'},
            {user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_OPEN_TIME, name: channelId, value: new Date().getTime().toString()},
        ];

        return dispatch(savePreferences(currentUserId, preferences));
    };
}


export function createGroupChannel(userIds: string[]): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.CREATE_CHANNEL_REQUEST, data: null});

        const {currentUserId} = getState().entities.users;

        let created;
        try {
            created = await Client4.createGroupChannel(userIds);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CREATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        let member: Partial<ChannelMembership|undefined> = {
            channel_id: created.id,
            user_id: currentUserId,
            roles: `${General.CHANNEL_USER_ROLE}`,
            last_viewed_at: 0,
            msg_count: 0,
            mention_count: 0,
            notify_props: {desktop: 'default', mark_unread: 'all'},
            last_update_at: created.create_at,
        };

        // Check the channel previous existency: if the channel already have
        // posts is because it existed before.
        if (created.total_msg_count > 0) {
            const storeMember = getMyChannelMemberSelector(getState(), created.id);
            if (storeMember === null) {
                try {
                    member = await Client4.getMyChannelMember(created.id);
                } catch (error) {
                    // Log the error and keep going with the generated membership.
                    dispatch(logError(error));
                }
            } else {
                member = storeMember;
            }
        }

        dispatch(markGroupChannelOpen(created.id));

        const profilesInChannel = userIds.map((id) => ({id}));
        profilesInChannel.push({id: currentUserId}); // currentUserId is optionally in userIds, but the reducer will get rid of a duplicate

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: created,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
            {
                type: ChannelTypes.CREATE_CHANNEL_SUCCESS,
            },
            {
                type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL,
                id: created.id,
                data: profilesInChannel,
            },
        ]));

        dispatch(addChannelToInitialCategory(created));

        dispatch(loadRolesIfNeeded((member && member.roles && member.roles.split(' ')) || []));

        return {data: created};
    };
}


export function patchChannel(channelId: string, patch: Channel): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let updated;
        try {
            updated = await Client4.patchChannel(channelId, patch);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }
        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: updated,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: updated};
    };
}


export function updateChannel(channel: Channel): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let updated;
        try {
            updated = await Client4.updateChannel(channel);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: updated,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: updated};
    };
}


export function updateChannelPrivacy(channelId: string, privacy: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let updatedChannel;
        try {
            updatedChannel = await Client4.updateChannelPrivacy(channelId, privacy);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: updatedChannel,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: updatedChannel};
    };
}


export function convertChannelToPrivate(channelId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let convertedChannel;
        try {
            convertedChannel = await Client4.convertChannelToPrivate(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: convertedChannel,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: convertedChannel};
    };
}


export function getChannelByNameAndTeamName(teamName: string, channelName: string, includeDeleted = false): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let data;
        try {
            data = await Client4.getChannelByNameAndTeamName(teamName, channelName, includeDeleted);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

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

        return {data};
    };
}


export function getChannel(channelId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let data;
        try {
            data = await Client4.getChannel(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

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

        return {data};
    };
}


export function getChannelAndMyMember(channelId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let channel;
        let member;
        try {
            const channelRequest = Client4.getChannel(channelId);
            const memberRequest = Client4.getMyChannelMember(channelId);

            channel = await channelRequest;
            member = await memberRequest;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: channel,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
        ]));
        dispatch(loadRolesIfNeeded(member.roles.split(' ')));

        return {data: {channel, member}};
    };
}


export function fetchMyChannelsAndMembers(teamId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({
            type: ChannelTypes.CHANNELS_REQUEST,
            data: null,
        });

        let channels;
        let channelMembers;
        const state = getState();
        const shouldFetchArchived = isMinimumServerVersion(getServerVersion(state), 5, 21);
        try {
            const channelRequest = Client4.getMyChannels(teamId, shouldFetchArchived);
            const memberRequest = Client4.getMyChannelMembers(teamId);
            channels = await channelRequest;
            channelMembers = await memberRequest;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        const {currentUserId} = state.entities.users;
        const {currentChannelId} = state.entities.channels;

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
                currentChannelId,
            },
            {
                type: ChannelTypes.CHANNELS_SUCCESS,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
                data: channelMembers,
                sync: !shouldFetchArchived,
                channels,
                remove: getChannelsIdForTeam(state, teamId),
                currentUserId,
                currentChannelId,
            },
        ]));
        const roles = new Set<string>();
        for (const member of channelMembers) {
            for (const role of member.roles.split(' ')) {
                roles.add(role);
            }
        }
        if (roles.size > 0) {
            dispatch(loadRolesIfNeeded(roles));
        }

        return {data: {channels, members: channelMembers}};
    };
}


export function getMyChannelMembers(teamId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let channelMembers;
        try {
            const channelMembersRequest = Client4.getMyChannelMembers(teamId);

            channelMembers = await channelMembersRequest;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        const state = getState();
        const {currentUserId} = state.entities.users;
        const {currentChannelId} = state.entities.channels;

        dispatch({
            type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
            data: channelMembers,
            remove: getChannelsIdForTeam(getState(), teamId),
            currentUserId,
            currentChannelId,
        });
        const roles = new Set<string>();

        for (const member of channelMembers) {
            for (const role of member.roles.split(' ')) {
                roles.add(role);
            }
        }
        if (roles.size > 0) {
            dispatch(loadRolesIfNeeded(roles));
        }

        return {data: channelMembers};
    };
}


export function leaveChannel(channelId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const {currentUserId} = state.entities.users;
        const {channels, myMembers} = state.entities.channels;
        const channel = channels[channelId];
        const member = myMembers[channelId];

        Client4.trackEvent('action', 'action_channels_leave', {channel_id: channelId});

        dispatch({
            type: ChannelTypes.LEAVE_CHANNEL,
            data: {
                id: channelId,
                user_id: currentUserId,
                team_id: channel.team_id,
                type: channel.type,
            },
        });

        (async function removeFromChannelWrapper() {
            try {
                await Client4.removeFromChannel(currentUserId, channelId);
            } catch {
                dispatch(batchActions([
                    {
                        type: ChannelTypes.RECEIVED_CHANNEL,
                        data: channel,
                    },
                    {
                        type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                        data: member,
                    },
                ]));

                // The category here may not be the one in which the channel was originally located,
                // much less the order in which it was placed. Treating this as a transient issue
                // for the user to resolve by refreshing or leaving again.
                dispatch(addChannelToInitialCategory(channel, false));
            }
        }());

        return {data: true};
    };
}


export function joinChannel(userId: string, teamId: string, channelId: string, channelName: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        if (!channelId && !channelName) {
            return {data: null};
        }

        let member: ChannelMembership | undefined | null;
        let channel: Channel;
        try {
            if (channelId) {
                member = await Client4.addToChannel(userId, channelId);
                channel = await Client4.getChannel(channelId);
            } else {
                channel = await Client4.getChannelByName(teamId, channelName, true);
                if ((channel.type === General.GM_CHANNEL) || (channel.type === General.DM_CHANNEL)) {
                    member = await Client4.getChannelMember(channel.id, userId);
                } else {
                    member = await Client4.addToChannel(userId, channel.id);
                }
            }
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        Client4.trackEvent('action', 'action_channels_join', {channel_id: channelId});

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: channel,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
        ]));

        dispatch(addChannelToInitialCategory(channel));

        if (member) {
            dispatch(loadRolesIfNeeded(member.roles.split(' ')));
        }

        return {data: {channel, member}};
    };
}


export function deleteChannel(channelId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let state = getState();
        const viewArchivedChannels = state.entities.general.config.ExperimentalViewArchivedChannels === 'true';

        try {
            await Client4.deleteChannel(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        state = getState();
        const {currentChannelId} = state.entities.channels;
        if (channelId === currentChannelId && !viewArchivedChannels) {
            const teamId = getCurrentTeamId(state);
            const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
            const channel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
            if (channel && channel.id) {
                dispatch({type: ChannelTypes.SELECT_CHANNEL, data: channel.id});
            }
        }

        dispatch({type: ChannelTypes.DELETE_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}});

        return {data: true};
    };
}


export function unarchiveChannel(channelId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        try {
            await Client4.unarchiveChannel(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        const state = getState();
        const config = getConfig(state);
        const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
        dispatch({type: ChannelTypes.UNARCHIVED_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}});

        return {data: true};
    };
}


export function viewChannel(channelId: string, prevChannelId = ''): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const {currentUserId} = getState().entities.users;

        const {myPreferences} = getState().entities.preferences;
        const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channelId}`];
        const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0;
        const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId);

        if (viewTime < new Date().getTime() - (3 * 60 * 60 * 1000)) {
            const preferences = [
                {user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, name: channelId, value: new Date().getTime().toString()},
            ];
            savePreferences(currentUserId, preferences)(dispatch);
        }

        try {
            await Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));

            return {error};
        }

        const actions: Action[] = [];

        const {myMembers} = getState().entities.channels;
        const member = myMembers[channelId];
        if (member) {
            if (isManuallyUnread(getState(), channelId)) {
                actions.push({
                    type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
                    data: {channelId},
                });
            }
            actions.push({
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: {...member, last_viewed_at: new Date().getTime()},
            });
            dispatch(loadRolesIfNeeded(member.roles.split(' ')));
        }

        const prevMember = myMembers[prevChannelId];
        if (!prevChanManuallyUnread && prevMember) {
            actions.push({
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: {...prevMember, last_viewed_at: new Date().getTime()},
            });
            dispatch(loadRolesIfNeeded(prevMember.roles.split(' ')));
        }

        dispatch(batchActions(actions));

        return {data: true};
    };
}


export function getChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.getChannels(teamId, page, perPage);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    };
}


export function getArchivedChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let channels;
        try {
            channels = await Client4.getArchivedChannels(teamId, page, perPage);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            return {error};
        }

        dispatch({
            type: ChannelTypes.RECEIVED_CHANNELS,
            teamId,
            data: channels,
        });

        return {data: channels};
    };
}


export function getAllChannelsWithCount(page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE, notAssociatedToGroup = '', excludeDefaultChannels = false, includeDeleted = false): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null});

        let payload;
        try {
            payload = await Client4.getAllChannels(page, perPage, notAssociatedToGroup, excludeDefaultChannels, true, includeDeleted) as ChannelsWithTotalCount;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_ALL_CHANNELS,
                data: payload.channels,
            },
            {
                type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS,
            },
            {
                type: ChannelTypes.RECEIVED_TOTAL_CHANNEL_COUNT,
                data: payload.total_count,
            },
        ]));

        return {data: payload};
    };
}


export function getAllChannels(page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE, notAssociatedToGroup = '', excludeDefaultChannels = false): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.getAllChannels(page, perPage, notAssociatedToGroup, excludeDefaultChannels);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_ALL_CHANNELS,
                data: channels,
            },
            {
                type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    };
}


export function autocompleteChannels(teamId: string, term: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.autocompleteChannels(teamId, term);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    };
}


export function autocompleteChannelsForSearch(teamId: string, term: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.autocompleteChannelsForSearch(teamId, term);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    };
}


export function searchChannels(teamId: string, term: string, archived?: boolean): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            if (archived) {
                channels = await Client4.searchArchivedChannels(teamId, term);
            } else {
                channels = await Client4.searchChannels(teamId, term);
            }
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    };
}


export function searchAllChannels(term: string, opts: ChannelSearchOpts = {}): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null});

        let response;
        try {
            response = await Client4.searchAllChannels(term, opts) as ChannelsWithTotalCount;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        const channels = response.channels || response;

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_ALL_CHANNELS,
                data: channels,
            },
            {
                type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS,
            },
        ]));

        return {data: response};
    };
}


export function addChannelMember(channelId: string, userId: string, postRootId = ''): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let member;
        try {
            member = await Client4.addToChannel(userId, channelId, postRootId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        Client4.trackEvent('action', 'action_channels_add_member', {channel_id: channelId});

        dispatch(batchActions([
            {
                type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL,
                data: {id: channelId, user_id: userId},
            },
            {
                type: ChannelTypes.RECEIVED_CHANNEL_MEMBER,
                data: member,
            },
            {
                type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
                id: channelId,
            },
        ], 'ADD_CHANNEL_MEMBER.BATCH'));

        return {data: member};
    };
}


export function removeChannelMember(channelId: string, userId: string): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        try {
            await Client4.removeFromChannel(userId, channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        Client4.trackEvent('action', 'action_channels_remove_member', {channel_id: channelId});

        dispatch(batchActions([
            {
                type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL,
                data: {id: channelId, user_id: userId},
            },
            {
                type: ChannelTypes.REMOVE_CHANNEL_MEMBER_SUCCESS,
                id: channelId,
            },
        ], 'REMOVE_CHANNEL_MEMBER.BATCH'));

        return {data: true};
    };
}


export function updateChannelHeader(channelId: string, header: string): ActionFunc {
    return async (dispatch: DispatchFunc) => {
        Client4.trackEvent('action', 'action_channels_update_header', {channel_id: channelId});

        dispatch({
            type: ChannelTypes.UPDATE_CHANNEL_HEADER,
            data: {
                channelId,
                header,
            },
        });

        return {data: true};
    };
}


export function updateChannelPurpose(channelId: string, purpose: string): ActionFunc {
    return async (dispatch: DispatchFunc) => {
        Client4.trackEvent('action', 'action_channels_update_purpose', {channel_id: channelId});

        dispatch({
            type: ChannelTypes.UPDATE_CHANNEL_PURPOSE,
            data: {
                channelId,
                purpose,
            },
        });

        return {data: true};
    };
}


export function markChannelAsRead(channelId: string, prevChannelId?: string, updateLastViewedAt = true): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId);

        // Send channel last viewed at to the server
        if (updateLastViewedAt) {
            Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId).then().catch((error) => {
                forceLogoutIfNecessary(error, dispatch, getState);
                dispatch(logError(error));
                return {error};
            });
        }

        const state = getState();
        const {channels, myMembers} = state.entities.channels;

        // Update channel member objects to set all mentions and posts as viewed
        const channel = channels[channelId];
        const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional

        // Update team member objects to set mentions and posts in channel as viewed
        const channelMember = myMembers[channelId];
        const prevChannelMember = (!prevChanManuallyUnread && prevChannelId) ? myMembers[prevChannelId] : null; // May also be null

        const actions: Action[] = [];

        if (channel && channelMember) {
            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
                data: {
                    teamId: channel.team_id,
                    channelId,
                    amount: channel.total_msg_count - channelMember.msg_count,
                    amountRoot: channel.total_msg_count_root - channelMember.msg_count_root,
                },
            });

            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
                data: {
                    teamId: channel.team_id,
                    channelId,
                    amount: channelMember.mention_count,
                    amountRoot: channelMember.mention_count_root,
                },
            });
        }

        if (channel && isManuallyUnread(getState(), channelId)) {
            actions.push({
                type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
                data: {channelId},
            });
        }

        if (prevChannel && prevChannelMember) {
            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
                data: {
                    teamId: prevChannel.team_id,
                    channelId: prevChannelId,
                    amount: prevChannel.total_msg_count - prevChannelMember.msg_count,
                    amountRoot: prevChannel.total_msg_count_root - prevChannelMember.msg_count_root,
                },
            });

            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
                data: {
                    teamId: prevChannel.team_id,
                    channelId: prevChannelId,
                    amount: prevChannelMember.mention_count,
                    amountRoot: prevChannelMember.mention_count_root,
                },
            });
        }

        if (actions.length > 0) {
            dispatch(batchActions(actions));
        }

        return {data: true};
    };
}


// Increments the number of posts in the channel by 1 and marks it as unread if necessary
export function markChannelAsUnread(teamId: string, channelId: string, mentions: string[], fetchedChannelMember = false, isRoot = false): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const {myMembers} = state.entities.channels;
        const {currentUserId} = state.entities.users;

        const actions: Action[] = [{
            type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
            data: {
                teamId,
                channelId,
                amount: 1,
                amountRoot: isRoot ? 1 : 0,
                onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
                    myMembers[channelId].notify_props.mark_unread === MarkUnread.MENTION,
                fetchedChannelMember,
            },
        }];

        if (!fetchedChannelMember) {
            actions.push({
                type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
                data: {
                    channelId,
                    amountRoot: isRoot ? 1 : 0,
                    amount: 1,
                },
            });
        }

        if (mentions && mentions.indexOf(currentUserId) !== -1) {
            actions.push({
                type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT,
                data: {
                    teamId,
                    channelId,
                    amountRoot: isRoot ? 1 : 0,
                    amount: 1,
                    fetchedChannelMember,
                },
            });
        }

        dispatch(batchActions(actions));

        return {data: true};
    };
}


export function getChannelMembersByIds(channelId: string, userIds: string[]) {
    return bindClientFunc({
        clientFunc: Client4.getChannelMembersByIds,
        onSuccess: ChannelTypes.RECEIVED_CHANNEL_MEMBERS,
        params: [
            channelId,
            userIds,
        ],
    });
}


export function favoriteChannel(channelId: string, updateCategories = true): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const config = getConfig(state);
        const currentUserId = getCurrentUserId(state);

        const preference: PreferenceType = {
            user_id: currentUserId,
            category: Preferences.CATEGORY_FAVORITE_CHANNEL,
            name: channelId,
            value: 'true',
        };

        Client4.trackEvent('action', 'action_channels_favorite');

        if (config.EnableLegacySidebar === 'true') {
            // The old sidebar is enabled, so favorite the channel by calling the preferences API
            return dispatch(savePreferences(currentUserId, [preference]));
        }

        // The new sidebar is enabled, so favorite the channel by moving it into the current team's Favorites category
        if (updateCategories) {
            const channel = getChannelSelector(state, channelId);
            const category = getCategoryInTeamByType(state, channel.team_id || getCurrentTeamId(state), CategoryTypes.FAVORITES);

            if (category) {
                await dispatch(addChannelToCategory(category.id, channelId));
            }
        }

        return dispatch({
            type: PreferenceTypes.RECEIVED_PREFERENCES,
            data: [preference],
        });
    };
}


export function unfavoriteChannel(channelId: string, updateCategories = true): ActionFunc {
    return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const config = getConfig(state);
        const currentUserId = getCurrentUserId(state);

        const preference: PreferenceType = {
            user_id: currentUserId,
            category: Preferences.CATEGORY_FAVORITE_CHANNEL,
            name: channelId,
            value: '',
        };

        Client4.trackEvent('action', 'action_channels_unfavorite');

        if (config.EnableLegacySidebar === 'true') {
            // The old sidebar is enabled, so unfavorite the channel by calling the preferences API
            return dispatch(deletePreferences(currentUserId, [preference]));
        }

        // The new sidebar is enabled, so unfavorite the channel by moving it into the current team's Channels/DMs category
        if (updateCategories) {
            const channel = getChannelSelector(state, channelId);
            const category = getCategoryInTeamByType(
                state,
                channel.team_id || getCurrentTeamId(state),
                channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL ? CategoryTypes.DIRECT_MESSAGES : CategoryTypes.CHANNELS,
            );

            if (category) {
                await dispatch(addChannelToCategory(category.id, channel.id));
            }
        }

        return dispatch({
            type: PreferenceTypes.DELETED_PREFERENCES,
            data: [preference],
        });
    };
}


export function membersMinusGroupMembers(channelID: string, groupIDs: string[], page = 0, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc {
    return bindClientFunc({
        clientFunc: Client4.channelMembersMinusGroupMembers,
        onSuccess: ChannelTypes.RECEIVED_CHANNEL_MEMBERS_MINUS_GROUP_MEMBERS,
        params: [
            channelID,
            groupIDs,
            page,
            perPage,
        ],
    });
}


export function getChannelModerations(channelId: string): ActionFunc {
    return bindClientFunc({
        clientFunc: async () => {
            const moderations = await Client4.getChannelModerations(channelId);
            return {channelId, moderations};
        },
        onSuccess: ChannelTypes.RECEIVED_CHANNEL_MODERATIONS,
        params: [
            channelId,
        ],
    });
}


export function patchChannelModerations(channelId: string, patch: ChannelModerationPatch[]): ActionFunc {
    return bindClientFunc({
        clientFunc: async () => {
            const moderations = await Client4.patchChannelModerations(channelId, patch);
            return {channelId, moderations};
        },
        onSuccess: ChannelTypes.RECEIVED_CHANNEL_MODERATIONS,
        params: [
            channelId,
        ],
    });
}


export function getChannelMemberCountsByGroup(channelId: string, includeTimezones: boolean): ActionFunc {
    return bindClientFunc({
        clientFunc: async () => {
            const channelMemberCountsByGroup = await Client4.getChannelMemberCountsByGroup(channelId, includeTimezones);
            return {channelId, memberCounts: channelMemberCountsByGroup};
        },
        onSuccess: ChannelTypes.RECEIVED_CHANNEL_MEMBER_COUNTS_BY_GROUP,
        params: [
            channelId,
        ],
    });
}


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let created;
        try {
            created = await Client4.createChannel(channel);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {
                    type: ChannelTypes.CREATE_CHANNEL_FAILURE,
                    error,
                },
                logError(error),
            ]));
            return {error};
        }

        const member = {
            channel_id: created.id,
            user_id: userId,
            roles: `${General.CHANNEL_USER_ROLE} ${General.CHANNEL_ADMIN_ROLE}`,
            last_viewed_at: 0,
            msg_count: 0,
            mention_count: 0,
            notify_props: {desktop: 'default', mark_unread: 'all'},
            last_update_at: created.create_at,
        };

        const actions: Action[] = [];
        const {channels, myMembers} = getState().entities.channels;

        if (!channels[created.id]) {
            actions.push({type: ChannelTypes.RECEIVED_CHANNEL, data: created});
        }

        if (!myMembers[created.id]) {
            actions.push({type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER, data: member});
            dispatch(loadRolesIfNeeded(member.roles.split(' ')));
        }

        dispatch(batchActions([
            ...actions,
            {
                type: ChannelTypes.CREATE_CHANNEL_SUCCESS,
            },
        ]));

        dispatch(addChannelToInitialCategory(created, true));

        return {data: created};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.CREATE_CHANNEL_REQUEST, data: null});

        let created;
        try {
            created = await Client4.createDirectChannel([userId, otherUserId]);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CREATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        const member = {
            channel_id: created.id,
            user_id: userId,
            roles: `${General.CHANNEL_USER_ROLE}`,
            last_viewed_at: 0,
            msg_count: 0,
            mention_count: 0,
            notify_props: {desktop: 'default', mark_unread: 'all'},
            last_update_at: created.create_at,
        };

        const preferences = [
            {user_id: userId, category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: otherUserId, value: 'true'},
            {user_id: userId, category: Preferences.CATEGORY_CHANNEL_OPEN_TIME, name: created.id, value: new Date().getTime().toString()},
        ];

        savePreferences(userId, preferences)(dispatch);

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: created,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
            {
                type: PreferenceTypes.RECEIVED_PREFERENCES,
                data: preferences,
            },
            {
                type: ChannelTypes.CREATE_CHANNEL_SUCCESS,
            },
            {
                type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL,
                id: created.id,
                data: [{id: userId}, {id: otherUserId}],
            },
        ]));

        dispatch(addChannelToInitialCategory(created));

        dispatch(loadRolesIfNeeded(member.roles.split(' ')));

        return {data: created};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const {currentUserId} = getState().entities.users;

        const preferences: PreferenceType[] = [
            {user_id: currentUserId, category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW, name: channelId, value: 'true'},
            {user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_OPEN_TIME, name: channelId, value: new Date().getTime().toString()},
        ];

        return dispatch(savePreferences(currentUserId, preferences));
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.CREATE_CHANNEL_REQUEST, data: null});

        const {currentUserId} = getState().entities.users;

        let created;
        try {
            created = await Client4.createGroupChannel(userIds);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CREATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        let member: Partial<ChannelMembership|undefined> = {
            channel_id: created.id,
            user_id: currentUserId,
            roles: `${General.CHANNEL_USER_ROLE}`,
            last_viewed_at: 0,
            msg_count: 0,
            mention_count: 0,
            notify_props: {desktop: 'default', mark_unread: 'all'},
            last_update_at: created.create_at,
        };

        // Check the channel previous existency: if the channel already have
        // posts is because it existed before.
        if (created.total_msg_count > 0) {
            const storeMember = getMyChannelMemberSelector(getState(), created.id);
            if (storeMember === null) {
                try {
                    member = await Client4.getMyChannelMember(created.id);
                } catch (error) {
                    // Log the error and keep going with the generated membership.
                    dispatch(logError(error));
                }
            } else {
                member = storeMember;
            }
        }

        dispatch(markGroupChannelOpen(created.id));

        const profilesInChannel = userIds.map((id) => ({id}));
        profilesInChannel.push({id: currentUserId}); // currentUserId is optionally in userIds, but the reducer will get rid of a duplicate

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: created,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
            {
                type: ChannelTypes.CREATE_CHANNEL_SUCCESS,
            },
            {
                type: UserTypes.RECEIVED_PROFILES_LIST_IN_CHANNEL,
                id: created.id,
                data: profilesInChannel,
            },
        ]));

        dispatch(addChannelToInitialCategory(created));

        dispatch(loadRolesIfNeeded((member && member.roles && member.roles.split(' ')) || []));

        return {data: created};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let updated;
        try {
            updated = await Client4.patchChannel(channelId, patch);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }
        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: updated,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: updated};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let updated;
        try {
            updated = await Client4.updateChannel(channel);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: updated,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: updated};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let updatedChannel;
        try {
            updatedChannel = await Client4.updateChannelPrivacy(channelId, privacy);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: updatedChannel,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: updatedChannel};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.UPDATE_CHANNEL_REQUEST, data: null});

        let convertedChannel;
        try {
            convertedChannel = await Client4.convertChannelToPrivate(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);

            dispatch(batchActions([
                {type: ChannelTypes.UPDATE_CHANNEL_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: convertedChannel,
            },
            {
                type: ChannelTypes.UPDATE_CHANNEL_SUCCESS,
            },
        ]));

        return {data: convertedChannel};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let data;
        try {
            data = await Client4.getChannelByNameAndTeamName(teamName, channelName, includeDeleted);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

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

        return {data};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let data;
        try {
            data = await Client4.getChannel(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

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

        return {data};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let channel;
        let member;
        try {
            const channelRequest = Client4.getChannel(channelId);
            const memberRequest = Client4.getMyChannelMember(channelId);

            channel = await channelRequest;
            member = await memberRequest;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: channel,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
        ]));
        dispatch(loadRolesIfNeeded(member.roles.split(' ')));

        return {data: {channel, member}};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({
            type: ChannelTypes.CHANNELS_REQUEST,
            data: null,
        });

        let channels;
        let channelMembers;
        const state = getState();
        const shouldFetchArchived = isMinimumServerVersion(getServerVersion(state), 5, 21);
        try {
            const channelRequest = Client4.getMyChannels(teamId, shouldFetchArchived);
            const memberRequest = Client4.getMyChannelMembers(teamId);
            channels = await channelRequest;
            channelMembers = await memberRequest;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        const {currentUserId} = state.entities.users;
        const {currentChannelId} = state.entities.channels;

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
                currentChannelId,
            },
            {
                type: ChannelTypes.CHANNELS_SUCCESS,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
                data: channelMembers,
                sync: !shouldFetchArchived,
                channels,
                remove: getChannelsIdForTeam(state, teamId),
                currentUserId,
                currentChannelId,
            },
        ]));
        const roles = new Set<string>();
        for (const member of channelMembers) {
            for (const role of member.roles.split(' ')) {
                roles.add(role);
            }
        }
        if (roles.size > 0) {
            dispatch(loadRolesIfNeeded(roles));
        }

        return {data: {channels, members: channelMembers}};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let channelMembers;
        try {
            const channelMembersRequest = Client4.getMyChannelMembers(teamId);

            channelMembers = await channelMembersRequest;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        const state = getState();
        const {currentUserId} = state.entities.users;
        const {currentChannelId} = state.entities.channels;

        dispatch({
            type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
            data: channelMembers,
            remove: getChannelsIdForTeam(getState(), teamId),
            currentUserId,
            currentChannelId,
        });
        const roles = new Set<string>();

        for (const member of channelMembers) {
            for (const role of member.roles.split(' ')) {
                roles.add(role);
            }
        }
        if (roles.size > 0) {
            dispatch(loadRolesIfNeeded(roles));
        }

        return {data: channelMembers};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const {currentUserId} = state.entities.users;
        const {channels, myMembers} = state.entities.channels;
        const channel = channels[channelId];
        const member = myMembers[channelId];

        Client4.trackEvent('action', 'action_channels_leave', {channel_id: channelId});

        dispatch({
            type: ChannelTypes.LEAVE_CHANNEL,
            data: {
                id: channelId,
                user_id: currentUserId,
                team_id: channel.team_id,
                type: channel.type,
            },
        });

        (async function removeFromChannelWrapper() {
            try {
                await Client4.removeFromChannel(currentUserId, channelId);
            } catch {
                dispatch(batchActions([
                    {
                        type: ChannelTypes.RECEIVED_CHANNEL,
                        data: channel,
                    },
                    {
                        type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                        data: member,
                    },
                ]));

                // The category here may not be the one in which the channel was originally located,
                // much less the order in which it was placed. Treating this as a transient issue
                // for the user to resolve by refreshing or leaving again.
                dispatch(addChannelToInitialCategory(channel, false));
            }
        }());

        return {data: true};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        if (!channelId && !channelName) {
            return {data: null};
        }

        let member: ChannelMembership | undefined | null;
        let channel: Channel;
        try {
            if (channelId) {
                member = await Client4.addToChannel(userId, channelId);
                channel = await Client4.getChannel(channelId);
            } else {
                channel = await Client4.getChannelByName(teamId, channelName, true);
                if ((channel.type === General.GM_CHANNEL) || (channel.type === General.DM_CHANNEL)) {
                    member = await Client4.getChannelMember(channel.id, userId);
                } else {
                    member = await Client4.addToChannel(userId, channel.id);
                }
            }
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        Client4.trackEvent('action', 'action_channels_join', {channel_id: channelId});

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNEL,
                data: channel,
            },
            {
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: member,
            },
        ]));

        dispatch(addChannelToInitialCategory(channel));

        if (member) {
            dispatch(loadRolesIfNeeded(member.roles.split(' ')));
        }

        return {data: {channel, member}};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let state = getState();
        const viewArchivedChannels = state.entities.general.config.ExperimentalViewArchivedChannels === 'true';

        try {
            await Client4.deleteChannel(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        state = getState();
        const {currentChannelId} = state.entities.channels;
        if (channelId === currentChannelId && !viewArchivedChannels) {
            const teamId = getCurrentTeamId(state);
            const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
            const channel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
            if (channel && channel.id) {
                dispatch({type: ChannelTypes.SELECT_CHANNEL, data: channel.id});
            }
        }

        dispatch({type: ChannelTypes.DELETE_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}});

        return {data: true};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        try {
            await Client4.unarchiveChannel(channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        const state = getState();
        const config = getConfig(state);
        const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
        dispatch({type: ChannelTypes.UNARCHIVED_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}});

        return {data: true};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const {currentUserId} = getState().entities.users;

        const {myPreferences} = getState().entities.preferences;
        const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channelId}`];
        const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0;
        const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId);

        if (viewTime < new Date().getTime() - (3 * 60 * 60 * 1000)) {
            const preferences = [
                {user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, name: channelId, value: new Date().getTime().toString()},
            ];
            savePreferences(currentUserId, preferences)(dispatch);
        }

        try {
            await Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));

            return {error};
        }

        const actions: Action[] = [];

        const {myMembers} = getState().entities.channels;
        const member = myMembers[channelId];
        if (member) {
            if (isManuallyUnread(getState(), channelId)) {
                actions.push({
                    type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
                    data: {channelId},
                });
            }
            actions.push({
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: {...member, last_viewed_at: new Date().getTime()},
            });
            dispatch(loadRolesIfNeeded(member.roles.split(' ')));
        }

        const prevMember = myMembers[prevChannelId];
        if (!prevChanManuallyUnread && prevMember) {
            actions.push({
                type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
                data: {...prevMember, last_viewed_at: new Date().getTime()},
            });
            dispatch(loadRolesIfNeeded(prevMember.roles.split(' ')));
        }

        dispatch(batchActions(actions));

        return {data: true};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.getChannels(teamId, page, perPage);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let channels;
        try {
            channels = await Client4.getArchivedChannels(teamId, page, perPage);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            return {error};
        }

        dispatch({
            type: ChannelTypes.RECEIVED_CHANNELS,
            teamId,
            data: channels,
        });

        return {data: channels};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null});

        let payload;
        try {
            payload = await Client4.getAllChannels(page, perPage, notAssociatedToGroup, excludeDefaultChannels, true, includeDeleted) as ChannelsWithTotalCount;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_ALL_CHANNELS,
                data: payload.channels,
            },
            {
                type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS,
            },
            {
                type: ChannelTypes.RECEIVED_TOTAL_CHANNEL_COUNT,
                data: payload.total_count,
            },
        ]));

        return {data: payload};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.getAllChannels(page, perPage, notAssociatedToGroup, excludeDefaultChannels);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_ALL_CHANNELS,
                data: channels,
            },
            {
                type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.autocompleteChannels(teamId, term);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            channels = await Client4.autocompleteChannelsForSearch(teamId, term);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null});

        let channels;
        try {
            if (archived) {
                channels = await Client4.searchArchivedChannels(teamId, term);
            } else {
                channels = await Client4.searchChannels(teamId, term);
            }
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_CHANNELS,
                teamId,
                data: channels,
            },
            {
                type: ChannelTypes.GET_CHANNELS_SUCCESS,
            },
        ]));

        return {data: channels};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null});

        let response;
        try {
            response = await Client4.searchAllChannels(term, opts) as ChannelsWithTotalCount;
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(batchActions([
                {type: ChannelTypes.GET_ALL_CHANNELS_FAILURE, error},
                logError(error),
            ]));
            return {error};
        }

        const channels = response.channels || response;

        dispatch(batchActions([
            {
                type: ChannelTypes.RECEIVED_ALL_CHANNELS,
                data: channels,
            },
            {
                type: ChannelTypes.GET_ALL_CHANNELS_SUCCESS,
            },
        ]));

        return {data: response};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        let member;
        try {
            member = await Client4.addToChannel(userId, channelId, postRootId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        Client4.trackEvent('action', 'action_channels_add_member', {channel_id: channelId});

        dispatch(batchActions([
            {
                type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL,
                data: {id: channelId, user_id: userId},
            },
            {
                type: ChannelTypes.RECEIVED_CHANNEL_MEMBER,
                data: member,
            },
            {
                type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
                id: channelId,
            },
        ], 'ADD_CHANNEL_MEMBER.BATCH'));

        return {data: member};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        try {
            await Client4.removeFromChannel(userId, channelId);
        } catch (error) {
            forceLogoutIfNecessary(error, dispatch, getState);
            dispatch(logError(error));
            return {error};
        }

        Client4.trackEvent('action', 'action_channels_remove_member', {channel_id: channelId});

        dispatch(batchActions([
            {
                type: UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL,
                data: {id: channelId, user_id: userId},
            },
            {
                type: ChannelTypes.REMOVE_CHANNEL_MEMBER_SUCCESS,
                id: channelId,
            },
        ], 'REMOVE_CHANNEL_MEMBER.BATCH'));

        return {data: true};
    }


async (dispatch: DispatchFunc) => {
        Client4.trackEvent('action', 'action_channels_update_header', {channel_id: channelId});

        dispatch({
            type: ChannelTypes.UPDATE_CHANNEL_HEADER,
            data: {
                channelId,
                header,
            },
        });

        return {data: true};
    }


async (dispatch: DispatchFunc) => {
        Client4.trackEvent('action', 'action_channels_update_purpose', {channel_id: channelId});

        dispatch({
            type: ChannelTypes.UPDATE_CHANNEL_PURPOSE,
            data: {
                channelId,
                purpose,
            },
        });

        return {data: true};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId);

        // Send channel last viewed at to the server
        if (updateLastViewedAt) {
            Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId).then().catch((error) => {
                forceLogoutIfNecessary(error, dispatch, getState);
                dispatch(logError(error));
                return {error};
            });
        }

        const state = getState();
        const {channels, myMembers} = state.entities.channels;

        // Update channel member objects to set all mentions and posts as viewed
        const channel = channels[channelId];
        const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional

        // Update team member objects to set mentions and posts in channel as viewed
        const channelMember = myMembers[channelId];
        const prevChannelMember = (!prevChanManuallyUnread && prevChannelId) ? myMembers[prevChannelId] : null; // May also be null

        const actions: Action[] = [];

        if (channel && channelMember) {
            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
                data: {
                    teamId: channel.team_id,
                    channelId,
                    amount: channel.total_msg_count - channelMember.msg_count,
                    amountRoot: channel.total_msg_count_root - channelMember.msg_count_root,
                },
            });

            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
                data: {
                    teamId: channel.team_id,
                    channelId,
                    amount: channelMember.mention_count,
                    amountRoot: channelMember.mention_count_root,
                },
            });
        }

        if (channel && isManuallyUnread(getState(), channelId)) {
            actions.push({
                type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
                data: {channelId},
            });
        }

        if (prevChannel && prevChannelMember) {
            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
                data: {
                    teamId: prevChannel.team_id,
                    channelId: prevChannelId,
                    amount: prevChannel.total_msg_count - prevChannelMember.msg_count,
                    amountRoot: prevChannel.total_msg_count_root - prevChannelMember.msg_count_root,
                },
            });

            actions.push({
                type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
                data: {
                    teamId: prevChannel.team_id,
                    channelId: prevChannelId,
                    amount: prevChannelMember.mention_count,
                    amountRoot: prevChannelMember.mention_count_root,
                },
            });
        }

        if (actions.length > 0) {
            dispatch(batchActions(actions));
        }

        return {data: true};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const {myMembers} = state.entities.channels;
        const {currentUserId} = state.entities.users;

        const actions: Action[] = [{
            type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
            data: {
                teamId,
                channelId,
                amount: 1,
                amountRoot: isRoot ? 1 : 0,
                onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
                    myMembers[channelId].notify_props.mark_unread === MarkUnread.MENTION,
                fetchedChannelMember,
            },
        }];

        if (!fetchedChannelMember) {
            actions.push({
                type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
                data: {
                    channelId,
                    amountRoot: isRoot ? 1 : 0,
                    amount: 1,
                },
            });
        }

        if (mentions && mentions.indexOf(currentUserId) !== -1) {
            actions.push({
                type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT,
                data: {
                    teamId,
                    channelId,
                    amountRoot: isRoot ? 1 : 0,
                    amount: 1,
                    fetchedChannelMember,
                },
            });
        }

        dispatch(batchActions(actions));

        return {data: true};
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const config = getConfig(state);
        const currentUserId = getCurrentUserId(state);

        const preference: PreferenceType = {
            user_id: currentUserId,
            category: Preferences.CATEGORY_FAVORITE_CHANNEL,
            name: channelId,
            value: 'true',
        };

        Client4.trackEvent('action', 'action_channels_favorite');

        if (config.EnableLegacySidebar === 'true') {
            // The old sidebar is enabled, so favorite the channel by calling the preferences API
            return dispatch(savePreferences(currentUserId, [preference]));
        }

        // The new sidebar is enabled, so favorite the channel by moving it into the current team's Favorites category
        if (updateCategories) {
            const channel = getChannelSelector(state, channelId);
            const category = getCategoryInTeamByType(state, channel.team_id || getCurrentTeamId(state), CategoryTypes.FAVORITES);

            if (category) {
                await dispatch(addChannelToCategory(category.id, channelId));
            }
        }

        return dispatch({
            type: PreferenceTypes.RECEIVED_PREFERENCES,
            data: [preference],
        });
    }


async (dispatch: DispatchFunc, getState: GetStateFunc) => {
        const state = getState();
        const config = getConfig(state);
        const currentUserId = getCurrentUserId(state);

        const preference: PreferenceType = {
            user_id: currentUserId,
            category: Preferences.CATEGORY_FAVORITE_CHANNEL,
            name: channelId,
            value: '',
        };

        Client4.trackEvent('action', 'action_channels_unfavorite');

        if (config.EnableLegacySidebar === 'true') {
            // The old sidebar is enabled, so unfavorite the channel by calling the preferences API
            return dispatch(deletePreferences(currentUserId, [preference]));
        }

        // The new sidebar is enabled, so unfavorite the channel by moving it into the current team's Channels/DMs category
        if (updateCategories) {
            const channel = getChannelSelector(state, channelId);
            const category = getCategoryInTeamByType(
                state,
                channel.team_id || getCurrentTeamId(state),
                channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL ? CategoryTypes.DIRECT_MESSAGES : CategoryTypes.CHANNELS,
            );

            if (category) {
                await dispatch(addChannelToCategory(category.id, channel.id));
            }
        }

        return dispatch({
            type: PreferenceTypes.DELETED_PREFERENCES,
            data: [preference],
        });
    }


async () => {
            const moderations = await Client4.getChannelModerations(channelId);
            return {channelId, moderations};
        }


async () => {
            const moderations = await Client4.patchChannelModerations(channelId, patch);
            return {channelId, moderations};
        }


async () => {
            const channelMemberCountsByGroup = await Client4.getChannelMemberCountsByGroup(channelId, includeTimezones);
            return {channelId, memberCounts: channelMemberCountsByGroup};
        }


export class ExtendedUnitStatusResponse extends ApplicationDataResponse {

  private readonly _id: number[] = [];
  private readonly _state: number[] = [];
  private readonly _timeRemaining: number[] = [];

  constructor(message: Buffer) {
    super(message);

    const recordLength = message[4];
    const unitCount = (message[1] - 3) / recordLength;
    let offset = 5;
    for(let i = 1; i <= unitCount; i++) {
      this._id.push(message.readUInt16BE(offset));
      this._state.push(message[offset + 2]);
      this._timeRemaining.push(message.readUInt16BE(offset + 3));
      offset += recordLength;
    }
  }

  get id(): number[] {
    return this._id;
  }

  get state(): number[] {
    return this._state;
  }

  get timeRemaining(): number[] {
    return this._timeRemaining;
  }
}


/*!
   Copyright 2019 Ron Buckton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/*@internal*/
export function defineTag<T extends object>(target: T, tag: string): T & Tagged {
    Object.defineProperty(target, Symbol.toStringTag, {
        writable: false,
        enumerable: false,
        configurable: true,
        value: tag
    });
    return target as T & { [Symbol.toStringTag]: string };
}


/*@internal*/
export function Tag(tag?: string) {
    return function (target: Function) {
        const tagString = tag || target.name;
        if (!tagString) throw new Error("Invalid tag");
        defineTag(target.prototype, tagString);
    }
}


/*@internal*/
export interface Tagged {
    [Symbol.toStringTag]: string;
}


function mockSimpleGrpcMethod(
  expectedRequest: {},
  response: {} | null,
  error: FakeError | null
) {
  return (actualRequest: {}, options: {}, callback: Callback) => {
    assert.deepStrictEqual(actualRequest, expectedRequest);
    if (error) {
      callback(error);
    } else if (response) {
      callback(null, response);
    } else {
      callback(null);
    }
  };
}


function mockLongRunningGrpcMethod(
  expectedRequest: {},
  response: {} | null,
  error?: {} | null
) {
  return (request: {}) => {
    assert.deepStrictEqual(request, expectedRequest);
    const mockOperation = {
      promise() {
        return new Promise((resolve, reject) => {
          if (error) {
            reject(error);
          } else {
            resolve([response]);
          }
        });
      },
    };
    return Promise.resolve([mockOperation]);
  };
}


(actualRequest: {}, options: {}, callback: Callback) => {
    assert.deepStrictEqual(actualRequest, expectedRequest);
    if (error) {
      callback(error);
    } else if (response) {
      callback(null, response);
    } else {
      callback(null);
    }
  }


(request: {}) => {
    assert.deepStrictEqual(request, expectedRequest);
    const mockOperation = {
      promise() {
        return new Promise((resolve, reject) => {
          if (error) {
            reject(error);
          } else {
            resolve([response]);
          }
        });
      },
    };
    return Promise.resolve([mockOperation]);
  }


(resolve, reject) => {
          if (error) {
            reject(error);
          } else {
            resolve([response]);
          }
        }


() => {
    const servicePath = cloudredisModule.v1.CloudRedisClient.servicePath;
    assert(servicePath);
  }


() => {
    const apiEndpoint = cloudredisModule.v1.CloudRedisClient.apiEndpoint;
    assert(apiEndpoint);
  }


() => {
    const port = cloudredisModule.v1.CloudRedisClient.port;
    assert(port);
    assert(typeof port === 'number');
  }


() => {
    const client = new cloudredisModule.v1.CloudRedisClient();
    assert(client);
  }


() => {
    const client = new cloudredisModule.v1.CloudRedisClient({
      fallback: true,
    });
    assert(client);
  }


async () => {
    const client = new cloudredisModule.v1.CloudRedisClient({
      credentials: {client_email: 'bogus', private_key: 'bogus'},
      projectId: 'bogus',
    });
    assert.strictEqual(client.cloudRedisStub, undefined);
    await client.initialize();
    assert(client.cloudRedisStub);
  }


() => {
    const client = new cloudredisModule.v1.CloudRedisClient({
      credentials: {client_email: 'bogus', private_key: 'bogus'},
      projectId: 'bogus',
    });
    client.close();
  }


() => {
    it('invokes getInstance without error', done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.IGetInstanceRequest = {};
      request.name = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.getInstance = mockSimpleGrpcMethod(
        request,
        expectedResponse,
        null
      );
      client.getInstance(request, (err: {}, response: {}) => {
        assert.ifError(err);
        assert.deepStrictEqual(response, expectedResponse);
        done();
      });
    });

    it('invokes getInstance with error', done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.IGetInstanceRequest = {};
      request.name = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.getInstance = mockSimpleGrpcMethod(
        request,
        null,
        error
      );
      client.getInstance(request, (err: FakeError, response: {}) => {
        assert(err instanceof FakeError);
        assert.strictEqual(err.code, FAKE_STATUS_CODE);
        assert(typeof response === 'undefined');
        done();
      });
    });
  }


done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.IGetInstanceRequest = {};
      request.name = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.getInstance = mockSimpleGrpcMethod(
        request,
        expectedResponse,
        null
      );
      client.getInstance(request, (err: {}, response: {}) => {
        assert.ifError(err);
        assert.deepStrictEqual(response, expectedResponse);
        done();
      });
    }


(err: {}, response: {}) => {
        assert.ifError(err);
        assert.deepStrictEqual(response, expectedResponse);
        done();
      }


done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.IGetInstanceRequest = {};
      request.name = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.getInstance = mockSimpleGrpcMethod(
        request,
        null,
        error
      );
      client.getInstance(request, (err: FakeError, response: {}) => {
        assert(err instanceof FakeError);
        assert.strictEqual(err.code, FAKE_STATUS_CODE);
        assert(typeof response === 'undefined');
        done();
      });
    }


(err: FakeError, response: {}) => {
        assert(err instanceof FakeError);
        assert.strictEqual(err.code, FAKE_STATUS_CODE);
        assert(typeof response === 'undefined');
        done();
      }


() => {
    it('invokes createInstance without error', done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.ICreateInstanceRequest = {};
      request.parent = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.createInstance = mockLongRunningGrpcMethod(
        request,
        expectedResponse
      );
      client
        .createInstance(request)
        .then((responses: [Operation]) => {
          const operation = responses[0];
          return operation ? operation.promise() : {};
        })
        .then((responses: [Operation]) => {
          assert.deepStrictEqual(responses[0], expectedResponse);
          done();
        })
        .catch((err: {}) => {
          done(err);
        });
    });

    it('invokes createInstance with error', done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.ICreateInstanceRequest = {};
      request.parent = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.createInstance = mockLongRunningGrpcMethod(
        request,
        null,
        error
      );
      client
        .createInstance(request)
        .then((responses: [Operation]) => {
          const operation = responses[0];
          return operation ? operation.promise() : {};
        })
        .then(() => {
          assert.fail();
        })
        .catch((err: FakeError) => {
          assert(err instanceof FakeError);
          assert.strictEqual(err.code, FAKE_STATUS_CODE);
          done();
        });
    });
  }


done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.ICreateInstanceRequest = {};
      request.parent = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.createInstance = mockLongRunningGrpcMethod(
        request,
        expectedResponse
      );
      client
        .createInstance(request)
        .then((responses: [Operation]) => {
          const operation = responses[0];
          return operation ? operation.promise() : {};
        })
        .then((responses: [Operation]) => {
          assert.deepStrictEqual(responses[0], expectedResponse);
          done();
        })
        .catch((err: {}) => {
          done(err);
        });
    }


(responses: [Operation]) => {
          const operation = responses[0];
          return operation ? operation.promise() : {};
        }


(responses: [Operation]) => {
          assert.deepStrictEqual(responses[0], expectedResponse);
          done();
        }


(err: {}) => {
          done(err);
        }


done => {
      const client = new cloudredisModule.v1.CloudRedisClient({
        credentials: {client_email: 'bogus', private_key: 'bogus'},
        projectId: 'bogus',
      });
      // Initialize client before mocking
      client.initialize();
      // Mock request
      const request: protosTypes.google.cloud.redis.v1.ICreateInstanceRequest = {};
      request.parent = '';
      // Mock response
      const expectedResponse = {};
      // Mock gRPC layer
      client._innerApiCalls.createInstance = mockLongRunningGrpcMethod(
        request,
        null,
        error
      );
      client
        .createInstance(request)
        .then((responses: [Operation]) => {
          const operation = responses[0];
          return operation ? operation.promise() : {};
        })
        .then(() => {
          assert.fail();
        })
        .catch((err: FakeError) => {
          assert(err instanceof FakeError);
          assert.strictEqual(err.code, FAKE_STATUS_CODE);
          done();
        });
    }


() => {
          assert.fail();
        }


(err: FakeError) => {
          assert(err instanceof FakeError);
          assert.strictEqual(err.code, FAKE_STATUS_CODE);
          done();
        }