mattermost-redux
Version:
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
823 lines (721 loc) • 24.3 kB
text/typescript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {ChannelTypes, UserTypes, SchemeTypes, GroupTypes, PostTypes} from 'action_types';
import {General} from '../../constants';
import {MarkUnread} from 'constants/channels';
import {GenericAction} from 'types/actions';
import {
Channel,
ChannelMembership,
ChannelStats,
ChannelMemberCountByGroup,
ChannelMemberCountsByGroup,
} from 'types/channels';
import {
RelationOneToMany,
RelationOneToOne,
IDMappedObjects,
UserIDMappedObjects,
} from 'types/utilities';
import {Team} from 'types/teams';
function removeMemberFromChannels(state: RelationOneToOne<Channel, UserIDMappedObjects<ChannelMembership>>, action: GenericAction) {
const nextState = {...state};
Object.keys(state).forEach((channel) => {
nextState[channel] = {...nextState[channel]};
delete nextState[channel][action.data.user_id];
});
return nextState;
}
function channelListToSet(state: any, action: GenericAction) {
const nextState = {...state};
action.data.forEach((channel: Channel) => {
const nextSet = new Set(nextState[channel.team_id]);
nextSet.add(channel.id);
nextState[channel.team_id] = nextSet;
});
return nextState;
}
function removeChannelFromSet(state: any, action: GenericAction) {
const id = action.data.team_id;
const nextSet = new Set(state[id]);
nextSet.delete(action.data.id);
return {
...state,
[id]: nextSet,
};
}
function currentChannelId(state = '', action: GenericAction) {
switch (action.type) {
case ChannelTypes.SELECT_CHANNEL:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
}
function channels(state: IDMappedObjects<Channel> = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_CHANNEL:
if (state[action.data.id] && action.data.type === General.DM_CHANNEL) {
action.data.display_name = action.data.display_name || state[action.data.id].display_name;
}
return {
...state,
[action.data.id]: action.data,
};
case ChannelTypes.RECEIVED_CHANNELS:
case ChannelTypes.RECEIVED_ALL_CHANNELS:
case SchemeTypes.RECEIVED_SCHEME_CHANNELS: {
const nextState = {...state};
for (let channel of action.data) {
if (state[channel.id] && channel.type === General.DM_CHANNEL && !channel.display_name) {
channel = {...channel, display_name: state[channel.id].display_name};
}
nextState[channel.id] = channel;
}
return nextState;
}
case ChannelTypes.RECEIVED_CHANNEL_DELETED: {
const {id, deleteAt} = action.data;
if (!state[id]) {
return state;
}
return {
...state,
[id]: {
...state[id],
delete_at: deleteAt,
},
};
}
case ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED: {
const {id} = action.data;
if (!state[id]) {
return state;
}
return {
...state,
[id]: {
...state[id],
delete_at: 0,
},
};
}
case ChannelTypes.UPDATE_CHANNEL_HEADER: {
const {channelId, header} = action.data;
if (!state[channelId]) {
return state;
}
return {
...state,
[channelId]: {
...state[channelId],
header,
},
};
}
case ChannelTypes.UPDATE_CHANNEL_PURPOSE: {
const {channelId, purpose} = action.data;
if (!state[channelId]) {
return state;
}
return {
...state,
[channelId]: {
...state[channelId],
purpose,
},
};
}
case ChannelTypes.LEAVE_CHANNEL: {
if (action.data && action.data.type === General.PRIVATE_CHANNEL) {
const nextState = {...state};
Reflect.deleteProperty(nextState, action.data.id);
return nextState;
}
return state;
}
case ChannelTypes.INCREMENT_TOTAL_MSG_COUNT: {
const {channelId, amount} = action.data;
const channel = state[channelId];
if (!channel) {
return state;
}
return {
...state,
[channelId]: {
...channel,
total_msg_count: channel.total_msg_count + amount,
},
};
}
case PostTypes.RECEIVED_NEW_POST: {
const {channel_id, create_at} = action.data; //eslint-disable-line @typescript-eslint/naming-convention
const channel = state[channel_id];
if (!channel) {
return state;
}
return {
...state,
[channel_id]: {
...channel,
last_post_at: Math.max(create_at, channel.last_post_at),
},
};
}
case ChannelTypes.UPDATED_CHANNEL_SCHEME: {
const {channelId, schemeId} = action.data;
const channel = state[channelId];
if (!channel) {
return state;
}
return {...state, [channelId]: {...channel, scheme_id: schemeId}};
}
case ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS: { // Used by the mobile app
const nextState = {...state};
const myChannels: Channel[] = action.data.channels;
let hasNewValues = false;
if (myChannels && myChannels.length) {
hasNewValues = true;
myChannels.forEach((c: Channel) => {
nextState[c.id] = c;
});
}
return hasNewValues ? nextState : state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
}
function channelsInTeam(state: RelationOneToMany<Team, Channel> = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_CHANNEL: {
const nextSet = new Set(state[action.data.team_id]);
nextSet.add(action.data.id);
return {
...state,
[action.data.team_id]: nextSet,
};
}
case ChannelTypes.RECEIVED_CHANNELS: {
return channelListToSet(state, action);
}
case ChannelTypes.LEAVE_CHANNEL: {
if (action.data && action.data.type === General.PRIVATE_CHANNEL) {
return removeChannelFromSet(state, action);
}
return state;
}
case ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS: { // Used by the mobile app
const values: GenericAction = {
type: action.type,
teamId: action.data.teamId,
sync: action.data.sync,
data: action.data.channels,
};
return channelListToSet(state, values);
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
}
function myMembers(state: RelationOneToOne<Channel, ChannelMembership> = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER: {
const channelMember = action.data;
return {
...state,
[channelMember.channel_id]: channelMember,
};
}
case ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS: {
const nextState: any = {...state};
const remove = action.remove as string[];
if (remove) {
remove.forEach((id: string) => {
Reflect.deleteProperty(nextState, id);
});
}
for (const cm of action.data) {
nextState[cm.channel_id] = cm;
}
return nextState;
}
case ChannelTypes.RECEIVED_CHANNEL_PROPS: {
const member = {...state[action.data.channel_id]};
member.notify_props = action.data.notifyProps;
return {
...state,
[action.data.channel_id]: member,
};
}
case ChannelTypes.SET_CHANNEL_MUTED: {
const {channelId, muted} = action.data;
if (!state[channelId]) {
return state;
}
return {
...state,
[channelId]: {
...state[channelId],
notify_props: {
...state[channelId].notify_props,
mark_unread: muted ? MarkUnread.MENTION : MarkUnread.ALL,
},
},
};
}
case ChannelTypes.INCREMENT_UNREAD_MSG_COUNT: {
const {
channelId,
amount,
onlyMentions,
fetchedChannelMember,
} = action.data;
const member = state[channelId];
if (!member) {
// Don't keep track of unread posts until we've loaded the actual channel member
return state;
}
if (!onlyMentions) {
// Incrementing the msg_count marks the channel as read, so don't do that if these posts should be unread
return state;
}
if (fetchedChannelMember) {
// We've already updated the channel member with the correct msg_count
return state;
}
return {
...state,
[channelId]: {
...member,
msg_count: member.msg_count + amount,
},
};
}
case ChannelTypes.DECREMENT_UNREAD_MSG_COUNT: {
const {channelId, amount} = action.data;
const member = state[channelId];
if (!member) {
// Don't keep track of unread posts until we've loaded the actual channel member
return state;
}
return {
...state,
[channelId]: {
...member,
msg_count: member.msg_count + amount,
},
};
}
case ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT: {
const {
channelId,
amount,
fetchedChannelMember,
} = action.data;
const member = state[channelId];
if (!member) {
// Don't keep track of unread posts until we've loaded the actual channel member
return state;
}
if (fetchedChannelMember) {
// We've already updated the channel member with the correct msg_count
return state;
}
return {
...state,
[channelId]: {
...member,
mention_count: member.mention_count + amount,
},
};
}
case ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT: {
const {channelId, amount} = action.data;
const member = state[channelId];
if (!member) {
// Don't keep track of unread posts until we've loaded the actual channel member
return state;
}
return {
...state,
[channelId]: {
...member,
mention_count: Math.max(member.mention_count - amount, 0),
},
};
}
case ChannelTypes.RECEIVED_LAST_VIEWED_AT: {
const {data} = action;
let member = state[data.channel_id];
member = {
...member,
last_viewed_at: data.last_viewed_at,
};
return {
...state,
[action.data.channel_id]: member,
};
}
case ChannelTypes.LEAVE_CHANNEL: {
const nextState = {...state};
if (action.data) {
Reflect.deleteProperty(nextState, action.data.id);
return nextState;
}
return state;
}
case ChannelTypes.UPDATED_CHANNEL_MEMBER_SCHEME_ROLES: {
return updateChannelMemberSchemeRoles(state, action);
}
case ChannelTypes.POST_UNREAD_SUCCESS: {
const data = action.data;
const channelState = state[data.channelId];
if (!channelState) {
return state;
}
return {...state, [data.channelId]: {...channelState, msg_count: data.msgCount, mention_count: data.mentionCount, last_viewed_at: data.lastViewedAt}};
}
case ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS: { // Used by the mobile app
const nextState: any = {...state};
const current = Object.values(nextState);
const {sync, channelMembers} = action.data;
let hasNewValues = channelMembers && channelMembers.length > 0;
// Remove existing channel memberships when the user is no longer a member
if (sync) {
current.forEach((member: ChannelMembership) => {
const id = member.channel_id;
if (channelMembers.find((cm: ChannelMembership) => cm.channel_id === id)) {
delete nextState[id];
hasNewValues = true;
}
});
}
if (hasNewValues) {
channelMembers.forEach((cm: ChannelMembership) => {
const id: string = cm.channel_id;
nextState[id] = cm;
});
return nextState;
}
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
}
function membersInChannel(state: RelationOneToOne<Channel, UserIDMappedObjects<ChannelMembership>> = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER:
case ChannelTypes.RECEIVED_CHANNEL_MEMBER: {
const member = action.data;
const members = {...(state[member.channel_id] || {})};
members[member.user_id] = member;
return {
...state,
[member.channel_id]: members,
};
}
case ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS:
case ChannelTypes.RECEIVED_CHANNEL_MEMBERS: {
const nextState = {...state};
const remove = action.remove as string[];
const currentUserId = action.currentUserId;
if (remove && currentUserId) {
remove.forEach((id) => {
if (nextState[id]) {
Reflect.deleteProperty(nextState[id], currentUserId);
}
});
}
for (const cm of action.data) {
if (nextState[cm.channel_id]) {
nextState[cm.channel_id] = {...nextState[cm.channel_id]};
} else {
nextState[cm.channel_id] = {};
}
nextState[cm.channel_id][cm.user_id] = cm;
}
return nextState;
}
case UserTypes.PROFILE_NO_LONGER_VISIBLE:
return removeMemberFromChannels(state, action);
case ChannelTypes.LEAVE_CHANNEL:
case ChannelTypes.REMOVE_MEMBER_FROM_CHANNEL:
case UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL: {
if (action.data) {
const data = action.data;
const members = {...(state[data.id] || {})};
if (state[data.id]) {
Reflect.deleteProperty(members, data.user_id);
return {
...state,
[data.id]: members,
};
}
}
return state;
}
case ChannelTypes.UPDATED_CHANNEL_MEMBER_SCHEME_ROLES: {
return updateChannelMemberSchemeRoles(state, action);
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
}
function stats(state: RelationOneToOne<Channel, ChannelStats> = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_CHANNEL_STATS: {
const nextState = {...state};
const stat = action.data;
nextState[stat.channel_id] = stat;
return nextState;
}
case ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS: {
const nextState = {...state};
const id = action.id;
const nextStat = nextState[id];
if (nextStat) {
const count = nextStat.member_count + 1;
return {
...nextState,
[id]: {
...nextStat,
member_count: count,
},
};
}
return state;
}
case ChannelTypes.REMOVE_CHANNEL_MEMBER_SUCCESS: {
const nextState = {...state};
const id = action.id;
const nextStat = nextState[id];
if (nextStat) {
const count = nextStat.member_count - 1;
return {
...nextState,
[id]: {
...nextStat,
member_count: count || 1,
},
};
}
return state;
}
case ChannelTypes.INCREMENT_PINNED_POST_COUNT: {
const nextState = {...state};
const id = action.id;
const nextStat = nextState[id];
if (nextStat) {
const count = nextStat.pinnedpost_count + 1;
return {
...nextState,
[id]: {
...nextStat,
pinnedpost_count: count,
},
};
}
return state;
}
case ChannelTypes.DECREMENT_PINNED_POST_COUNT: {
const nextState = {...state};
const id = action.id;
const nextStat = nextState[id];
if (nextStat) {
const count = nextStat.pinnedpost_count - 1;
return {
...nextState,
[id]: {
...nextStat,
pinnedpost_count: count,
},
};
}
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
}
function groupsAssociatedToChannel(state: any = {}, action: GenericAction) {
switch (action.type) {
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM: {
const {groupsByChannelId} = action.data;
const nextState = {...state};
for (const channelID of Object.keys(groupsByChannelId)) {
if (groupsByChannelId[channelID]) {
const associatedGroupIDs = new Set<string>([]);
for (const group of groupsByChannelId[channelID]) {
associatedGroupIDs.add(group.id);
}
const ids = Array.from(associatedGroupIDs);
nextState[channelID] = {ids, totalCount: ids.length};
}
}
return nextState;
}
case GroupTypes.RECEIVED_GROUP_ASSOCIATED_TO_CHANNEL: {
const {channelID, groups} = action.data;
const nextState = {...state};
const associatedGroupIDs = new Set(state[channelID] ? state[channelID].ids : []);
for (const group of groups) {
associatedGroupIDs.add(group.id);
}
nextState[channelID] = {ids: Array.from(associatedGroupIDs), totalCount: associatedGroupIDs.size};
return nextState;
}
case GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_CHANNEL: {
const {channelID, groups, totalGroupCount} = action.data;
const nextState = {...state};
const associatedGroupIDs = new Set<string>([]);
for (const group of groups) {
associatedGroupIDs.add(group.id);
}
nextState[channelID] = {ids: Array.from(associatedGroupIDs), totalCount: totalGroupCount};
return nextState;
}
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNEL: {
const {channelID, groups} = action.data;
const nextState = {...state};
const associatedGroupIDs = new Set<string>([]);
for (const group of groups) {
associatedGroupIDs.add(group.id);
}
const ids = Array.from(associatedGroupIDs);
nextState[channelID] = {ids, totalCount: ids.length};
return nextState;
}
case GroupTypes.RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL:
case GroupTypes.RECEIVED_GROUPS_NOT_ASSOCIATED_TO_CHANNEL: {
const {channelID, groups} = action.data;
const nextState = {...state};
const associatedGroupIDs = new Set(state[channelID] ? state[channelID].ids : []);
for (const group of groups) {
associatedGroupIDs.delete(group.id);
}
nextState[channelID] = {ids: Array.from(associatedGroupIDs), totalCount: associatedGroupIDs.size};
return nextState;
}
default:
return state;
}
}
function updateChannelMemberSchemeRoles(state: any, action: GenericAction) {
const {channelId, userId, isSchemeUser, isSchemeAdmin} = action.data;
const channel = state[channelId];
if (channel) {
const member = channel[userId];
if (member) {
return {
...state,
[channelId]: {
...state[channelId],
[userId]: {
...state[channelId][userId],
scheme_user: isSchemeUser,
scheme_admin: isSchemeAdmin,
},
},
};
}
}
return state;
}
function totalCount(state = 0, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_TOTAL_CHANNEL_COUNT: {
return action.data;
}
default:
return state;
}
}
export function manuallyUnread(state: RelationOneToOne<Channel, boolean> = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.REMOVE_MANUALLY_UNREAD: {
if (state[action.data.channelId]) {
const newState = {...state};
delete newState[action.data.channelId];
return newState;
}
return state;
}
case UserTypes.LOGOUT_SUCCESS: {
// user is logging out, remove any reference
return {};
}
case ChannelTypes.ADD_MANUALLY_UNREAD:
case ChannelTypes.POST_UNREAD_SUCCESS: {
return {...state, [action.data.channelId]: true};
}
default:
return state;
}
}
export function channelModerations(state: any = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_CHANNEL_MODERATIONS: {
const {channelId, moderations} = action.data;
return {
...state,
[channelId]: moderations,
};
}
default:
return state;
}
}
export function channelMemberCountsByGroup(state: any = {}, action: GenericAction) {
switch (action.type) {
case ChannelTypes.RECEIVED_CHANNEL_MEMBER_COUNTS_BY_GROUP: {
const {channelId, memberCounts} = action.data;
const memberCountsByGroup: ChannelMemberCountsByGroup = {};
memberCounts.forEach((channelMemberCount: ChannelMemberCountByGroup) => {
memberCountsByGroup[channelMemberCount.group_id] = channelMemberCount;
});
return {
...state,
[channelId]: memberCountsByGroup,
};
}
default:
return state;
}
}
export default combineReducers({
// the current selected channel
currentChannelId,
// object where every key is the channel id and has and object with the channel detail
channels,
// object where every key is a team id and has set of channel ids that are on the team
channelsInTeam,
// object where every key is the channel id and has an object with the channel members detail
myMembers,
// object where every key is the channel id with an object where key is a user id and has an object with the channel members detail
membersInChannel,
// object where every key is the channel id and has an object with the channel stats
stats,
groupsAssociatedToChannel,
totalCount,
// object where every key is the channel id, if present means a user requested to mark that channel as unread.
manuallyUnread,
// object where every key is the channel id and has an object with the channel moderations
channelModerations,
// object where every key is the channel id containing map of <group_id: ChannelMemberCountByGroup>
channelMemberCountsByGroup,
});